mirror of
https://github.com/ppy/osu.git
synced 2025-01-14 17:52:56 +08:00
Move KPS calculation to a standalone class
This commit is contained in:
parent
89855cc1d6
commit
42d1bdfc95
@ -0,0 +1,91 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mania;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Screens.Play.HUD.KPSCounter;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
|
{
|
||||||
|
public class TestSceneKeyPerSecondCounter : PlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||||
|
protected override bool HasCustomSteps => false;
|
||||||
|
protected override bool Autoplay => true;
|
||||||
|
|
||||||
|
private GameplayClock gameplayClock;
|
||||||
|
private DrawableRuleset drawableRuleset;
|
||||||
|
|
||||||
|
// private DependencyProvidingContainer dependencyContainer;
|
||||||
|
private KeysPerSecondCounter counter;
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public new void SetUpSteps()
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
CreateTest(() => AddStep("Create components", () =>
|
||||||
|
{
|
||||||
|
Logger.Log($"{(Player != null ? Player.ToString() : "null")}", level: LogLevel.Debug);
|
||||||
|
dependencyContainer = new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativePositionAxes = Axes.Both,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createCounter()
|
||||||
|
{
|
||||||
|
AddStep("Create counter", () =>
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
if (!Contains(dependencyContainer))
|
||||||
|
{
|
||||||
|
Add(dependencyContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dependencyContainer.CachedDependencies.Length == 0)
|
||||||
|
{
|
||||||
|
dependencyContainer.CachedDependencies = new (Type, object)[]
|
||||||
|
{
|
||||||
|
(typeof(GameplayClock), ,
|
||||||
|
(typeof(DrawableRuleset),)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Dependencies.Cache(gameplayClock = Player.GameplayClockContainer.GameplayClock));
|
||||||
|
*/
|
||||||
|
|
||||||
|
Dependencies.Cache(gameplayClock = Player.GameplayClockContainer.GameplayClock);
|
||||||
|
Dependencies.Cache(drawableRuleset = Player.DrawableRuleset);
|
||||||
|
|
||||||
|
Add(counter = new KeysPerSecondCounter
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
Scale = new Vector2(5),
|
||||||
|
Position = new Vector2(10, 100)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
AddAssert("ensure counter added", () => Contains(counter));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInGameTimeConsistency()
|
||||||
|
{
|
||||||
|
createCounter();
|
||||||
|
|
||||||
|
AddUntilStep("Wait until first note", () => counter.Current.Value != 0);
|
||||||
|
AddStep("Pause gameplay", () => gameplayClock.IsPaused.Value = true);
|
||||||
|
AddAssert("KPS = 1", () => counter.Current.Value == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD.KPSCounter;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
public void TestManualTrigger()
|
public void TestManualTrigger()
|
||||||
{
|
{
|
||||||
AddAssert("Counter = 0", () => counter.Current.Value == 0);
|
AddAssert("Counter = 0", () => counter.Current.Value == 0);
|
||||||
AddRepeatStep("manual trigger", KeysPerSecondCounter.AddTimestamp, 20);
|
AddRepeatStep("manual trigger", KeysPerSecondCalculator.AddInput, 20);
|
||||||
AddAssert("Counter is not 0", () => counter.Current.Value > 0);
|
AddAssert("Counter is not 0", () => counter.Current.Value > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Play.HUD.KPSCounter
|
||||||
|
{
|
||||||
|
public class KeysPerSecondCalculator : IDisposable
|
||||||
|
{
|
||||||
|
private static KeysPerSecondCalculator instance;
|
||||||
|
|
||||||
|
public static void AddInput()
|
||||||
|
{
|
||||||
|
instance?.onNewInput.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KeysPerSecondCalculator GetInstance(GameplayClock gameplayClock = null, DrawableRuleset drawableRuleset = null)
|
||||||
|
{
|
||||||
|
if (instance != null) return instance;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new KeysPerSecondCalculator(gameplayClock, drawableRuleset);
|
||||||
|
}
|
||||||
|
catch (ArgumentNullException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly List<double> timestamps;
|
||||||
|
private readonly GameplayClock gameplayClock;
|
||||||
|
private readonly DrawableRuleset drawableRuleset;
|
||||||
|
|
||||||
|
private event Action onNewInput;
|
||||||
|
|
||||||
|
private IClock workingClock => (IClock)drawableRuleset.FrameStableClock ?? gameplayClock;
|
||||||
|
|
||||||
|
// Having the rate from mods is preffered to using GameplayClock.TrueGameplayRate()
|
||||||
|
// as it returns 0 when paused in replays, not useful for players who want to "analyze" a replay.
|
||||||
|
private double rate => (drawableRuleset.Mods.FirstOrDefault(m => m is ModRateAdjust) as ModRateAdjust)?.SpeedChange.Value
|
||||||
|
?? 1;
|
||||||
|
|
||||||
|
private double maxTime = double.NegativeInfinity;
|
||||||
|
|
||||||
|
public bool Ready => workingClock != null && gameplayClock != null;
|
||||||
|
public int Value => timestamps.Count(isTimestampWithinSpan);
|
||||||
|
|
||||||
|
private KeysPerSecondCalculator(GameplayClock gameplayClock, DrawableRuleset drawableRuleset)
|
||||||
|
{
|
||||||
|
instance = this;
|
||||||
|
timestamps = new List<double>();
|
||||||
|
this.gameplayClock = gameplayClock ?? throw new ArgumentNullException(nameof(gameplayClock));
|
||||||
|
this.drawableRuleset = drawableRuleset;
|
||||||
|
onNewInput += addTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addTimestamp()
|
||||||
|
{
|
||||||
|
if (workingClock != null && workingClock.CurrentTime >= maxTime)
|
||||||
|
{
|
||||||
|
timestamps.Add(workingClock.CurrentTime);
|
||||||
|
maxTime = workingClock.CurrentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool isTimestampWithinSpan(double timestamp)
|
||||||
|
{
|
||||||
|
if (!Ready)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
double span = 1000 * rate;
|
||||||
|
double relativeTime = workingClock.CurrentTime - timestamp;
|
||||||
|
return relativeTime >= 0 && relativeTime <= span;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
~KeysPerSecondCalculator() => Dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -3,17 +3,12 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Framework.Timing;
|
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
@ -22,28 +17,20 @@ using osu.Game.Rulesets.UI;
|
|||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
namespace osu.Game.Screens.Play.HUD.KPSCounter
|
||||||
{
|
{
|
||||||
public class KeysPerSecondCounter : RollingCounter<int>, ISkinnableDrawable
|
public class KeysPerSecondCounter : RollingCounter<int>, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
private static List<double> timestamps;
|
|
||||||
private static double maxTime = double.NegativeInfinity;
|
|
||||||
|
|
||||||
private static event Action onNewInput;
|
|
||||||
|
|
||||||
private const int invalidation_timeout = 1000;
|
|
||||||
private const float alpha_when_invalid = 0.3f;
|
private const float alpha_when_invalid = 0.3f;
|
||||||
|
|
||||||
private readonly Bindable<bool> valid = new Bindable<bool>();
|
private readonly Bindable<bool> valid = new Bindable<bool>();
|
||||||
|
private GameplayClock gameplayClock;
|
||||||
private static GameplayClock gameplayClock;
|
|
||||||
private static IClock referenceClock;
|
|
||||||
|
|
||||||
private static IClock clock => referenceClock ?? gameplayClock;
|
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private DrawableRuleset drawableRuleset { get; set; }
|
private DrawableRuleset drawableRuleset { get; set; }
|
||||||
|
|
||||||
|
private KeysPerSecondCalculator calculator => KeysPerSecondCalculator.GetInstance(gameplayClock, drawableRuleset);
|
||||||
|
|
||||||
[SettingSource("Smoothing time", "How smooth the counter should change\nThe more it is smooth, the less it's accurate.")]
|
[SettingSource("Smoothing time", "How smooth the counter should change\nThe more it is smooth, the less it's accurate.")]
|
||||||
public BindableNumber<double> SmoothingTime { get; } = new BindableNumber<double>(350)
|
public BindableNumber<double> SmoothingTime { get; } = new BindableNumber<double>(350)
|
||||||
{
|
{
|
||||||
@ -51,66 +38,30 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
MinValue = 0
|
MinValue = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
public static void AddTimestamp()
|
|
||||||
{
|
|
||||||
Logger.Log($"Input timestamp attempt C: {clock.CurrentTime}ms | GC: {gameplayClock.CurrentTime} | RC: {referenceClock?.CurrentTime ?? -1} | Max: {maxTime})", level: LogLevel.Debug);
|
|
||||||
|
|
||||||
if (clock.CurrentTime >= maxTime)
|
|
||||||
{
|
|
||||||
Logger.Log("Input timestamp added.", level: LogLevel.Debug);
|
|
||||||
timestamps?.Add(clock.CurrentTime);
|
|
||||||
maxTime = timestamps?.Max() ?? clock.CurrentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
onNewInput?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Reset()
|
|
||||||
{
|
|
||||||
timestamps?.Clear();
|
|
||||||
maxTime = int.MinValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override double RollingDuration => SmoothingTime.Value;
|
protected override double RollingDuration => SmoothingTime.Value;
|
||||||
|
|
||||||
public bool UsesFixedAnchor { get; set; }
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
public KeysPerSecondCounter()
|
public KeysPerSecondCounter()
|
||||||
{
|
{
|
||||||
timestamps ??= new List<double>();
|
|
||||||
Current.Value = 0;
|
Current.Value = 0;
|
||||||
onNewInput += updateCounter;
|
|
||||||
Scheduler.AddOnce(updateCounter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours, GameplayClock clock)
|
private void load(OsuColour colours, GameplayClock clock, DrawableRuleset ruleset)
|
||||||
{
|
{
|
||||||
gameplayClock = clock;
|
gameplayClock = clock;
|
||||||
Colour = colours.BlueLighter;
|
Colour = colours.BlueLighter;
|
||||||
valid.BindValueChanged(e =>
|
valid.BindValueChanged(e =>
|
||||||
DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint));
|
DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint));
|
||||||
referenceClock = drawableRuleset?.FrameStableClock;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
updateCounter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
updateCounter();
|
valid.Value = calculator.Ready;
|
||||||
}
|
Current.Value = calculator.Value;
|
||||||
|
|
||||||
private void updateCounter()
|
|
||||||
{
|
|
||||||
valid.Value = timestamps != null && MathHelper.ApproximatelyEquivalent(gameplayClock.CurrentTime, referenceClock.CurrentTime, 500);
|
|
||||||
Current.Value = timestamps?.Count(timestamp => clock.CurrentTime - timestamp is >= 0 and <= invalidation_timeout) ?? 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IHasText CreateText() => new TextComponent
|
protected override IHasText CreateText() => new TextComponent
|
@ -10,7 +10,7 @@ using osu.Framework.Graphics.Sprites;
|
|||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD.KPSCounter;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
public void Increment()
|
public void Increment()
|
||||||
{
|
{
|
||||||
KeysPerSecondCounter.AddTimestamp();
|
KeysPerSecondCalculator.AddInput();
|
||||||
if (!IsCounting)
|
if (!IsCounting)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Screens.Play.HUD;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -66,7 +65,6 @@ namespace osu.Game.Screens.Play
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
private void load(OsuConfigManager config)
|
||||||
{
|
{
|
||||||
KeysPerSecondCounter.Reset();
|
|
||||||
config.BindWith(OsuSetting.KeyOverlay, configVisibility);
|
config.BindWith(OsuSetting.KeyOverlay, configVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ using osu.Game.Rulesets.Scoring;
|
|||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Scoring.Legacy;
|
using osu.Game.Scoring.Legacy;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD.KPSCounter;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
@ -1046,7 +1046,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
fadeOut();
|
fadeOut();
|
||||||
|
|
||||||
KeysPerSecondCounter.Reset();
|
KeysPerSecondCalculator.GetInstance().Dispose();
|
||||||
|
|
||||||
return base.OnExiting(e);
|
return base.OnExiting(e);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user