mirror of
https://github.com/ppy/osu.git
synced 2024-12-13 08:32:57 +08:00
Refactor spinner SPM counter for skinning purposes
This commit is contained in:
parent
f49481e308
commit
0bf84e473d
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
|
||||
private void runSpmTest(Mod mod)
|
||||
{
|
||||
SpinnerSpmCounter spmCounter = null;
|
||||
SpinnerSpmCalculator spmCalculator = null;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
|
||||
});
|
||||
|
||||
AddUntilStep("fetch SPM counter", () =>
|
||||
AddUntilStep("fetch SPM calculator", () =>
|
||||
{
|
||||
spmCounter = this.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
||||
return spmCounter != null;
|
||||
spmCalculator = this.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||
return spmCalculator != null;
|
||||
});
|
||||
|
||||
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
|
||||
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
Beatmap = singleSpinnerBeatmap,
|
||||
PassCondition = () =>
|
||||
{
|
||||
var counter = Player.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
||||
return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
|
||||
var counter = Player.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||
return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -168,13 +168,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
double estimatedSpm = 0;
|
||||
|
||||
addSeekStep(1000);
|
||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
|
||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
|
||||
|
||||
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);
|
||||
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)]
|
||||
@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("retrieve spinner state", () =>
|
||||
{
|
||||
expectedProgress = drawableSpinner.Progress;
|
||||
expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
|
||||
expectedSpm = drawableSpinner.SpinsPerMinute.Value;
|
||||
});
|
||||
|
||||
addSeekStep(0);
|
||||
@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
addSeekStep(1000);
|
||||
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
|
||||
|
@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
||||
|
||||
public SpinnerRotationTracker RotationTracker { get; private set; }
|
||||
public SpinnerSpmCounter SpmCounter { get; private set; }
|
||||
|
||||
private SpinnerSpmCalculator spmCalculator;
|
||||
|
||||
private Container<DrawableSpinnerTick> ticks;
|
||||
private PausableSkinnableSound spinningSample;
|
||||
@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@ -63,7 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Origin = Anchor.Centre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
AddInternal(spmCalculator = new SpinnerSpmCalculator
|
||||
{
|
||||
Result = { BindTarget = SpinsPerMinute },
|
||||
});
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
ticks = new Container<DrawableSpinnerTick>(),
|
||||
new AspectContainer
|
||||
@ -77,20 +88,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
RotationTracker = new SpinnerRotationTracker(this)
|
||||
}
|
||||
},
|
||||
SpmCounter = new SpinnerSpmCounter
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = 120,
|
||||
Alpha = 0
|
||||
},
|
||||
spinningSample = new PausableSkinnableSound
|
||||
{
|
||||
Volume = { Value = 0 },
|
||||
Looping = true,
|
||||
Frequency = { Value = spinning_sample_initial_frequency }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
base.UpdateHitStateTransforms(state);
|
||||
@ -282,22 +275,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
|
||||
{
|
||||
Result.TimeStarted ??= Time.Current;
|
||||
fadeInCounter();
|
||||
}
|
||||
if (Result.TimeStarted == null && RotationTracker.Tracking)
|
||||
Result.TimeStarted = Time.Current;
|
||||
|
||||
// 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.
|
||||
if (Time.Current <= HitObject.EndTime)
|
||||
SpmCounter.SetRotation(Result.RateAdjustedRotation);
|
||||
spmCalculator.SetRotation(Result.RateAdjustedRotation);
|
||||
|
||||
updateBonusScore();
|
||||
}
|
||||
|
||||
private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
|
||||
|
||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||
|
||||
private int wholeSpins;
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
private OsuSpriteText bonusCounter;
|
||||
|
||||
private Container spmContainer;
|
||||
private OsuSpriteText spmCounter;
|
||||
|
||||
public DefaultSpinner()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Numeric.With(size: 24),
|
||||
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> spinsPerMinute;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
bonusCounter.FadeOutFromOne(1500);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,77 +1,37 @@
|
||||
// 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 System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
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]
|
||||
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()
|
||||
{
|
||||
base.LoadComplete();
|
||||
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)
|
||||
{
|
||||
// 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)
|
||||
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 });
|
||||
@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
private void resetState(DrawableHitObject hitObject)
|
||||
{
|
||||
SpinsPerMinute = 0;
|
||||
result.Value = 0;
|
||||
records.Clear();
|
||||
}
|
||||
|
||||
@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
if (drawableSpinner != null)
|
||||
drawableSpinner.HitObjectApplied -= resetState;
|
||||
}
|
||||
|
||||
private struct RotationRecord
|
||||
{
|
||||
public float Rotation;
|
||||
public double Time;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user