1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 20:07:29 +08:00

Move KPS calculation to a standalone class

This commit is contained in:
Ryuki 2022-08-05 04:17:01 +02:00
parent 89855cc1d6
commit 42d1bdfc95
No known key found for this signature in database
GPG Key ID: A353889EAEACBF49
7 changed files with 195 additions and 64 deletions

View File

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

View File

@ -7,7 +7,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.KPSCounter;
using osuTK;
using osuTK.Input;
@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestManualTrigger()
{
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);
}

View File

@ -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();
}
}

View File

@ -3,17 +3,12 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -22,28 +17,20 @@ using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD
namespace osu.Game.Screens.Play.HUD.KPSCounter
{
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 readonly Bindable<bool> valid = new Bindable<bool>();
private static GameplayClock gameplayClock;
private static IClock referenceClock;
private static IClock clock => referenceClock ?? gameplayClock;
private GameplayClock gameplayClock;
[Resolved(canBeNull: true)]
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.")]
public BindableNumber<double> SmoothingTime { get; } = new BindableNumber<double>(350)
{
@ -51,66 +38,30 @@ namespace osu.Game.Screens.Play.HUD
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;
public bool UsesFixedAnchor { get; set; }
public KeysPerSecondCounter()
{
timestamps ??= new List<double>();
Current.Value = 0;
onNewInput += updateCounter;
Scheduler.AddOnce(updateCounter);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, GameplayClock clock)
private void load(OsuColour colours, GameplayClock clock, DrawableRuleset ruleset)
{
gameplayClock = clock;
Colour = colours.BlueLighter;
valid.BindValueChanged(e =>
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()
{
base.Update();
updateCounter();
}
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;
valid.Value = calculator.Ready;
Current.Value = calculator.Value;
}
protected override IHasText CreateText() => new TextComponent

View File

@ -10,7 +10,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.KPSCounter;
using osuTK;
using osuTK.Graphics;
@ -56,7 +56,7 @@ namespace osu.Game.Screens.Play
public void Increment()
{
KeysPerSecondCounter.AddTimestamp();
KeysPerSecondCalculator.AddInput();
if (!IsCounting)
return;

View File

@ -11,7 +11,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Screens.Play.HUD;
using osuTK;
using osuTK.Graphics;
@ -66,7 +65,6 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
KeysPerSecondCounter.Reset();
config.BindWith(OsuSetting.KeyOverlay, configVisibility);
}

View File

@ -34,7 +34,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
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.Skinning;
using osu.Game.Users;
@ -1046,7 +1046,7 @@ namespace osu.Game.Screens.Play
fadeOut();
KeysPerSecondCounter.Reset();
KeysPerSecondCalculator.GetInstance().Dispose();
return base.OnExiting(e);
}