Skip to content

Commit

Permalink
Merge pull request #19599 from ItsShamed/kps
Browse files Browse the repository at this point in the history
Add clicks/second counter to HUD
  • Loading branch information
peppy committed Sep 8, 2022
2 parents 6945c43 + 27aa355 commit f1fa442
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 12 deletions.
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
130 changes: 130 additions & 0 deletions osu.Game.Tests/Visual/Gameplay/TestSceneClicksPerSecondCalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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.Audio;
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 { set => adjustableAudioComponent.Tempo.Value = value; }

private readonly AudioAdjustments adjustableAudioComponent = new AudioAdjustments();

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 IAdjustableAudioComponent AdjustmentsFromMods => adjustableAudioComponent;

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.GetTrueGameplayRate();

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;
}
}
}

0 comments on commit f1fa442

Please sign in to comment.