diff --git a/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs b/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs index af6681e9cf..d6e0f995e5 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneLatencyCertifierScreen.cs @@ -24,6 +24,24 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("wait for load", () => latencyCertifier.IsLoaded); } + [Test] + public void TestSimple() + { + AddStep("set visual mode to simple", () => latencyCertifier.VisualMode.Value = LatencyVisualMode.Simple); + } + + [Test] + public void TestCircleGameplay() + { + AddStep("set visual mode to circles", () => latencyCertifier.VisualMode.Value = LatencyVisualMode.CircleGameplay); + } + + [Test] + public void TestCycleVisualModes() + { + AddRepeatStep("cycle mode", () => InputManager.Key(Key.Space), 6); + } + [Test] public void TestCertification() { @@ -38,7 +56,6 @@ namespace osu.Game.Tests.Visual.Settings clickUntilResults(true); AddAssert("check at results", () => !latencyCertifier.ChildrenOfType().Any()); - AddAssert("check no buttons", () => !latencyCertifier.ChildrenOfType().Any()); checkDifficulty(1); } diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs new file mode 100644 index 0000000000..dd59721a65 --- /dev/null +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -0,0 +1,240 @@ +// 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.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Utility.SampleComponents; +using osuTK; + +namespace osu.Game.Screens.Utility +{ + public class CircleGameplay : LatencySampleComponent + { + private int nextLocation; + + private readonly List hitEvents = new List(); + + private double? lastGeneratedBeatTime; + + private Container circles = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + InternalChildren = new Drawable[] + { + circles = new Container + { + RelativeSizeAxes = Axes.Both, + }, + }; + + SampleBPM.BindValueChanged(_ => + { + circles.Clear(); + lastGeneratedBeatTime = null; + }); + } + + protected override void UpdateAtLimitedRate(InputState inputState) + { + double beatLength = 60000 / SampleBPM.Value; + + int nextBeat = (int)(Clock.CurrentTime / beatLength) + 1; + + // We want to generate a few hit objects ahead of the current time (to allow them to animate). + double generateUpTo = (nextBeat + 2) * beatLength; + + while (lastGeneratedBeatTime == null || lastGeneratedBeatTime < generateUpTo) + { + double time = ++nextBeat * beatLength; + + if (time <= lastGeneratedBeatTime) + continue; + + newBeat(time); + lastGeneratedBeatTime = time; + } + } + + private void newBeat(double time) + { + nextLocation++; + + Vector2 location; + + float adjust = SampleVisualSpacing.Value * 0.25f; + + float spacingLow = 0.5f - adjust; + float spacingHigh = 0.5f + adjust; + + switch (nextLocation % 4) + { + default: + location = new Vector2(spacingLow, spacingLow); + break; + + case 1: + location = new Vector2(spacingHigh, spacingHigh); + break; + + case 2: + location = new Vector2(spacingHigh, spacingLow); + break; + + case 3: + location = new Vector2(spacingLow, spacingHigh); + break; + } + + circles.Add(new SampleHitCircle(time) + { + RelativePositionAxes = Axes.Both, + Position = location, + Hit = hit, + }); + } + + private void hit(HitEvent h) + { + hitEvents.Add(h); + } + + public class SampleHitCircle : LatencySampleComponent + { + public HitEvent? HitEvent; + + public Action? Hit { get; set; } + + public readonly double HitTime; + + private CircularContainer approach = null!; + private Circle circle = null!; + + private const float size = 100; + private const float duration = 200; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + => circle.ReceivePositionalInputAt(screenSpacePos); + + public SampleHitCircle(double hitTime) + { + HitTime = hitTime; + + Origin = Anchor.Centre; + AutoSizeAxes = Axes.Both; + AlwaysPresent = true; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChildren = new Drawable[] + { + circle = new Circle + { + Colour = OverlayColourProvider.Content1, + Size = new Vector2(size), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + approach = new CircularContainer + { + BorderColour = colours.Blue, + Size = new Vector2(size), + Masking = true, + BorderThickness = 4, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + Alpha = 0, + AlwaysPresent = true, + RelativeSizeAxes = Axes.Both, + }, + } + }, + }; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (HitEvent != null) + return false; + + if (Math.Abs(Clock.CurrentTime - HitTime) > duration) + return false; + + attemptHit(); + return true; + } + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (!IsActive.Value) + return false; + + if (Math.Abs(Clock.CurrentTime - HitTime) > duration) + return false; + + if (IsHovered) + attemptHit(); + return base.OnKeyDown(e); + } + + protected override void UpdateAtLimitedRate(InputState inputState) + { + if (HitEvent == null) + { + double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450); + + approach.Scale = new Vector2(1 + 4 * (float)MathHelper.Clamp((HitTime - Clock.CurrentTime) / preempt, 0, 100)); + Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1); + + if (Clock.CurrentTime > HitTime + duration) + Expire(); + } + } + + private void attemptHit() => Schedule(() => + { + if (HitEvent != null) + return; + + // in case it was hit outside of display range, show immediately + // so the user isn't confused. + this.FadeIn(); + + approach.Expire(); + + circle + .FadeOut(duration) + .ScaleTo(1.5f, duration); + + HitEvent = new HitEvent(Clock.CurrentTime - HitTime, HitResult.Good, new HitObject + { + HitWindows = new HitWindows(), + }, null, null); + + Hit?.Invoke(HitEvent.Value); + + this.Delay(duration).Expire(); + }); + } + } +} diff --git a/osu.Game/Screens/Utility/LatencyArea.cs b/osu.Game/Screens/Utility/LatencyArea.cs index 2ef48bb571..3b9e95ce16 100644 --- a/osu.Game/Screens/Utility/LatencyArea.cs +++ b/osu.Game/Screens/Utility/LatencyArea.cs @@ -9,14 +9,14 @@ 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 osu.Game.Screens.Utility.SampleComponents; using osuTK.Input; namespace osu.Game.Screens.Utility { + [Cached] public class LatencyArea : CompositeDrawable { [Resolved] @@ -28,10 +28,14 @@ namespace osu.Game.Screens.Utility private readonly Key key; + private Container visualContent = null!; + public readonly int? TargetFrameRate; public readonly BindableBool IsActiveArea = new BindableBool(); + public readonly Bindable VisualMode = new Bindable(); + public LatencyArea(Key key, int? targetFrameRate) { this.key = key; @@ -61,20 +65,9 @@ namespace osu.Game.Screens.Utility Origin = Anchor.TopCentre, Action = () => ReportUserBest?.Invoke(), }, - new Container + visualContent = new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new LatencyMovableBox(IsActiveArea) - { - RelativeSizeAxes = Axes.Both, - }, - new LatencyCursorContainer(IsActiveArea) - { - RelativeSizeAxes = Axes.Both, - }, - } }, }; @@ -82,6 +75,43 @@ namespace osu.Game.Screens.Utility { background.FadeColour(active.NewValue ? overlayColourProvider.Background4 : overlayColourProvider.Background6, 200, Easing.OutQuint); }, true); + + VisualMode.BindValueChanged(mode => + { + switch (mode.NewValue) + { + case LatencyVisualMode.Simple: + visualContent.Children = new Drawable[] + { + new LatencyMovableBox + { + RelativeSizeAxes = Axes.Both, + }, + new LatencyCursorContainer + { + RelativeSizeAxes = Axes.Both, + }, + }; + break; + + case LatencyVisualMode.CircleGameplay: + visualContent.Children = new Drawable[] + { + new CircleGameplay + { + RelativeSizeAxes = Axes.Both, + }, + new LatencyCursorContainer + { + RelativeSizeAxes = Axes.Both, + }, + }; + break; + + default: + throw new ArgumentOutOfRangeException(); + } + }, true); } protected override bool OnMouseMove(MouseMoveEvent e) @@ -102,140 +132,5 @@ namespace osu.Game.Screens.Utility 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 index 5c12dd3a83..8210e6825a 100644 --- a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs +++ b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs @@ -7,6 +7,7 @@ using System; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -24,11 +25,13 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; +using osu.Game.Overlays.Settings; using osuTK; using osuTK.Input; namespace osu.Game.Screens.Utility { + [Cached] public class LatencyCertifierScreen : OsuScreen { private FrameSync previousFrameSyncMode; @@ -42,12 +45,16 @@ namespace osu.Game.Screens.Utility public override float BackgroundParallaxAmount => 0; - private readonly OsuTextFlowContainer explanatoryText; + private readonly LinkFlowContainer explanatoryText; private readonly Container mainArea; private readonly Container resultsArea; + public readonly BindableDouble SampleBPM = new BindableDouble(120) { MinValue = 60, MaxValue = 300, Precision = 1 }; + public readonly BindableDouble SampleApproachRate = new BindableDouble(9) { MinValue = 5, MaxValue = 12, Precision = 0.1 }; + public readonly BindableFloat SampleVisualSpacing = new BindableFloat(0.5f) { MinValue = 0f, MaxValue = 1, Precision = 0.1f }; + /// /// The rate at which the game host should attempt to run. /// @@ -62,6 +69,8 @@ namespace osu.Game.Screens.Utility [Resolved] private FrameworkConfigManager config { get; set; } = null!; + public readonly Bindable VisualMode = new Bindable(); + private const int rounds_to_complete = 5; private const int rounds_to_complete_certified = 20; @@ -81,9 +90,14 @@ namespace osu.Game.Screens.Utility private double lastPoll; private int pollingMax; + private readonly FillFlowContainer settings; + [Resolved] private GameHost host { get; set; } = null!; + [Resolved] + private MusicController musicController { get; set; } = null!; + public LatencyCertifierScreen() { InternalChildren = new Drawable[] @@ -116,18 +130,54 @@ namespace osu.Game.Screens.Utility Anchor = Anchor.TopCentre, Origin = Anchor.TopRight, }, - explanatoryText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) + settings = new FillFlowContainer { + Name = "Settings", + AutoSizeAxes = Axes.Y, + Width = 800, + Padding = new MarginPadding(10), + Spacing = new Vector2(2), + Direction = FillDirection.Vertical, 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. -", + Children = new Drawable[] + { + explanatoryText = new LinkFlowContainer(cp => cp.Font = OsuFont.Default.With(size: 20)) + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TextAnchor = Anchor.TopCentre, + }, + new SettingsSlider + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.None, + Width = 400, + LabelText = "bpm", + Current = SampleBPM + }, + new SettingsSlider + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.None, + Width = 400, + LabelText = "visual spacing", + Current = SampleVisualSpacing + }, + new SettingsSlider + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.None, + Width = 400, + LabelText = "approach rate", + Current = SampleApproachRate + }, + }, }, resultsArea = new Container { @@ -143,6 +193,12 @@ Do whatever you need to try and perceive the difference in latency, then choose AutoSizeAxes = Axes.Y, }, }; + + explanatoryText.AddParagraph(@"Welcome to the latency certifier!"); + explanatoryText.AddParagraph(@"Do whatever you need to try and perceive the difference in latency, then choose your best side. Read more about the methodology "); + explanatoryText.AddLink("here", "https://github.com/ppy/osu/wiki/Latency-and-unlimited-frame-rates#methodology"); + explanatoryText.AddParagraph(@"Use the arrow keys or Z/X/F/J to control the display."); + explanatoryText.AddParagraph(@"Tab key to change focus. Space to change display mode"); } protected override bool OnMouseMove(MouseMoveEvent e) @@ -162,6 +218,8 @@ Do whatever you need to try and perceive the difference in latency, then choose config.SetValue(FrameworkSetting.FrameSync, FrameSync.Unlimited); host.UpdateThread.ActiveHz = target_host_update_frames; host.AllowBenchmarkUnlimitedFrames = true; + + musicController.Stop(); } public override bool OnExiting(ScreenExitEvent e) @@ -182,6 +240,11 @@ Do whatever you need to try and perceive the difference in latency, then choose { switch (e.Key) { + case Key.Space: + int availableModes = Enum.GetValues(typeof(LatencyVisualMode)).Length; + VisualMode.Value = (LatencyVisualMode)(((int)VisualMode.Value + 1) % availableModes); + return true; + case Key.Tab: var firstArea = mainArea.FirstOrDefault(a => !a.IsActiveArea.Value); if (firstArea != null) @@ -195,6 +258,8 @@ Do whatever you need to try and perceive the difference in latency, then choose private void showResults() { mainArea.Clear(); + resultsArea.Clear(); + settings.Hide(); var displayMode = host.Window?.CurrentDisplayMode.Value; @@ -301,7 +366,9 @@ Do whatever you need to try and perceive the difference in latency, then choose 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!", + 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!", }); } } @@ -374,6 +441,8 @@ Do whatever you need to try and perceive the difference in latency, then choose private void loadNextRound() { + settings.Show(); + attemptsAtCurrentDifficulty++; statusText.Text = $"Level {DifficultyLevel}\nRound {attemptsAtCurrentDifficulty} of {totalRoundForNextResultsScreen}"; @@ -386,12 +455,14 @@ Do whatever you need to try and perceive the difference in latency, then choose new LatencyArea(Key.Number1, betterSide == 1 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : (int?)null) { Width = 0.5f, + VisualMode = { BindTarget = VisualMode }, IsActiveArea = { Value = true }, ReportUserBest = () => recordResult(betterSide == 0), }, new LatencyArea(Key.Number2, betterSide == 0 ? mapDifficultyToTargetFrameRate(DifficultyLevel) : (int?)null) { Width = 0.5f, + VisualMode = { BindTarget = VisualMode }, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, ReportUserBest = () => recordResult(betterSide == 1) diff --git a/osu.Game/Screens/Utility/LatencyVisualMode.cs b/osu.Game/Screens/Utility/LatencyVisualMode.cs new file mode 100644 index 0000000000..f9f51f0829 --- /dev/null +++ b/osu.Game/Screens/Utility/LatencyVisualMode.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable +namespace osu.Game.Screens.Utility +{ + public enum LatencyVisualMode + { + CircleGameplay, + Simple, + } +} diff --git a/osu.Game/Screens/Utility/SampleComponents/LatencyCursorContainer.cs b/osu.Game/Screens/Utility/SampleComponents/LatencyCursorContainer.cs new file mode 100644 index 0000000000..e4c2b504cb --- /dev/null +++ b/osu.Game/Screens/Utility/SampleComponents/LatencyCursorContainer.cs @@ -0,0 +1,57 @@ +// 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.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osu.Game.Overlays; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Screens.Utility.SampleComponents +{ + public class LatencyCursorContainer : LatencySampleComponent + { + private Circle cursor = null!; + + [Resolved] + private OverlayColourProvider overlayColourProvider { get; set; } = null!; + + public LatencyCursorContainer() + { + Masking = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + InternalChild = cursor = new Circle + { + Size = new Vector2(40), + Origin = Anchor.Centre, + Colour = overlayColourProvider.Colour2, + }; + } + + protected override bool OnHover(HoverEvent e) => false; + + protected override void UpdateAtLimitedRate(InputState inputState) + { + cursor.Colour = inputState.Mouse.IsPressed(MouseButton.Left) ? overlayColourProvider.Content1 : overlayColourProvider.Colour2; + + if (IsActive.Value) + { + cursor.Position = ToLocalSpace(inputState.Mouse.Position); + cursor.Alpha = 1; + } + else + { + cursor.Alpha = 0; + } + } + } +} diff --git a/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs new file mode 100644 index 0000000000..a7da05fbb1 --- /dev/null +++ b/osu.Game/Screens/Utility/SampleComponents/LatencyMovableBox.cs @@ -0,0 +1,84 @@ +// 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.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Screens.Utility.SampleComponents +{ + public class LatencyMovableBox : LatencySampleComponent + { + private Box box = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + 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 UpdateAtLimitedRate(InputState inputState) + { + if (!IsActive.Value) + { + lastFrameTime = null; + box.Colour = OverlayColourProvider.Colour1; + return; + } + + if (lastFrameTime != null) + { + float movementAmount = (float)(Clock.CurrentTime - lastFrameTime) / 400; + + var buttons = inputState.Keyboard.Keys; + + box.Colour = buttons.HasAnyButtonPressed ? OverlayColourProvider.Content1 : OverlayColourProvider.Colour1; + + foreach (var key in buttons) + { + switch (key) + { + case Key.F: + 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; + } + } +} diff --git a/osu.Game/Screens/Utility/SampleComponents/LatencySampleComponent.cs b/osu.Game/Screens/Utility/SampleComponents/LatencySampleComponent.cs new file mode 100644 index 0000000000..c3233d5aa5 --- /dev/null +++ b/osu.Game/Screens/Utility/SampleComponents/LatencySampleComponent.cs @@ -0,0 +1,55 @@ +// 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.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Framework.Input.States; +using osu.Game.Overlays; + +namespace osu.Game.Screens.Utility.SampleComponents +{ + public abstract class LatencySampleComponent : CompositeDrawable + { + protected readonly BindableDouble SampleBPM = new BindableDouble(); + protected readonly BindableDouble SampleApproachRate = new BindableDouble(); + protected readonly BindableFloat SampleVisualSpacing = new BindableFloat(); + + protected readonly BindableBool IsActive = new BindableBool(); + + private InputManager inputManager = null!; + + [Resolved] + private LatencyArea latencyArea { get; set; } = null!; + + [Resolved] + protected OverlayColourProvider OverlayColourProvider { get; private set; } = null!; + + [BackgroundDependencyLoader] + private void load(LatencyCertifierScreen latencyCertifierScreen) + { + SampleBPM.BindTo(latencyCertifierScreen.SampleBPM); + SampleApproachRate.BindTo(latencyCertifierScreen.SampleApproachRate); + SampleVisualSpacing.BindTo(latencyCertifierScreen.SampleVisualSpacing); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + IsActive.BindTo(latencyArea.IsActiveArea); + } + + protected sealed override void Update() + { + base.Update(); + UpdateAtLimitedRate(inputManager.CurrentState); + } + + protected abstract void UpdateAtLimitedRate(InputState inputState); + } +}