diff --git a/osu.Android.props b/osu.Android.props index aad8cf10d0..155a21bacb 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs b/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs new file mode 100644 index 0000000000..af6681e9cf --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Utility; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Settings +{ + public class TestSceneLatencyCertifierScreen : ScreenTestScene + { + private LatencyCertifierScreen latencyCertifier = null!; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("Load screen", () => LoadScreen(latencyCertifier = new LatencyCertifierScreen())); + AddUntilStep("wait for load", () => latencyCertifier.IsLoaded); + } + + [Test] + public void TestCertification() + { + checkDifficulty(1); + clickUntilResults(true); + continueFromResults(); + checkDifficulty(2); + + clickUntilResults(false); + continueFromResults(); + checkDifficulty(1); + + clickUntilResults(true); + AddAssert("check at results", () => !latencyCertifier.ChildrenOfType().Any()); + AddAssert("check no buttons", () => !latencyCertifier.ChildrenOfType().Any()); + checkDifficulty(1); + } + + private void continueFromResults() + { + AddAssert("check at results", () => !latencyCertifier.ChildrenOfType().Any()); + AddStep("hit enter to continue", () => InputManager.Key(Key.Enter)); + } + + private void checkDifficulty(int difficulty) + { + AddAssert($"difficulty is {difficulty}", () => latencyCertifier.DifficultyLevel == difficulty); + } + + private void clickUntilResults(bool clickCorrect) + { + AddUntilStep("click correct button until results", () => + { + var latencyArea = latencyCertifier + .ChildrenOfType() + .SingleOrDefault(a => clickCorrect ? a.TargetFrameRate == null : a.TargetFrameRate != null); + + // reached results + if (latencyArea == null) + return true; + + latencyArea.ChildrenOfType().Single().TriggerClick(); + return false; + }); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs index 8833420523..2b845e9d6b 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/GeneralSettings.cs @@ -9,6 +9,7 @@ using osu.Framework.Screens; using osu.Game.Localisation; using osu.Game.Screens; using osu.Game.Screens.Import; +using osu.Game.Screens.Utility; namespace osu.Game.Overlays.Settings.Sections.DebugSettings { @@ -30,13 +31,18 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings { LabelText = DebugSettingsStrings.BypassFrontToBackPass, Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) + }, + new SettingsButton + { + Text = DebugSettingsStrings.ImportFiles, + Action = () => performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) + }, + new SettingsButton + { + Text = @"Run latency certifier", + Action = () => performer?.PerformFromScreen(menu => menu.Push(new LatencyCertifierScreen())) } }; - Add(new SettingsButton - { - Text = DebugSettingsStrings.ImportFiles, - Action = () => performer?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) - }); } } } diff --git a/osu.Game/Screens/Utility/ButtonWithKeyBind.cs b/osu.Game/Screens/Utility/ButtonWithKeyBind.cs new file mode 100644 index 0000000000..ef87e0bca7 --- /dev/null +++ b/osu.Game/Screens/Utility/ButtonWithKeyBind.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable +using osu.Framework.Allocation; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osuTK.Input; + +namespace osu.Game.Screens.Utility +{ + public class ButtonWithKeyBind : SettingsButton + { + private readonly Key key; + + public ButtonWithKeyBind(Key key) + { + this.key = key; + } + + public new LocalisableString Text + { + get => base.Text; + set => base.Text = $"{value} (Press {key.ToString().Replace("Number", string.Empty)})"; + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (!e.Repeat && e.Key == key) + { + TriggerClick(); + return true; + } + + return base.OnKeyDown(e); + } + + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Height = 100; + SpriteText.Colour = overlayColourProvider.Background6; + SpriteText.Font = OsuFont.TorusAlternate.With(size: 34); + } + } +} diff --git a/osu.Game/Screens/Utility/LatencyArea.cs b/osu.Game/Screens/Utility/LatencyArea.cs new file mode 100644 index 0000000000..2ef48bb571 --- /dev/null +++ b/osu.Game/Screens/Utility/LatencyArea.cs @@ -0,0 +1,241 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Game.Overlays; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Screens.Utility +{ + public class LatencyArea : CompositeDrawable + { + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } = null!; + + public Action? ReportUserBest { get; set; } + + private Drawable? background; + + private readonly Key key; + + public readonly int? TargetFrameRate; + + public readonly BindableBool IsActiveArea = new BindableBool(); + + public LatencyArea(Key key, int? targetFrameRate) + { + this.key = key; + TargetFrameRate = targetFrameRate; + + RelativeSizeAxes = Axes.Both; + Masking = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + InternalChildren = new[] + { + background = new Box + { + Colour = overlayColourProvider.Background6, + RelativeSizeAxes = Axes.Both, + }, + new ButtonWithKeyBind(key) + { + Text = "Feels better", + Y = 20, + Width = 0.8f, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = () => ReportUserBest?.Invoke(), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new LatencyMovableBox(IsActiveArea) + { + RelativeSizeAxes = Axes.Both, + }, + new LatencyCursorContainer(IsActiveArea) + { + RelativeSizeAxes = Axes.Both, + }, + } + }, + }; + + IsActiveArea.BindValueChanged(active => + { + background.FadeColour(active.NewValue ? overlayColourProvider.Background4 : overlayColourProvider.Background6, 200, Easing.OutQuint); + }, true); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + IsActiveArea.Value = true; + return base.OnMouseMove(e); + } + + private double lastFrameTime; + + public override bool UpdateSubTree() + { + double elapsed = Clock.CurrentTime - lastFrameTime; + if (TargetFrameRate.HasValue && elapsed < 1000.0 / TargetFrameRate) + return false; + + lastFrameTime = Clock.CurrentTime; + + return base.UpdateSubTree(); + } + + public class LatencyMovableBox : CompositeDrawable + { + private Box box = null!; + private InputManager inputManager = null!; + + private readonly BindableBool isActive; + + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } = null!; + + public LatencyMovableBox(BindableBool isActive) + { + this.isActive = isActive; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + + InternalChild = box = new Box + { + Size = new Vector2(40), + RelativePositionAxes = Axes.Both, + Position = new Vector2(0.5f), + Origin = Anchor.Centre, + Colour = overlayColourProvider.Colour1, + }; + } + + protected override bool OnHover(HoverEvent e) => false; + + private double? lastFrameTime; + + protected override void Update() + { + base.Update(); + + if (!isActive.Value) + { + lastFrameTime = null; + box.Colour = overlayColourProvider.Colour1; + return; + } + + if (lastFrameTime != null) + { + float movementAmount = (float)(Clock.CurrentTime - lastFrameTime) / 400; + + var buttons = inputManager.CurrentState.Keyboard.Keys; + + box.Colour = buttons.HasAnyButtonPressed ? overlayColourProvider.Content1 : overlayColourProvider.Colour1; + + foreach (var key in buttons) + { + switch (key) + { + case Key.K: + case Key.Up: + box.Y = MathHelper.Clamp(box.Y - movementAmount, 0.1f, 0.9f); + break; + + case Key.J: + case Key.Down: + box.Y = MathHelper.Clamp(box.Y + movementAmount, 0.1f, 0.9f); + break; + + case Key.Z: + case Key.Left: + box.X = MathHelper.Clamp(box.X - movementAmount, 0.1f, 0.9f); + break; + + case Key.X: + case Key.Right: + box.X = MathHelper.Clamp(box.X + movementAmount, 0.1f, 0.9f); + break; + } + } + } + + lastFrameTime = Clock.CurrentTime; + } + } + + public class LatencyCursorContainer : CompositeDrawable + { + private Circle cursor = null!; + private InputManager inputManager = null!; + + private readonly BindableBool isActive; + + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } = null!; + + public LatencyCursorContainer(BindableBool isActive) + { + this.isActive = isActive; + Masking = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + InternalChild = cursor = new Circle + { + Size = new Vector2(40), + Origin = Anchor.Centre, + Colour = overlayColourProvider.Colour2, + }; + + inputManager = GetContainingInputManager(); + } + + protected override bool OnHover(HoverEvent e) => false; + + protected override void Update() + { + cursor.Colour = inputManager.CurrentState.Mouse.IsPressed(MouseButton.Left) ? overlayColourProvider.Content1 : overlayColourProvider.Colour2; + + if (isActive.Value) + { + cursor.Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position); + cursor.Alpha = 1; + } + else + { + cursor.Alpha = 0; + } + + base.Update(); + } + } + } +} diff --git a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs new file mode 100644 index 0000000000..0a9d98450f --- /dev/null +++ b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs @@ -0,0 +1,461 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Platform; +using osu.Framework.Platform.Windows; +using osu.Framework.Screens; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Screens.Utility +{ + public class LatencyCertifierScreen : OsuScreen + { + private FrameSync previousFrameSyncMode; + private double previousActiveHz; + + private readonly OsuTextFlowContainer statusText; + + public override bool HideOverlaysOnEnter => true; + + public override bool CursorVisible => mainArea.Count == 0; + + public override float BackgroundParallaxAmount => 0; + + private readonly OsuTextFlowContainer explanatoryText; + + private readonly Container mainArea; + + private readonly Container resultsArea; + + /// + /// The rate at which the game host should attempt to run. + /// + private const int target_host_update_frames = 4000; + + [Cached] + private readonly OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Orange); + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private FrameworkConfigManager config { get; set; } = null!; + + private const int rounds_to_complete = 5; + + private const int rounds_to_complete_certified = 20; + + /// + /// Whether we are now in certification mode and decreasing difficulty. + /// + private bool isCertifying; + + private int totalRoundForNextResultsScreen => isCertifying ? rounds_to_complete_certified : rounds_to_complete; + + private int attemptsAtCurrentDifficulty; + private int correctAtCurrentDifficulty; + + public int DifficultyLevel { get; private set; } = 1; + + private double lastPoll; + private int pollingMax; + + [Resolved] + private GameHost host { get; set; } = null!; + + public LatencyCertifierScreen() + { + InternalChildren = new Drawable[] + { + new Box + { + Colour = overlayColourProvider.Background6, + RelativeSizeAxes = Axes.Both, + }, + mainArea = new Container + { + RelativeSizeAxes = Axes.Both, + }, + // Make sure the edge between the two comparisons can't be used to ascertain latency. + new Box + { + Name = "separator", + Colour = ColourInfo.GradientHorizontal(overlayColourProvider.Background6, overlayColourProvider.Background6.Opacity(0)), + Width = 100, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft, + }, + new Box + { + Name = "separator", + Colour = ColourInfo.GradientHorizontal(overlayColourProvider.Background6.Opacity(0), overlayColourProvider.Background6), + Width = 100, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight, + }, + explanatoryText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + TextAnchor = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = @"Welcome to the latency certifier! +Use the arrow keys, Z/X/J/K to move the square. +Use the Tab key to change focus. +Do whatever you need to try and perceive the difference in latency, then choose your best side. +", + }, + resultsArea = new Container + { + RelativeSizeAxes = Axes.Both, + }, + statusText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 40)) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TextAnchor = Anchor.TopCentre, + Y = 150, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (lastPoll > 0) + pollingMax = (int)Math.Max(pollingMax, 1000 / (Clock.CurrentTime - lastPoll)); + lastPoll = Clock.CurrentTime; + return base.OnMouseMove(e); + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + previousFrameSyncMode = config.Get(FrameworkSetting.FrameSync); + previousActiveHz = host.UpdateThread.ActiveHz; + config.SetValue(FrameworkSetting.FrameSync, FrameSync.Unlimited); + host.UpdateThread.ActiveHz = target_host_update_frames; + host.AllowBenchmarkUnlimitedFrames = true; + } + + public override bool OnExiting(ScreenExitEvent e) + { + host.AllowBenchmarkUnlimitedFrames = false; + config.SetValue(FrameworkSetting.FrameSync, previousFrameSyncMode); + host.UpdateThread.ActiveHz = previousActiveHz; + return base.OnExiting(e); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + loadNextRound(); + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + switch (e.Key) + { + case Key.Tab: + var firstArea = mainArea.FirstOrDefault(a => !a.IsActiveArea.Value); + if (firstArea != null) + firstArea.IsActiveArea.Value = true; + return true; + } + + return base.OnKeyDown(e); + } + + private void showResults() + { + mainArea.Clear(); + + var displayMode = host.Window?.CurrentDisplayMode.Value; + + string exclusive = "unknown"; + + if (host.Window is WindowsWindow windowsWindow) + exclusive = windowsWindow.FullscreenCapability.ToString(); + + statusText.Clear(); + + float successRate = (float)correctAtCurrentDifficulty / attemptsAtCurrentDifficulty; + bool isPass = successRate == 1; + + statusText.AddParagraph($"You scored {correctAtCurrentDifficulty} out of {attemptsAtCurrentDifficulty} ({successRate:0%})!", cp => cp.Colour = isPass ? colours.Green : colours.Red); + statusText.AddParagraph($"Level {DifficultyLevel} ({mapDifficultyToTargetFrameRate(DifficultyLevel):N0} Hz)", + cp => cp.Font = OsuFont.Default.With(size: 24)); + + statusText.AddParagraph(string.Empty); + statusText.AddParagraph(string.Empty); + statusText.AddIcon(isPass ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.TimesCircle, cp => cp.Colour = isPass ? colours.Green : colours.Red); + statusText.AddParagraph(string.Empty); + + if (!isPass && DifficultyLevel > 1) + { + statusText.AddParagraph("To complete certification, the difficulty level will now decrease until you can get 20 rounds correct in a row!", + cp => cp.Font = OsuFont.Default.With(size: 24, weight: FontWeight.SemiBold)); + statusText.AddParagraph(string.Empty); + } + + statusText.AddParagraph($"Polling: {pollingMax} Hz Monitor: {displayMode?.RefreshRate ?? 0:N0} Hz Exclusive: {exclusive}", + cp => cp.Font = OsuFont.Default.With(size: 15, weight: FontWeight.SemiBold)); + + statusText.AddParagraph($"Input: {host.InputThread.Clock.FramesPerSecond} Hz " + + $"Update: {host.UpdateThread.Clock.FramesPerSecond} Hz " + + $"Draw: {host.DrawThread.Clock.FramesPerSecond} Hz" + , cp => cp.Font = OsuFont.Default.With(size: 15, weight: FontWeight.SemiBold)); + + if (isCertifying && isPass) + { + showCertifiedScreen(); + return; + } + + string cannotIncreaseReason = string.Empty; + + if (mapDifficultyToTargetFrameRate(DifficultyLevel + 1) > target_host_update_frames) + cannotIncreaseReason = "You've reached the maximum level."; + else if (mapDifficultyToTargetFrameRate(DifficultyLevel + 1) > Clock.FramesPerSecond) + cannotIncreaseReason = "Game is not running fast enough to test this level"; + + FillFlowContainer buttonFlow; + + resultsArea.Add(buttonFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Spacing = new Vector2(20), + Padding = new MarginPadding(20), + }); + + if (isPass) + { + buttonFlow.Add(new ButtonWithKeyBind(Key.Enter) + { + Text = "Continue to next level", + BackgroundColour = colours.Green, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => changeDifficulty(DifficultyLevel + 1), + Enabled = { Value = string.IsNullOrEmpty(cannotIncreaseReason) }, + TooltipText = cannotIncreaseReason + }); + } + else + { + if (DifficultyLevel == 1) + { + buttonFlow.Add(new ButtonWithKeyBind(Key.Enter) + { + Text = "Retry", + TooltipText = "Are you even trying..?", + BackgroundColour = colours.Pink2, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => + { + isCertifying = false; + changeDifficulty(1); + }, + }); + } + else + { + buttonFlow.Add(new ButtonWithKeyBind(Key.Enter) + { + Text = "Begin certification at last level", + BackgroundColour = colours.Yellow, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => + { + isCertifying = true; + changeDifficulty(DifficultyLevel - 1); + }, + TooltipText = isPass ? $"Chain {rounds_to_complete_certified} rounds to confirm your perception!" : "You've reached your limits. Go to the previous level to complete certification!", + }); + } + } + } + + private void showCertifiedScreen() + { + Drawable background; + Drawable certifiedText; + + resultsArea.AddRange(new[] + { + background = new Box + { + Colour = overlayColourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + (certifiedText = new OsuSpriteText + { + Alpha = 0, + Font = OsuFont.TorusAlternate.With(size: 80, weight: FontWeight.Bold), + Text = "Certified!", + Blending = BlendingParameters.Additive, + }).WithEffect(new GlowEffect + { + Colour = overlayColourProvider.Colour1, + PadExtent = true + }).With(e => + { + e.Anchor = Anchor.Centre; + e.Origin = Anchor.Centre; + }), + new OsuSpriteText + { + Text = $"You should use a frame limiter with update rate of {mapDifficultyToTargetFrameRate(DifficultyLevel + 1)} Hz (or fps) for best results!", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), + Y = 80, + } + }); + + background.FadeInFromZero(1000, Easing.OutQuint); + + certifiedText.FadeInFromZero(500, Easing.InQuint); + + certifiedText + .ScaleTo(10) + .ScaleTo(1, 600, Easing.InQuad) + .Then() + .ScaleTo(1.05f, 10000, Easing.OutQuint); + } + + private void changeDifficulty(int difficulty) + { + Debug.Assert(difficulty > 0); + + resultsArea.Clear(); + + correctAtCurrentDifficulty = 0; + attemptsAtCurrentDifficulty = 0; + + pollingMax = 0; + lastPoll = 0; + + DifficultyLevel = difficulty; + + loadNextRound(); + } + + private void loadNextRound() + { + attemptsAtCurrentDifficulty++; + statusText.Text = $"Level {DifficultyLevel}\nRound {attemptsAtCurrentDifficulty} of {totalRoundForNextResultsScreen}"; + + mainArea.Clear(); + + int betterSide = RNG.Next(0, 2); + + mainArea.AddRange(new[] + { + new LatencyArea(Key.Number1, betterSide == 1 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : (int?)null) + { + Width = 0.5f, + IsActiveArea = { Value = true }, + ReportUserBest = () => recordResult(betterSide == 0), + }, + new LatencyArea(Key.Number2, betterSide == 0 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : (int?)null) + { + Width = 0.5f, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + ReportUserBest = () => recordResult(betterSide == 1) + } + }); + + foreach (var area in mainArea) + { + area.IsActiveArea.BindValueChanged(active => + { + if (active.NewValue) + mainArea.Children.First(a => a != area).IsActiveArea.Value = false; + }); + } + } + + private void recordResult(bool correct) + { + // Fading this out will improve the frame rate after the first round due to less text on screen. + explanatoryText.FadeOut(500, Easing.OutQuint); + + if (correct) + correctAtCurrentDifficulty++; + + if (attemptsAtCurrentDifficulty < totalRoundForNextResultsScreen) + loadNextRound(); + else + showResults(); + } + + private static int mapDifficultyToTargetFrameRate(int difficulty) + { + switch (difficulty) + { + case 1: + return 15; + + case 2: + return 30; + + case 3: + return 45; + + case 4: + return 60; + + case 5: + return 120; + + case 6: + return 240; + + case 7: + return 480; + + case 8: + return 720; + + case 9: + return 960; + + default: + return 1000 + ((difficulty - 10) * 500); + } + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 63b8cf4cb5..b6218c5950 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index a0fafa635b..ba57aba01b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - +