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

Merge pull request #25143 from peppy/split-spinner-fix

Apply various fixes and cleanup to spinner logic
This commit is contained in:
Bartłomiej Dach 2023-10-16 22:30:24 +02:00 committed by GitHub
commit 240d317928
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 462 additions and 77 deletions

View File

@ -0,0 +1,111 @@
// 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 osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class SpinFramesGenerator
{
/// <summary>
/// A small amount to spin beyond a given angle to mitigate floating-point precision errors.
/// </summary>
public const float SPIN_ERROR = MathF.PI / 8;
/// <summary>
/// The offset from the centre of the spinner at which to spin.
/// </summary>
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)>();
/// <summary>
/// Creates a new <see cref="SpinFramesGenerator"/> that can be used to generate spinner spin frames.
/// </summary>
/// <param name="startTime">The time at which to start spinning.</param>
/// <param name="startAngle">The angle, in radians, at which to start spinning from. Defaults to the positive-y-axis.</param>
public SpinFramesGenerator(double startTime, float startAngle = -MathF.PI / 2f)
{
this.startTime = startTime;
this.startAngle = startAngle;
}
/// <summary>
/// Performs a single spin.
/// </summary>
/// <param name="delta">The amount of degrees to spin.</param>
/// <param name="duration">The time to spend to perform the spin.</param>
/// <returns>This <see cref="SpinFramesGenerator"/>.</returns>
public SpinFramesGenerator Spin(float delta, double duration)
{
sequences.Add((delta / 360 * 2 * MathF.PI, duration));
return this;
}
/// <summary>
/// Constructs the replay frames.
/// </summary>
/// <returns>The replay frames.</returns>
public List<ReplayFrame> Build()
{
List<ReplayFrame> frames = new List<ReplayFrame>();
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));
}
}
}

View File

@ -356,15 +356,16 @@ namespace osu.Game.Rulesets.Osu.Tests
},
};
performTest(hitObjects, new List<ReplayFrame>
List<ReplayFrame> frames = new List<ReplayFrame>
{
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);

View File

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

View File

@ -0,0 +1,290 @@
// 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.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<JudgementResult> judgementResults = new List<JudgementResult>();
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;
});
/// <summary>
/// While off-centre, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
/// </summary>
[Test]
[Ignore("An upcoming implementation will fix this case")]
public void TestVibrateWithoutSpinningOffCentre()
{
List<ReplayFrame> frames = new List<ReplayFrame>();
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);
}
/// <summary>
/// While centred on the slider, vibrates backwards and forwards on the x-axis, from centre-50 to centre+50, every 50ms.
/// </summary>
[Test]
[Ignore("An upcoming implementation will fix this case")]
public void TestVibrateWithoutSpinningOnCentre()
{
List<ReplayFrame> frames = new List<ReplayFrame>();
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);
}
/// <summary>
/// Spins in a single direction.
/// </summary>
[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);
}
/// <summary>
/// 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).
/// </summary>
[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);
}
/// <summary>
/// Spin in one direction then spin in the other.
/// </summary>
[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<ReplayFrame> 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<GameplayClockContainer>().Single();
drawableRuleset = currentPlayer.ChildrenOfType<DrawableRuleset>().Single();
});
addSeekStep(frames.Last().Time);
DrawableSpinner drawableSpinner = null!;
AddUntilStep("get spinner", () => (drawableSpinner = currentPlayer.ChildrenOfType<DrawableSpinner>().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<ReplayFrame> frames)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{
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<ReplayFrame> 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,
})
{
}
}
}
}

View File

@ -1,7 +1,6 @@
// 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 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<ReplayFrame> generateReplay(int spins)
{
var replayFrames = new List<ReplayFrame>();
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<ReplayFrame> generateReplay(int spins) => new SpinFramesGenerator(time_spinner_start)
.Spin(spins * 360, time_spinner_end - time_spinner_start)
.Build();
private void performTest(List<ReplayFrame> frames)
{

View File

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

View File

@ -284,15 +284,16 @@ namespace osu.Game.Rulesets.Osu.Tests
},
};
performTest(hitObjects, new List<ReplayFrame>
List<ReplayFrame> frames = new List<ReplayFrame>
{
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);

View File

@ -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.
/// </example>
public float RateAdjustedRotation;
public float TotalRotation;
/// <summary>
/// Time instant at which the spin was started (the first user input which caused an increase in spin).

View File

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

View File

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

View File

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

View File

@ -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
/// <remarks>
/// Will be a no-op if not a valid time to spin.
/// </remarks>
/// <param name="angle">The delta angle.</param>
public void AddRotation(float angle)
/// <param name="delta">The delta angle.</param>
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;
}