1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 16:07:24 +08:00

Add SpinnerSpinHistory and tests

This commit is contained in:
Dean Herbert 2023-10-16 19:07:03 +09:00
parent d9fc532a9f
commit af7180a5b5
No known key found for this signature in database
5 changed files with 282 additions and 31 deletions

View File

@ -0,0 +1,132 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class SpinnerSpinHistoryTest
{
private SpinnerSpinHistory history = null!;
[SetUp]
public void Setup()
{
history = new SpinnerSpinHistory();
}
[TestCase(0, 0)]
[TestCase(10, 10)]
[TestCase(180, 180)]
[TestCase(350, 350)]
[TestCase(360, 360)]
[TestCase(370, 370)]
[TestCase(540, 540)]
[TestCase(720, 720)]
// ---
[TestCase(-0, 0)]
[TestCase(-10, 10)]
[TestCase(-180, 180)]
[TestCase(-350, 350)]
[TestCase(-360, 360)]
[TestCase(-370, 370)]
[TestCase(-540, 540)]
[TestCase(-720, 720)]
public void TestSpinOneDirection(float spin, float expectedRotation)
{
history.ReportDelta(500, spin);
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
}
[TestCase(0, 0, 0, 0)]
// ---
[TestCase(10, -10, 0, 10)]
[TestCase(-10, 10, 0, 10)]
// ---
[TestCase(10, -20, 0, 10)]
[TestCase(-10, 20, 0, 10)]
// ---
[TestCase(20, -10, 0, 20)]
[TestCase(-20, 10, 0, 20)]
// ---
[TestCase(10, -360, 0, 350)]
[TestCase(-10, 360, 0, 350)]
// ---
[TestCase(360, -10, 0, 370)]
[TestCase(360, 10, 0, 370)]
[TestCase(-360, 10, 0, 370)]
[TestCase(-360, -10, 0, 370)]
// ---
[TestCase(10, 10, 10, 30)]
[TestCase(10, 10, -10, 20)]
[TestCase(10, -10, 10, 10)]
[TestCase(-10, -10, -10, 30)]
[TestCase(-10, -10, 10, 20)]
[TestCase(-10, 10, 10, 10)]
// ---
[TestCase(10, -20, -350, 360)]
[TestCase(10, -20, 350, 340)]
[TestCase(-10, 20, 350, 360)]
[TestCase(-10, 20, -350, 340)]
public void TestSpinMultipleDirections(float spin1, float spin2, float spin3, float expectedRotation)
{
history.ReportDelta(500, spin1);
history.ReportDelta(1000, spin2);
history.ReportDelta(1500, spin3);
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
}
// One spin
[TestCase(370, -50, 320)]
[TestCase(-370, 50, 320)]
// Two spins
[TestCase(740, -420, 320)]
[TestCase(-740, 420, 320)]
public void TestRemoveAndCrossFullSpin(float deltaToAdd, float deltaToRemove, float expectedRotation)
{
history.ReportDelta(1000, deltaToAdd);
history.ReportDelta(500, deltaToRemove);
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
}
// One spin + partial
[TestCase(400, -30, -50, 320)]
[TestCase(-400, 30, 50, 320)]
// Two spins + partial
[TestCase(800, -430, -50, 320)]
[TestCase(-800, 430, 50, 320)]
public void TestRemoveAndCrossFullAndPartialSpins(float deltaToAdd1, float deltaToAdd2, float deltaToRemove, float expectedRotation)
{
history.ReportDelta(1000, deltaToAdd1);
history.ReportDelta(1500, deltaToAdd2);
history.ReportDelta(500, deltaToRemove);
Assert.That(history.TotalRotation, Is.EqualTo(expectedRotation));
}
[Test]
public void TestRewindMultipleFullSpins()
{
history.ReportDelta(500, 360);
history.ReportDelta(1000, 720);
Assert.That(history.TotalRotation, Is.EqualTo(1080));
history.ReportDelta(250, -180);
Assert.That(history.TotalRotation, Is.EqualTo(180));
}
[Test]
public void TestRewindIntoSegmentThatHasNotCrossedZero()
{
history.ReportDelta(1000, -180);
history.ReportDelta(1500, 90);
history.ReportDelta(2000, 450);
history.ReportDelta(1750, -45);
Assert.That(history.TotalRotation, Is.EqualTo(180));
}
}
}

View File

@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Tests
/// 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>();
@ -81,7 +80,6 @@ namespace osu.Game.Rulesets.Osu.Tests
/// 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>();
@ -130,7 +128,6 @@ namespace osu.Game.Rulesets.Osu.Tests
/// 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)
@ -149,7 +146,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[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)
@ -162,7 +158,6 @@ namespace osu.Game.Rulesets.Osu.Tests
}
[Test]
[Ignore("An upcoming implementation will fix this case")]
public void TestRewind()
{
AddStep("set manual clock", () => manualClock = new ManualClock { Rate = 1 });

View File

@ -4,6 +4,7 @@
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Judgements
{
@ -15,28 +16,15 @@ namespace osu.Game.Rulesets.Osu.Judgements
public Spinner Spinner => (Spinner)HitObject;
/// <summary>
/// The total rotation performed on the spinner disc, disregarding the spin direction,
/// adjusted for the track's playback rate.
/// The total amount that the spinner was rotated.
/// </summary>
/// <remarks>
/// <para>
/// This value is always non-negative and is monotonically increasing with time
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
/// </para>
/// <para>
/// The rotation from each frame is multiplied by the clock's current playback rate.
/// The reason this is done is to ensure that spinners give the same score and require the same number of spins
/// regardless of whether speed-modifying mods are applied.
/// </para>
/// </remarks>
/// <example>
/// Assuming no speed-modifying mods are active,
/// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
/// this property will return the value of 720 (as opposed to 0).
/// 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 TotalRotation;
public float TotalRotation => History.TotalRotation;
/// <summary>
/// Stores the spinning history of the spinner.<br />
/// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner.
/// </summary>
public readonly SpinnerSpinHistory History = new SpinnerSpinHistory();
/// <summary>
/// Time instant at which the spin was started (the first user input which caused an increase in spin).

View File

@ -0,0 +1,138 @@
// 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.Diagnostics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
/// <summary>
/// Stores the spinning history of a single spinner.<br />
/// Instants of movement deltas may be added or removed from this in order to calculate the total rotation for the spinner.
/// </summary>
/// <remarks>
/// A single, full rotation of the spinner is defined as a 360-degree rotation of the spinner, starting from 0, going in a single direction.<br />
/// </remarks>
/// <example>
/// If the player spins 90-degrees clockwise, then changes direction, they need to spin 90-degrees counter-clockwise to return to 0
/// and then continue rotating the spinner for another 360-degrees in the same direction.
/// </example>
public class SpinnerSpinHistory
{
/// <summary>
/// The sum of all complete spins and any current partial spin, in degrees.
/// </summary>
/// <remarks>
/// This is the final scoring value.
/// </remarks>
public float TotalRotation => 360 * segments.Count + currentMaxRotation;
/// <summary>
/// The list of all segments where either:
/// <list type="bullet">
/// <item>The spinning direction was changed.</item>
/// <item>A full spin of 360 degrees was performed in either direction.</item>
/// </list>
/// </summary>
private readonly Stack<SpinSegment> segments = new Stack<SpinSegment>();
/// <summary>
/// The total accumulated rotation.
/// </summary>
private float currentAbsoluteRotation;
private float lastCompletionAbsoluteRotation;
/// <summary>
/// For the current spin, represents the maximum rotation (from 0..360) achieved by the user.
/// </summary>
private float currentMaxRotation;
/// <summary>
/// The current spin, from -360..360.
/// </summary>
private float currentRotation => currentAbsoluteRotation - lastCompletionAbsoluteRotation;
private double lastReportTime = double.NegativeInfinity;
/// <summary>
/// Report a delta update based on user input.
/// </summary>
/// <param name="currentTime">The current time.</param>
/// <param name="delta">The delta of the angle moved through since the last report.</param>
public void ReportDelta(double currentTime, float delta)
{
// TODO: Debug.Assert(Math.Abs(delta) < 180);
// This will require important frame guarantees.
currentAbsoluteRotation += delta;
if (currentTime >= lastReportTime)
addDelta(currentTime, delta);
else
rewindDelta(currentTime, delta);
lastReportTime = currentTime;
}
private void addDelta(double currentTime, float delta)
{
if (delta == 0)
return;
currentMaxRotation = Math.Max(currentMaxRotation, Math.Abs(currentRotation));
while (currentMaxRotation >= 360)
{
int direction = Math.Sign(currentRotation);
segments.Push(new SpinSegment(currentTime, direction));
lastCompletionAbsoluteRotation += direction * 360;
currentMaxRotation = Math.Abs(currentRotation);
}
}
private void rewindDelta(double currentTime, float delta)
{
while (segments.TryPeek(out var segment) && segment.StartTime > currentTime)
{
segments.Pop();
lastCompletionAbsoluteRotation -= segment.Direction * 360;
currentMaxRotation = Math.Abs(currentRotation);
}
currentMaxRotation = Math.Abs(currentRotation);
}
/// <summary>
/// Represents a single segment of history.
/// </summary>
/// <remarks>
/// Each time the player changes direction, a new segment is recorded.
/// A segment stores the current absolute angle of rotation. Generally this would be either -360 or 360 for a completed spin, or
/// a number representing the last incomplete spin.
/// </remarks>
private class SpinSegment
{
/// <summary>
/// The start time of this segment, when the direction change occurred.
/// </summary>
public readonly double StartTime;
/// <summary>
/// The direction this segment started in.
/// </summary>
public readonly int Direction;
public SpinSegment(double startTime, int direction)
{
Debug.Assert(direction == -1 || direction == 1);
StartTime = startTime;
Direction = direction;
}
}
}
}

View File

@ -101,15 +101,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
rotationTransferred = true;
}
currentRotation += delta;
double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate;
delta = (float)(delta * Math.Abs(rate));
Debug.Assert(Math.Abs(delta) <= 180);
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (see: ModTimeRamp)
drawableSpinner.Result.TotalRotation += (float)(Math.Abs(delta) * rate);
currentRotation += delta;
drawableSpinner.Result.History.ReportDelta(Time.Current, delta);
}
private void resetState(DrawableHitObject obj)