1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 05:42:54 +08:00

Merge pull request #12217 from frenzibyte/legacy-spm-counter

Add support for legacy spinner SPM counter
This commit is contained in:
Dan Balasescu 2021-04-12 20:19:05 +09:00 committed by GitHub
commit 32868d0d45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 152 additions and 102 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.410.0" />
</ItemGroup>
</Project>

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -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

View File

@ -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,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
AddRangeInternal(new Drawable[]
{
spmCalculator = new SpinnerSpmCalculator
{
Result = { BindTarget = SpinsPerMinute },
},
ticks = new Container<DrawableSpinnerTick>(),
new AspectContainer
{
@ -77,20 +87,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 +164,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 +274,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;

View File

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

View File

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

View File

@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected const float SPRITE_SCALE = 0.625f;
private const float spm_hide_offset = 50f;
protected DrawableSpinner DrawableSpinner { get; private set; }
private Sprite spin;
@ -35,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private LegacySpriteText bonusCounter;
private Sprite spmBackground;
private LegacySpriteText spmCounter;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
{
@ -79,11 +84,27 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 299,
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
spmBackground = new Sprite
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft,
Texture = source.GetTexture("spinner-rpm"),
Scale = new Vector2(SPRITE_SCALE),
Position = new Vector2(-87, 445 + spm_hide_offset),
},
spmCounter = new LegacySpriteText(source, LegacyFont.Score)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight,
Scale = new Vector2(SPRITE_SCALE * 0.9f),
Position = new Vector2(80, 448 + spm_hide_offset),
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
}
});
}
private IBindable<double> gainedBonus;
private IBindable<double> spinsPerMinute;
private readonly Bindable<bool> completed = new Bindable<bool>();
@ -99,6 +120,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
});
spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
spinsPerMinute.BindValueChanged(spm =>
{
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
}, true);
completed.BindValueChanged(onCompletedChanged, true);
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
@ -142,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (drawableHitObject)
{
case DrawableSpinner d:
double fadeOutLength = Math.Min(400, d.HitObject.Duration);
using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn))
{
spmBackground.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
}
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true))
spin.FadeOutFromOne(fadeOutLength);
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
spin.FadeOutFromOne(spinFadeOutLength);
break;
case DrawableSpinnerTick d:

View File

@ -30,7 +30,7 @@
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.410.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
<PackageReference Include="Sentry" Version="3.2.0" />
<PackageReference Include="SharpCompress" Version="0.28.1" />
<PackageReference Include="NUnit" Version="3.13.1" />

View File

@ -71,7 +71,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.410.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>