Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply various fixes and cleanup to spinner logic #25143

Merged
merged 9 commits into from
Oct 16, 2023
111 changes: 111 additions & 0 deletions osu.Game.Rulesets.Osu.Tests/SpinFramesGenerator.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
15 changes: 8 additions & 7 deletions osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -356,15 +356,16 @@ public void TestHitCircleBeforeSpinner()
},
};

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);
Expand Down
4 changes: 2 additions & 2 deletions osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public void TestApplyNewSpinner()
});

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
{
Expand All @@ -41,7 +41,7 @@ public void TestApplyNewSpinner()
Duration = 1000,
})));

AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0);
AddAssert("rotation is reset", () => dho.Result.TotalRotation == 0);
}

private Spinner prepareObject(Spinner circle)
Expand Down