diff --git a/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs new file mode 100644 index 0000000000..e6dc72033a --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs @@ -0,0 +1,111 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class SpinFramesGenerator + { + /// + /// A small amount to spin beyond a given angle to mitigate floating-point precision errors. + /// + public const float SPIN_ERROR = MathF.PI / 8; + + /// + /// The offset from the centre of the spinner at which to spin. + /// + private const float centre_spin_offset = 50; + + private readonly double startTime; + private readonly float startAngle; + private readonly List<(float deltaAngle, double duration)> sequences = new List<(float deltaAngle, double duration)>(); + + /// + /// Creates a new that can be used to generate spinner spin frames. + /// + /// The time at which to start spinning. + /// The angle, in radians, at which to start spinning from. Defaults to the positive-y-axis. + public SpinFramesGenerator(double startTime, float startAngle = -MathF.PI / 2f) + { + this.startTime = startTime; + this.startAngle = startAngle; + } + + /// + /// Performs a single spin. + /// + /// The amount of degrees to spin. + /// The time to spend to perform the spin. + /// This . + public SpinFramesGenerator Spin(float delta, double duration) + { + sequences.Add((delta / 360 * 2 * MathF.PI, duration)); + return this; + } + + /// + /// Constructs the replay frames. + /// + /// The replay frames. + public List Build() + { + List frames = new List(); + + double lastTime = startTime; + float lastAngle = startAngle; + int lastDirection = 0; + + for (int i = 0; i < sequences.Count; i++) + { + var seq = sequences[i]; + + int seqDirection = Math.Sign(seq.deltaAngle); + float seqError = SPIN_ERROR * seqDirection; + + if (seqDirection == lastDirection) + { + // Spinning in the same direction, but the error was already added in the last rotation. + seqError = 0; + } + else if (lastDirection != 0) + { + // Spinning in a different direction, we need to account for the error of the start angle, so double it. + seqError *= 2; + } + + double seqStartTime = lastTime; + double seqEndTime = lastTime + seq.duration; + float seqStartAngle = lastAngle; + float seqEndAngle = seqStartAngle + seq.deltaAngle + seqError; + + // Intermediate spin frames. + for (; lastTime < seqEndTime; lastTime += 10) + frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt((lastTime - seqStartTime) / (seqEndTime - seqStartTime), seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + + // Final frame at the end of the current spin. + frames.Add(new OsuReplayFrame(seqEndTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton)); + + lastTime = seqEndTime; + lastAngle = seqEndAngle; + lastDirection = seqDirection; + } + + // Key release frame. + if (frames.Count > 0) + frames.Add(new OsuReplayFrame(frames[^1].Time, ((OsuReplayFrame)frames[^1]).Position)); + + return frames; + } + + private static Vector2 calcOffsetAt(double p, float startAngle, float endAngle) + { + float angle = startAngle + (endAngle - startAngle) * (float)p; + return new Vector2(256, 192) + centre_spin_offset * new Vector2(MathF.Cos(angle), MathF.Sin(angle)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs index a2ef72fe57..fa6aa580a3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs @@ -356,15 +356,16 @@ namespace osu.Game.Rulesets.Osu.Tests }, }; - performTest(hitObjects, new List + List frames = new List { new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - }); + }; + + frames.AddRange(new SpinFramesGenerator(time_spinner + 10) + .Spin(360, 500) + .Build()); + + performTest(hitObjects, frames); addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Meh); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 1ae17432be..dae81f4cff 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); AddStep("rotate some", () => dho.RotationTracker.AddRotation(180)); - AddAssert("rotation is set", () => dho.Result.RateAdjustedRotation == 180); + AddAssert("rotation is set", () => dho.Result.TotalRotation == 180); AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner { @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests Duration = 1000, }))); - AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0); + AddAssert("rotation is reset", () => dho.Result.TotalRotation == 0); } private Spinner prepareObject(Spinner circle) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs new file mode 100644 index 0000000000..c4bf0d4e2e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -0,0 +1,290 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneSpinnerInput : RateAdjustedBeatmapTestScene + { + private const int centre_x = 256; + private const int centre_y = 192; + private const double time_spinner_start = 1500; + private const double time_spinner_end = 8000; + + private readonly List judgementResults = new List(); + + private ScoreAccessibleReplayPlayer currentPlayer = null!; + private ManualClock? manualClock; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) + { + return manualClock == null + ? base.CreateWorkingBeatmap(beatmap, storyboard) + : new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio); + } + + [SetUp] + public void Setup() => Schedule(() => + { + manualClock = null; + }); + + /// + /// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms. + /// + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestVibrateWithoutSpinningOffCentre() + { + List frames = new List(); + + const int vibrate_time = 50; + const float y_pos = centre_y - 50; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, y_pos), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, y_pos), OsuAction.LeftButton)); + + direction *= -1; + } + + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms. + /// + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestVibrateWithoutSpinningOnCentre() + { + List frames = new List(); + + const int vibrate_time = 50; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton)); + + direction *= -1; + } + + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// Spins in a single direction. + /// + [TestCase(180, 0)] + [TestCase(-180, 0)] + [TestCase(360, 1)] + [TestCase(-360, 1)] + [TestCase(540, 1)] + [TestCase(-540, 1)] + [TestCase(720, 2)] + [TestCase(-720, 2)] + public void TestSpinSingleDirection(float amount, int expectedTicks) + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(amount, 500) + .Build()); + + assertTicksHit(expectedTicks); + assertSpinnerHit(false); + } + + /// + /// Spin half-way clockwise then perform one full spin counter-clockwise. + /// No ticks should be hit since the total rotation is -0.5 (0.5 CW + 1 CCW = 0.5 CCW). + /// + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestSpinHalfBothDirections() + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(180, 500) // Rotate to +0.5. + .Spin(-360, 500) // Rotate to -0.5 + .Build()); + + assertTicksHit(0); + assertSpinnerHit(false); + } + + /// + /// Spin in one direction then spin in the other. + /// + [TestCase(180, -540, 1)] + [TestCase(-180, 540, 1)] + [TestCase(180, -900, 2)] + [TestCase(-180, 900, 2)] + [Ignore("An upcoming implementation will fix this case")] + public void TestSpinOneDirectionThenChangeDirection(float direction1, float direction2, int expectedTicks) + { + performTest(new SpinFramesGenerator(time_spinner_start) + .Spin(direction1, 500) + .Spin(direction2, 500) + .Build()); + + assertTicksHit(expectedTicks); + assertSpinnerHit(false); + } + + [Test] + [Ignore("An upcoming implementation will fix this case")] + public void TestRewind() + { + AddStep("set manual clock", () => manualClock = new ManualClock { Rate = 1 }); + + List frames = new SpinFramesGenerator(time_spinner_start) + .Spin(360, 500) // 2000ms -> 1 full CW spin + .Spin(-180, 500) // 2500ms -> 0.5 CCW spins + .Spin(90, 500) // 3000ms -> 0.25 CW spins + .Spin(450, 500) // 3500ms -> 1 full CW spin + .Spin(180, 500) // 4000ms -> 0.5 CW spins + .Build(); + + loadPlayer(frames); + + GameplayClockContainer clock = null!; + DrawableRuleset drawableRuleset = null!; + AddStep("get gameplay objects", () => + { + clock = currentPlayer.ChildrenOfType().Single(); + drawableRuleset = currentPlayer.ChildrenOfType().Single(); + }); + + addSeekStep(frames.Last().Time); + + DrawableSpinner drawableSpinner = null!; + AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType().Single()) != null); + + assertTotalRotation(4000, 900); + assertTotalRotation(3750, 810); + assertTotalRotation(3500, 720); + assertTotalRotation(3250, 530); + assertTotalRotation(3000, 540); + assertTotalRotation(2750, 540); + assertTotalRotation(2500, 540); + assertTotalRotation(2250, 360); + assertTotalRotation(2000, 180); + assertTotalRotation(1500, 0); + + void assertTotalRotation(double time, float expected) + { + addSeekStep(time); + AddAssert($"total rotation @ {time} is {expected}", () => drawableSpinner.Result.TotalRotation, + () => Is.EqualTo(expected).Within(MathHelper.RadiansToDegrees(SpinFramesGenerator.SPIN_ERROR * 2))); + } + + void addSeekStep(double time) + { + AddStep($"seek to {time}", () => clock.Seek(time)); + AddUntilStep("wait for seek to finish", () => drawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time)); + } + } + + private void assertTicksHit(int count) + { + AddAssert($"{count} ticks hit", () => judgementResults.Where(r => r.HitObject is SpinnerTick).Count(r => r.IsHit), () => Is.EqualTo(count)); + } + + private void assertSpinnerHit(bool shouldBeHit) + { + AddAssert($"spinner is {(shouldBeHit ? "hit" : "missed")}", () => judgementResults.Single(r => r.HitObject is Spinner).IsHit, () => Is.EqualTo(shouldBeHit)); + } + + private void loadPlayer(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new Spinner + { + StartTime = time_spinner_start, + EndTime = time_spinner_end, + Position = new Vector2(centre_x, centre_y) + } + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty(), + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults.Clear(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + } + + private void performTest(List frames) + { + loadPlayer(frames); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private partial class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs index c969cb11b4..8d8c2e9639 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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 NUnit.Framework; @@ -11,14 +10,12 @@ using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; -using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -59,26 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult)); } - private static List generateReplay(int spins) - { - var replayFrames = new List(); - - const int frames_per_spin = 30; - - for (int i = 0; i < spins * frames_per_spin; ++i) - { - float totalProgress = i / (float)(spins * frames_per_spin); - float spinProgress = (i % frames_per_spin) / (float)frames_per_spin; - double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress; - float posX = MathF.Cos(2 * MathF.PI * spinProgress); - float posY = MathF.Sin(2 * MathF.PI * spinProgress); - Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50; - - replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton)); - } - - return replayFrames; - } + private static List generateReplay(int spins) => new SpinFramesGenerator(time_spinner_start) + .Spin(spins * 360, time_spinner_end - time_spinner_start) + .Build(); private void performTest(List frames) { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 116c974f32..8711aa9c09 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -63,11 +63,11 @@ namespace osu.Game.Rulesets.Osu.Tests trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); }); AddAssert("is disc rotation not almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.Not.EqualTo(0).Within(100)); - AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.Not.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute not almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.Not.EqualTo(0).Within(100)); addSeekStep(0); AddAssert("is disc rotation almost 0", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(0).Within(trackerRotationTolerance)); - AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(0).Within(100)); + AddAssert("is disc rotation absolute almost 0", () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(0).Within(100)); } [Test] @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Tests finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f); }); - AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); + AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.TotalRotation); addSeekStep(spinner_start_time + 2500); AddAssert("disc rotation rewound", @@ -92,13 +92,13 @@ namespace osu.Game.Rulesets.Osu.Tests () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance)); AddAssert("is cumulative rotation rewound", // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. - () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); + () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100)); addSeekStep(spinner_start_time + 5000); AddAssert("is disc rotation almost same", () => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance)); AddAssert("is cumulative rotation almost same", - () => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); + () => drawableSpinner.Result.TotalRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100)); } [Test] @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) long totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; + return totalScore == (int)(drawableSpinner.Result.TotalRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index f0af3f0c39..3475680c71 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -284,15 +284,16 @@ namespace osu.Game.Rulesets.Osu.Tests }, }; - performTest(hitObjects, new List + List frames = new List { new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, - new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, - }); + }; + + frames.AddRange(new SpinFramesGenerator(time_spinner + 10) + .Spin(360, 500) + .Build()); + + performTest(hitObjects, frames); addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Great); diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs index 941cb667cf..c5e15d63ea 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Judgements /// If Double Time is active instead (with a speed multiplier of 1.5x), /// in the same scenario the property will return 720 * 1.5 = 1080. /// - public float RateAdjustedRotation; + public float TotalRotation; /// /// Time instant at which the spin was started (the first user input which caused an increase in spin). diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 24446db92a..9fa180cf93 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // these become implicitly hit. return 1; - return Math.Clamp(Result.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1); + return Math.Clamp(Result.TotalRotation / 360 / HitObject.SpinsRequired, 0, 1); } } @@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // 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) - spmCalculator.SetRotation(Result.RateAdjustedRotation); + spmCalculator.SetRotation(Result.TotalRotation); updateBonusScore(); } @@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (ticks.Count == 0) return; - int spins = (int)(Result.RateAdjustedRotation / 360); + int spins = (int)(Result.TotalRotation / 360); if (spins < completedFullSpins) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs index bdc93eb63f..079758c21e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { get { - int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.TotalRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs index 75f3247448..b498975a83 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { get { - int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.TotalRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 719cf57d98..69c2bf3dd0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; @@ -22,11 +23,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private readonly DrawableSpinner drawableSpinner; - private Vector2 mousePosition; + private Vector2? mousePosition; + private float? lastAngle; - private float lastAngle; private float currentRotation; - private bool rotationTransferred; [Resolved(canBeNull: true)] @@ -63,17 +63,23 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default protected override void Update() { base.Update(); - float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - float delta = thisAngle - lastAngle; + if (mousePosition is Vector2 pos) + { + float thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(pos.X - DrawSize.X / 2, pos.Y - DrawSize.Y / 2)); + float delta = lastAngle == null ? 0 : thisAngle - lastAngle.Value; - if (Tracking) - AddRotation(delta); + // Normalise the delta to -180 .. 180 + if (delta > 180) delta -= 360; + if (delta < -180) delta += 360; - lastAngle = thisAngle; + if (Tracking) + AddRotation(delta); + + lastAngle = thisAngle; + } IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f; - Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed)); } @@ -83,41 +89,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// /// Will be a no-op if not a valid time to spin. /// - /// The delta angle. - public void AddRotation(float angle) + /// The delta angle. + public void AddRotation(float delta) { if (!isSpinnableTime) return; if (!rotationTransferred) { - currentRotation = Rotation * 2; + currentRotation = Rotation; rotationTransferred = true; } - if (angle > 180) - { - lastAngle += 360; - angle -= 360; - } - else if (-angle > 180) - { - lastAngle -= 360; - angle += 360; - } + currentRotation += delta; + + double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate; + + Debug.Assert(Math.Abs(delta) <= 180); - currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate)); + drawableSpinner.Result.TotalRotation += (float)(Math.Abs(delta) * rate); } private void resetState(DrawableHitObject obj) { Tracking = false; IsSpinning.Value = false; - mousePosition = default; - lastAngle = currentRotation = Rotation = 0; + mousePosition = null; + lastAngle = null; + currentRotation = 0; + Rotation = 0; rotationTransferred = false; }