Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add clicks/second counter to HUD #19599

Merged
merged 43 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b2e7da5
Add basic Queue based implementation of KPS
ItsShamed Jul 28, 2022
0791508
Add some tests
ItsShamed Jul 28, 2022
2df2401
Merge branch 'master' into kps
ItsShamed Jul 28, 2022
89855cc
Change KPS Counter implementation base and add better replay integration
ItsShamed Jul 30, 2022
42d1bdf
Move KPS calculation to a standalone class
ItsShamed Aug 5, 2022
24c29b7
Do not add KPS calculation when gameplay rate is 0
ItsShamed Aug 5, 2022
b4e0fa7
Rewrite tests for KPS
ItsShamed Aug 5, 2022
0c07df2
Remove DT from KPS test
ItsShamed Aug 5, 2022
0886137
Prevent KeysPerSecondCounter from NRE when no instance is initialized
ItsShamed Aug 5, 2022
b2557a8
Refactor KPS
ItsShamed Aug 6, 2022
b52a07c
Use DI to provide dependencies for KPS Calculator and improve input
ItsShamed Aug 8, 2022
edb8e5e
Temporarily emptying `TestSceneKeysPerSecondCounter` until a good tes…
ItsShamed Aug 9, 2022
9e80d3f
Re-adjust timespan conditions in `KeysPerSecondCalculator`
ItsShamed Aug 10, 2022
46e372c
Add more readiness checks in `KeysPerSecondCalculator`
ItsShamed Aug 10, 2022
0a94fb4
Make KPS counter strictly depending only on KPS calculator
ItsShamed Aug 10, 2022
d58d5ee
Add basic tests for KPS
ItsShamed Aug 10, 2022
0e1efbd
Rename `DrawableCookieziRuleset` to `MockDrawableRuleset`
ItsShamed Aug 10, 2022
3c6461b
Remove KPS acronym usage
ItsShamed Aug 11, 2022
787dee2
Move `KeysPerSecondCalculator` instantiation from `HUDOverlay` to `Pl…
ItsShamed Aug 11, 2022
d29cba8
Remove useless comment in `KeysPerSecondCounter`
ItsShamed Aug 11, 2022
9b252b1
Make `KeysPerSecondCalculator` dependency in `HUDOverlay` nullable
ItsShamed Aug 11, 2022
e08f717
Change displayed metric from "KPS" to "clicks/s"
ItsShamed Aug 13, 2022
d5f10cb
Revert 787dee24 and initialize calculator in `HUDOverlay`
ItsShamed Aug 14, 2022
9dc8065
Make `ActionListener` and `KeysPerSecondCalculator` not rely on event…
ItsShamed Aug 14, 2022
2aa3a1b
Rename all "KeysPerSecond" usages to "ClicksPerSecond"
ItsShamed Aug 14, 2022
3de35a1
Update calculator and tests to match changes on clocks
ItsShamed Aug 18, 2022
da407aa
Merge branch 'master' into kps
peppy Aug 20, 2022
3ac6500
Add new test resources for CPS counter
ItsShamed Aug 18, 2022
5cf54a7
Code cleanup for CPS counter
ItsShamed Aug 21, 2022
c56390c
Use less custom classes for CPS tests
ItsShamed Aug 21, 2022
b597049
Code cleanup for CPS tests
ItsShamed Aug 22, 2022
5129716
Merge branch 'master' into kps
peppy Aug 24, 2022
f3847b9
Tidy up attach logic
peppy Aug 24, 2022
5cddc7e
Code cleanup (CPS)
ItsShamed Aug 24, 2022
7337d4c
Merge branch 'master' into kps
peppy Aug 31, 2022
5c2fb3e
Simplify calculation method
peppy Sep 8, 2022
6729bb3
Change `FrameStableClock` to inherit `IGameplayClock`
peppy Sep 8, 2022
ee094e3
Rewrite tests
peppy Sep 8, 2022
a98c6b2
Add comment metioning why we need to use `DrawableRuleset` lookup
peppy Sep 8, 2022
15a4eb4
Rename test scene to match class name
peppy Sep 8, 2022
d4afc36
Merge branch 'master' into kps
peppy Sep 8, 2022
45239fc
Update `TrueGameplayRate` accessing
peppy Sep 8, 2022
27aa355
Update in line with `TrueGameplayRate` changes
peppy Sep 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file not shown.
4 changes: 3 additions & 1 deletion osu.Game.Tests/Skins/SkinDeserialisationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ public class SkinDeserialisationTest
"Archives/modified-default-20220723.osk",
"Archives/modified-classic-20220723.osk",
// Covers legacy song progress, UR counter, colour hit error metre.
"Archives/modified-classic-20220801.osk"
"Archives/modified-classic-20220801.osk",
// Covers clicks/s counter
"Archives/modified-default-20220818.osk"
};

/// <summary>
Expand Down
124 changes: 124 additions & 0 deletions osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK;

namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneClicksPerSecondCalculator : OsuTestScene
{
private ClicksPerSecondCalculator calculator = null!;

private TestGameplayClock manualGameplayClock = null!;

[SetUpSteps]
public void SetUpSteps()
{
AddStep("create components", () =>
{
manualGameplayClock = new TestGameplayClock();

Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(IGameplayClock), manualGameplayClock) },
Children = new Drawable[]
{
calculator = new ClicksPerSecondCalculator(),
new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(ClicksPerSecondCalculator), calculator) },
Child = new ClicksPerSecondCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(5),
}
}
},
};
});
}

[Test]
public void TestBasicConsistency()
{
seek(1000);
AddStep("add inputs in past", () => addInputs(new double[] { 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 }));
checkClicksPerSecondValue(10);
}

[Test]
public void TestRateAdjustConsistency()
{
seek(1000);
AddStep("add inputs in past", () => addInputs(new double[] { 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 }));
checkClicksPerSecondValue(10);
AddStep("set rate 0.5x", () => manualGameplayClock.TrueGameplayRate = 0.5);
checkClicksPerSecondValue(5);
}

[Test]
public void TestInputsDiscardedOnRewind()
{
seek(1000);
AddStep("add inputs in past", () => addInputs(new double[] { 0, 100, 200, 300, 400, 500, 600, 700, 800, 900 }));
checkClicksPerSecondValue(10);
seek(500);
checkClicksPerSecondValue(6);
seek(1000);
checkClicksPerSecondValue(6);
}

private void checkClicksPerSecondValue(int i) => AddAssert("clicks/s is correct", () => calculator.Value, () => Is.EqualTo(i));

private void seekClockImmediately(double time) => manualGameplayClock.CurrentTime = time;

private void seek(double time) => AddStep($"Seek to {time}ms", () => seekClockImmediately(time));

private void addInputs(IEnumerable<double> inputs)
{
double baseTime = manualGameplayClock.CurrentTime;

foreach (double timestamp in inputs)
{
seekClockImmediately(timestamp);
calculator.AddInputTimestamp();
}

seekClockImmediately(baseTime);
}

private class TestGameplayClock : IGameplayClock
{
public double CurrentTime { get; set; }

public double Rate => 1;

public bool IsRunning => true;

public double TrueGameplayRate { get; set; } = 1;

public void ProcessFrame()
{
}

public double ElapsedFrameTime => throw new NotImplementedException();
public double FramesPerSecond => throw new NotImplementedException();
public FrameTimeInfo TimeInfo => throw new NotImplementedException();
public double StartTime => throw new NotImplementedException();
public IEnumerable<double> NonGameplayAdjustments => throw new NotImplementedException();
public IBindable<bool> IsPaused => throw new NotImplementedException();
}
}
}
8 changes: 6 additions & 2 deletions osu.Game/Rulesets/UI/DrawableRuleset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK;

namespace osu.Game.Rulesets.UI
Expand All @@ -38,7 +39,7 @@ namespace osu.Game.Rulesets.UI
/// Displays an interactive ruleset gameplay instance.
/// </summary>
/// <typeparam name="TObject">The type of HitObject contained by this DrawableRuleset.</typeparam>
public abstract class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter
public abstract class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, ICanAttachHUDPieces
where TObject : HitObject
{
public override event Action<JudgementResult> NewResult;
Expand Down Expand Up @@ -338,7 +339,10 @@ public override void SetReplayScore(Score replayScore)
public abstract DrawableHitObject<TObject> CreateDrawableRepresentation(TObject h);

public void Attach(KeyCounterDisplay keyCounter) =>
(KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter);
(KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(keyCounter);

public void Attach(ClicksPerSecondCalculator calculator) =>
(KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(calculator);

/// <summary>
/// Creates a key conversion input manager. An exception will be thrown if a valid <see cref="RulesetInputManager{T}"/> is not returned.
Expand Down
2 changes: 1 addition & 1 deletion osu.Game/Rulesets/UI/FrameStabilityContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.UI
/// </summary>
[Cached(typeof(IGameplayClock))]
[Cached(typeof(IFrameStableClock))]
public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock
public sealed class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock
{
public ReplayInputHandler? ReplayInputHandler { get; set; }

Expand Down
4 changes: 2 additions & 2 deletions osu.Game/Rulesets/UI/IFrameStableClock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// See the LICENCE file in the repository root for full licence text.

using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Game.Screens.Play;

namespace osu.Game.Rulesets.UI
{
public interface IFrameStableClock : IFrameBasedClock
public interface IFrameStableClock : IGameplayClock
{
IBindable<bool> IsCatchingUp { get; }

Expand Down
41 changes: 37 additions & 4 deletions osu.Game/Rulesets/UI/RulesetInputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using static osu.Game.Input.Handlers.ReplayInputHandler;

namespace osu.Game.Rulesets.UI
{
public abstract class RulesetInputManager<T> : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler
public abstract class RulesetInputManager<T> : PassThroughInputManager, ICanAttachHUDPieces, IHasReplayHandler, IHasRecordingHandler
where T : struct
{
public readonly KeyBindingContainer<T> KeyBindingContainer;
Expand Down Expand Up @@ -168,7 +169,7 @@ public void Attach(KeyCounterDisplay keyCounter)
.Select(action => new KeyCounterAction<T>(action)));
}

public class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler<T>
private class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler<T>
{
public ActionReceptor(KeyCounterDisplay target)
: base(target)
Expand All @@ -186,6 +187,37 @@ public void OnReleased(KeyBindingReleaseEvent<T> e)

#endregion

#region Keys per second Counter Attachment

public void Attach(ClicksPerSecondCalculator calculator)
{
var listener = new ActionListener(calculator);

KeyBindingContainer.Add(listener);
}

private class ActionListener : Component, IKeyBindingHandler<T>
{
private readonly ClicksPerSecondCalculator calculator;

public ActionListener(ClicksPerSecondCalculator calculator)
{
this.calculator = calculator;
}

public bool OnPressed(KeyBindingPressEvent<T> e)
{
calculator.AddInputTimestamp();
return false;
}

public void OnReleased(KeyBindingReleaseEvent<T> e)
{
}
}

#endregion

protected virtual KeyBindingContainer<T> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new RulesetKeyBindingContainer(ruleset, variant, unique);

Expand Down Expand Up @@ -221,12 +253,13 @@ public interface IHasRecordingHandler
}

/// <summary>
/// Supports attaching a <see cref="KeyCounterDisplay"/>.
/// Supports attaching various HUD pieces.
/// Keys will be populated automatically and a receptor will be injected inside.
/// </summary>
public interface ICanAttachKeyCounter
public interface ICanAttachHUDPieces
{
void Attach(KeyCounterDisplay keyCounter);
void Attach(ClicksPerSecondCalculator calculator);
}

public class RulesetInputManagerInputState<T> : InputState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI;

namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
{
public class ClicksPerSecondCalculator : Component
{
private readonly List<double> timestamps = new List<double>();

[Resolved]
private IGameplayClock gameplayClock { get; set; } = null!;

[Resolved(canBeNull: true)]
private DrawableRuleset? drawableRuleset { get; set; }

public int Value { get; private set; }

// Even though `FrameStabilityContainer` caches as a `GameplayClock`, we need to check it directly via `drawableRuleset`
// as this calculator is not contained within the `FrameStabilityContainer` and won't see the dependency.
private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock;

public ClicksPerSecondCalculator()
{
RelativeSizeAxes = Axes.Both;
}

public void AddInputTimestamp() => timestamps.Add(clock.CurrentTime);

protected override void Update()
{
base.Update();

double latestValidTime = clock.CurrentTime;
double earliestTimeValid = latestValidTime - 1000 * gameplayClock.TrueGameplayRate;

int count = 0;

for (int i = timestamps.Count - 1; i >= 0; i--)
{
// handle rewinding by removing future timestamps as we go
if (timestamps[i] > latestValidTime)
{
timestamps.RemoveAt(i);
continue;
}

if (timestamps[i] >= earliestTimeValid)
count++;
}

Value = count;
}
}
}