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

Refactor spinner SPM counter for skinning purposes

This commit is contained in:
Salman Ahmed 2021-03-26 13:09:44 +03:00
parent f49481e308
commit 0bf84e473d
6 changed files with 114 additions and 96 deletions

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private void runSpmTest(Mod mod) private void runSpmTest(Mod mod)
{ {
SpinnerSpmCounter spmCounter = null; SpinnerSpmCalculator spmCalculator = null;
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1 PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
}); });
AddUntilStep("fetch SPM counter", () => AddUntilStep("fetch SPM calculator", () =>
{ {
spmCounter = this.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault(); spmCalculator = this.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
return spmCounter != null; return spmCalculator != null;
}); });
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5)); AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5));
} }
} }
} }

View File

@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Beatmap = singleSpinnerBeatmap, Beatmap = singleSpinnerBeatmap,
PassCondition = () => PassCondition = () =>
{ {
var counter = Player.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault(); var counter = Player.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1); return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
} }
}); });
} }

View File

@ -168,13 +168,13 @@ namespace osu.Game.Rulesets.Osu.Tests
double estimatedSpm = 0; double estimatedSpm = 0;
addSeekStep(1000); addSeekStep(1000);
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
addSeekStep(2000); addSeekStep(2000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
addSeekStep(1000); addSeekStep(1000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
} }
[TestCase(0.5)] [TestCase(0.5)]
@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("retrieve spinner state", () => AddStep("retrieve spinner state", () =>
{ {
expectedProgress = drawableSpinner.Progress; expectedProgress = drawableSpinner.Progress;
expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute; expectedSpm = drawableSpinner.SpinsPerMinute.Value;
}); });
addSeekStep(0); addSeekStep(0);
@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(1000); addSeekStep(1000);
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0)); AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
} }
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay

View File

@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result; public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
public SpinnerRotationTracker RotationTracker { get; private set; } public SpinnerRotationTracker RotationTracker { get; private set; }
public SpinnerSpmCounter SpmCounter { get; private set; }
private SpinnerSpmCalculator spmCalculator;
private Container<DrawableSpinnerTick> ticks; private Container<DrawableSpinnerTick> ticks;
private PausableSkinnableSound spinningSample; private PausableSkinnableSound spinningSample;
@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary> /// </summary>
public IBindable<double> GainedBonus => gainedBonus; public IBindable<double> GainedBonus => gainedBonus;
private readonly Bindable<double> gainedBonus = new Bindable<double>(); private readonly Bindable<double> gainedBonus = new BindableDouble();
/// <summary>
/// The number of spins per minute this spinner is spinning at, for display purposes.
/// </summary>
public readonly IBindable<double> SpinsPerMinute = new BindableDouble();
private const double fade_out_duration = 160; private const double fade_out_duration = 160;
@ -63,7 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre; Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[] AddInternal(spmCalculator = new SpinnerSpmCalculator
{
Result = { BindTarget = SpinsPerMinute },
});
AddRangeInternal(new Drawable[]
{ {
ticks = new Container<DrawableSpinnerTick>(), ticks = new Container<DrawableSpinnerTick>(),
new AspectContainer new AspectContainer
@ -77,20 +88,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RotationTracker = new SpinnerRotationTracker(this) RotationTracker = new SpinnerRotationTracker(this)
} }
}, },
SpmCounter = new SpinnerSpmCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 120,
Alpha = 0
},
spinningSample = new PausableSkinnableSound spinningSample = new PausableSkinnableSound
{ {
Volume = { Value = 0 }, Volume = { Value = 0 },
Looping = true, Looping = true,
Frequency = { Value = spinning_sample_initial_frequency } Frequency = { Value = spinning_sample_initial_frequency }
} }
}; });
PositionBindable.BindValueChanged(pos => Position = pos.NewValue); PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
} }
@ -161,17 +165,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
} }
protected override void UpdateStartTimeStateTransforms()
{
base.UpdateStartTimeStateTransforms();
if (Result?.TimeStarted is double startTime)
{
using (BeginAbsoluteSequence(startTime))
fadeInCounter();
}
}
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)
{ {
base.UpdateHitStateTransforms(state); base.UpdateHitStateTransforms(state);
@ -282,22 +275,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
if (!SpmCounter.IsPresent && RotationTracker.Tracking) if (Result.TimeStarted == null && RotationTracker.Tracking)
{ Result.TimeStarted = Time.Current;
Result.TimeStarted ??= Time.Current;
fadeInCounter();
}
// don't update after end time to avoid the rate display dropping during fade out. // don't update after end time to avoid the rate display dropping during fade out.
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period. // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
if (Time.Current <= HitObject.EndTime) if (Time.Current <= HitObject.EndTime)
SpmCounter.SetRotation(Result.RateAdjustedRotation); spmCalculator.SetRotation(Result.RateAdjustedRotation);
updateBonusScore(); updateBonusScore();
} }
private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
private int wholeSpins; private int wholeSpins;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization; using System.Globalization;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private OsuSpriteText bonusCounter; private OsuSpriteText bonusCounter;
private Container spmContainer;
private OsuSpriteText spmCounter;
public DefaultSpinner() public DefaultSpinner()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre, Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 24), Font = OsuFont.Numeric.With(size: 24),
Y = -120, Y = -120,
},
spmContainer = new Container
{
Alpha = 0f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 120,
Children = new[]
{
spmCounter = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"0",
Font = OsuFont.Numeric.With(size: 24)
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"SPINS PER MINUTE",
Font = OsuFont.Numeric.With(size: 12),
Y = 30
}
}
} }
}); });
} }
private IBindable<double> gainedBonus; private IBindable<double> gainedBonus;
private IBindable<double> spinsPerMinute;
protected override void LoadComplete() protected override void LoadComplete()
{ {
@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
bonusCounter.FadeOutFromOne(1500); bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
}); });
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
spinsPerMinute.BindValueChanged(spm =>
{
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
}, true);
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
{
base.Update();
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
fadeCounterOnTimeStart();
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSpinner))
return;
fadeCounterOnTimeStart();
}
private void fadeCounterOnTimeStart()
{
if (drawableSpinner.Result?.TimeStarted is double startTime)
{
using (BeginAbsoluteSequence(startTime))
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
}
} }
} }
} }

View File

@ -1,77 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Default namespace osu.Game.Rulesets.Osu.Skinning.Default
{ {
public class SpinnerSpmCounter : Container public class SpinnerSpmCalculator : Component
{ {
private readonly Queue<RotationRecord> records = new Queue<RotationRecord>();
private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
/// <summary>
/// The resultant spins per minute value, which is updated via <see cref="SetRotation"/>.
/// </summary>
public IBindable<double> Result => result;
private readonly Bindable<double> result = new BindableDouble();
[Resolved] [Resolved]
private DrawableHitObject drawableSpinner { get; set; } private DrawableHitObject drawableSpinner { get; set; }
private readonly OsuSpriteText spmText;
public SpinnerSpmCounter()
{
Children = new Drawable[]
{
spmText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"0",
Font = OsuFont.Numeric.With(size: 24)
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"SPINS PER MINUTE",
Font = OsuFont.Numeric.With(size: 12),
Y = 30
}
};
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
drawableSpinner.HitObjectApplied += resetState; drawableSpinner.HitObjectApplied += resetState;
} }
private double spm;
public double SpinsPerMinute
{
get => spm;
private set
{
if (value == spm) return;
spm = value;
spmText.Text = Math.Truncate(value).ToString(@"#0");
}
}
private struct RotationRecord
{
public float Rotation;
public double Time;
}
private readonly Queue<RotationRecord> records = new Queue<RotationRecord>();
private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
public void SetRotation(float currentRotation) public void SetRotation(float currentRotation)
{ {
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result. // Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration) while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
record = records.Dequeue(); record = records.Dequeue();
SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360; result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
} }
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current }); records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private void resetState(DrawableHitObject hitObject) private void resetState(DrawableHitObject hitObject)
{ {
SpinsPerMinute = 0; result.Value = 0;
records.Clear(); records.Clear();
} }
@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
if (drawableSpinner != null) if (drawableSpinner != null)
drawableSpinner.HitObjectApplied -= resetState; drawableSpinner.HitObjectApplied -= resetState;
} }
private struct RotationRecord
{
public float Rotation;
public double Time;
}
} }
} }