1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 20:07:29 +08:00

Merge pull request #7745 from bdach/overlapping-scroll-origin

Fix lifetime calculation in overlapping scroll algorithm
This commit is contained in:
Dan Balasescu 2020-02-07 17:15:35 +09:00 committed by GitHub
commit 230435b5a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 134 additions and 58 deletions

View File

@ -18,12 +18,21 @@ namespace osu.Game.Tests.ScrollAlgorithms
}
[Test]
public void TestDisplayStartTime()
public void TestPointDisplayStartTime()
{
Assert.AreEqual(-8000, algorithm.GetDisplayStartTime(2000, 10000));
Assert.AreEqual(-3000, algorithm.GetDisplayStartTime(2000, 5000));
Assert.AreEqual(2000, algorithm.GetDisplayStartTime(7000, 5000));
Assert.AreEqual(7000, algorithm.GetDisplayStartTime(17000, 10000));
Assert.AreEqual(-8000, algorithm.GetDisplayStartTime(2000, 0, 10000, 1));
Assert.AreEqual(-3000, algorithm.GetDisplayStartTime(2000, 0, 5000, 1));
Assert.AreEqual(2000, algorithm.GetDisplayStartTime(7000, 0, 5000, 1));
Assert.AreEqual(7000, algorithm.GetDisplayStartTime(17000, 0, 10000, 1));
}
[Test]
public void TestObjectDisplayStartTime()
{
Assert.AreEqual(900, algorithm.GetDisplayStartTime(2000, 50, 1000, 500)); // 2000 - (1 + 50 / 500) * 1000
Assert.AreEqual(8900, algorithm.GetDisplayStartTime(10000, 50, 1000, 500)); // 10000 - (1 + 50 / 500) * 1000
Assert.AreEqual(13500, algorithm.GetDisplayStartTime(15000, 250, 1000, 500)); // 15000 - (1 + 250 / 500) * 1000
Assert.AreEqual(19000, algorithm.GetDisplayStartTime(25000, 100, 5000, 500)); // 25000 - (1 + 100 / 500) * 5000
}
[Test]

View File

@ -27,11 +27,22 @@ namespace osu.Game.Tests.ScrollAlgorithms
}
[Test]
public void TestDisplayStartTime()
public void TestPointDisplayStartTime()
{
Assert.AreEqual(1000, algorithm.GetDisplayStartTime(2000, 1000)); // Like constant
Assert.AreEqual(10000, algorithm.GetDisplayStartTime(10500, 1000)); // 10500 - (1000 * 0.5)
Assert.AreEqual(20000, algorithm.GetDisplayStartTime(22000, 1000)); // 23000 - (1000 / 0.5)
Assert.AreEqual(1000, algorithm.GetDisplayStartTime(2000, 0, 1000, 1)); // Like constant
Assert.AreEqual(10000, algorithm.GetDisplayStartTime(10500, 0, 1000, 1)); // 10500 - (1000 * 0.5)
Assert.AreEqual(20000, algorithm.GetDisplayStartTime(22000, 0, 1000, 1)); // 23000 - (1000 / 0.5)
}
[Test]
public void TestObjectDisplayStartTime()
{
Assert.AreEqual(900, algorithm.GetDisplayStartTime(2000, 50, 1000, 500)); // 2000 - (1 + 50 / 500) * 1000 / 1
Assert.AreEqual(9450, algorithm.GetDisplayStartTime(10000, 50, 1000, 500)); // 10000 - (1 + 50 / 500) * 1000 / 2
Assert.AreEqual(14250, algorithm.GetDisplayStartTime(15000, 250, 1000, 500)); // 15000 - (1 + 250 / 500) * 1000 / 2
Assert.AreEqual(16500, algorithm.GetDisplayStartTime(18000, 250, 2000, 500)); // 18000 - (1 + 250 / 500) * 2000 / 2
Assert.AreEqual(17800, algorithm.GetDisplayStartTime(20000, 50, 1000, 500)); // 20000 - (1 + 50 / 500) * 1000 / 0.5
Assert.AreEqual(19800, algorithm.GetDisplayStartTime(22000, 50, 1000, 500)); // 22000 - (1 + 50 / 500) * 1000 / 0.5
}
[Test]

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@ -19,6 +20,7 @@ using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -30,7 +32,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(IReadOnlyList<Mod>))]
private IReadOnlyList<Mod> mods { get; set; } = Array.Empty<Mod>();
private const int spawn_interval = 5000;
private const int time_range = 5000;
private const int spawn_rate = time_range / 10;
private readonly ScrollingTestContainer[] scrollContainers = new ScrollingTestContainer[4];
private readonly TestPlayfield[] playfields = new TestPlayfield[4];
@ -50,13 +53,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{
RelativeSizeAxes = Axes.Both,
Child = playfields[0] = new TestPlayfield(),
TimeRange = spawn_interval
TimeRange = time_range
},
scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Down)
{
RelativeSizeAxes = Axes.Both,
Child = playfields[1] = new TestPlayfield(),
TimeRange = spawn_interval
TimeRange = time_range
},
},
new Drawable[]
@ -65,13 +68,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{
RelativeSizeAxes = Axes.Both,
Child = playfields[2] = new TestPlayfield(),
TimeRange = spawn_interval
TimeRange = time_range
},
scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Right)
{
RelativeSizeAxes = Axes.Both,
Child = playfields[3] = new TestPlayfield(),
TimeRange = spawn_interval
TimeRange = time_range
}
}
}
@ -84,31 +87,55 @@ namespace osu.Game.Tests.Visual.Gameplay
{
scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0)));
for (int i = 0; i <= spawn_interval; i += 1000)
for (int i = spawn_rate / 2; i <= time_range; i += spawn_rate)
addHitObject(Time.Current + i);
hitObjectSpawnDelegate?.Cancel();
hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + spawn_interval), 1000, true);
hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + time_range), spawn_rate, true);
}
private IList<MultiplierControlPoint> testControlPoints => new List<MultiplierControlPoint>
{
new MultiplierControlPoint(time_range) { DifficultyPoint = { SpeedMultiplier = 1.25 } },
new MultiplierControlPoint(1.5 * time_range) { DifficultyPoint = { SpeedMultiplier = 1 } },
new MultiplierControlPoint(2 * time_range) { DifficultyPoint = { SpeedMultiplier = 1.5 } }
};
[Test]
public void TestScrollAlgorithms()
{
AddStep("Constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
AddStep("Overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
AddStep("Sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
AddStep("constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
AddStep("overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
AddStep("sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
AddSliderStep("Time range", 100, 10000, spawn_interval, v => scrollContainers.Where(c => c != null).ForEach(c => c.TimeRange = v));
AddStep("Add control point", () => addControlPoint(Time.Current + spawn_interval));
AddSliderStep("time range", 100, 10000, time_range, v => scrollContainers.Where(c => c != null).ForEach(c => c.TimeRange = v));
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
[Test]
public void TestScrollLifetime()
public void TestConstantScrollLifetime()
{
AddStep("Set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
AddStep("set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
// scroll container time range must be less than the rate of spawning hitobjects
// otherwise the hitobjects will spawn already partly visible on screen and look wrong
AddStep("Set time range", () => scrollContainers.ForEach(c => c.TimeRange = spawn_interval / 2.0));
AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
}
[Test]
public void TestSequentialScrollLifetime()
{
AddStep("set sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
[Test]
public void TestOverlappingScrollLifetime()
{
AddStep("set overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
private void addHitObject(double time)
@ -122,28 +149,27 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
private void addControlPoint(double time)
private TestDrawableControlPoint createDrawablePoint(TestPlayfield playfield, double t)
{
scrollContainers.ForEach(c =>
var obj = new TestDrawableControlPoint(playfield.Direction, t);
setAnchor(obj, playfield);
return obj;
}
private void addControlPoints(IList<MultiplierControlPoint> controlPoints, double sequenceStartTime)
{
controlPoints.ForEach(point => point.StartTime += sequenceStartTime);
scrollContainers.ForEach(container =>
{
c.ControlPoints.Add(new MultiplierControlPoint(time) { DifficultyPoint = { SpeedMultiplier = 3 } });
c.ControlPoints.Add(new MultiplierControlPoint(time + 2000) { DifficultyPoint = { SpeedMultiplier = 2 } });
c.ControlPoints.Add(new MultiplierControlPoint(time + 3000) { DifficultyPoint = { SpeedMultiplier = 1 } });
container.ControlPoints.AddRange(controlPoints);
});
playfields.ForEach(p =>
foreach (var playfield in playfields)
{
TestDrawableControlPoint createDrawablePoint(double t)
{
var obj = new TestDrawableControlPoint(p.Direction, t);
setAnchor(obj, p);
return obj;
}
p.Add(createDrawablePoint(time));
p.Add(createDrawablePoint(time + 2000));
p.Add(createDrawablePoint(time + 3000));
});
foreach (var controlPoint in controlPoints)
playfield.Add(createDrawablePoint(playfield, controlPoint.StartTime));
}
}
private void setAnchor(DrawableHitObject obj, TestPlayfield playfield)
@ -236,7 +262,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AutoSizeAxes = Axes.Both;
AddInternal(new Box { Size = new Vector2(75) });
AddInternal(new Box
{
Size = new Vector2(75),
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)
});
}
}
}

View File

@ -5,7 +5,11 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
{
public class ConstantScrollAlgorithm : IScrollAlgorithm
{
public double GetDisplayStartTime(double time, double timeRange) => time - timeRange;
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
{
var adjustedTime = TimeAt(-offset, originTime, timeRange, scrollLength);
return adjustedTime - timeRange;
}
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
{

View File

@ -6,15 +6,33 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
public interface IScrollAlgorithm
{
/// <summary>
/// Given a point in time, computes the time at which it enters the time range.
/// Given a point in time associated with an object's origin
/// and the spatial distance between the edge and the origin of the object along the scrolling axis,
/// computes the time at which the object initially enters the time range.
/// </summary>
/// <remarks>
/// E.g. For a constant time range of 5000ms, the time at which t=7000ms enters the time range is 2000ms.
/// </remarks>
/// <param name="time">The point in time.</param>
/// <example>
/// Let's assume the following parameters:
/// <list type="bullet">
/// <item><paramref name="originTime"/> = 7000ms,</item>
/// <item><paramref name="offset"/> = 100px,</item>
/// <item><paramref name="timeRange"/> = 5000ms,</item>
/// <item><paramref name="scrollLength"/> = 1000px</item>
/// </list>
/// and a constant scrolling rate.
/// To arrive at the end of the scrolling container, the object's origin has to cover
/// <code>1000 + 100 = 1100px</code>
/// so that the edge starts at the end of the scrolling container.
/// One scroll length of 1000px covers 5000ms of time, so the time required to cover 1100px is equal to
/// <code>5000 * (1100 / 1000) = 5500ms,</code>
/// and therefore the object should start being visible at
/// <code>7000 - 5500 = 1500ms.</code>
/// </example>
/// <param name="originTime">The time point at which the object origin should enter the time range.</param>
/// <param name="offset">The spatial distance between the object's edge and its origin along the scrolling axis.</param>
/// <param name="timeRange">The amount of visible time.</param>
/// <returns>The time at which <paramref name="time"/> enters <paramref name="timeRange"/>.</returns>
double GetDisplayStartTime(double time, double timeRange);
/// <param name="scrollLength">The absolute spatial length through <paramref name="timeRange"/>.</param>
/// <returns>The time at which the object should enter the time range.</returns>
double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength);
/// <summary>
/// Computes the spatial length within a start and end time.

View File

@ -20,11 +20,12 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
searchPoint = new MultiplierControlPoint();
}
public double GetDisplayStartTime(double time, double timeRange)
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
{
var controlPoint = controlPointAt(originTime);
// The total amount of time that the hitobject will remain visible within the timeRange, which decreases as the speed multiplier increases
double visibleDuration = timeRange / controlPointAt(time).Multiplier;
return time - visibleDuration;
double visibleDuration = (scrollLength + offset) * timeRange / controlPoint.Multiplier / scrollLength;
return originTime - visibleDuration;
}
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)

View File

@ -20,7 +20,11 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
positionCache = new Dictionary<double, double>();
}
public double GetDisplayStartTime(double time, double timeRange) => time - timeRange - 1000;
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
{
double adjustedTime = TimeAt(-offset, originTime, timeRange, scrollLength);
return adjustedTime - timeRange - 1000;
}
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
{

View File

@ -133,8 +133,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
break;
}
var adjustedStartTime = scrollingInfo.Algorithm.TimeAt(-originAdjustment, hitObject.HitObject.StartTime, timeRange.Value, scrollLength);
return scrollingInfo.Algorithm.GetDisplayStartTime(adjustedStartTime, timeRange.Value);
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
}
// Cant use AddOnce() since the delegate is re-constructed every invocation

View File

@ -86,8 +86,8 @@ namespace osu.Game.Tests.Visual
}
}
public double GetDisplayStartTime(double time, double timeRange)
=> implementation.GetDisplayStartTime(time, timeRange);
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
=> implementation.GetDisplayStartTime(originTime, offset, timeRange, scrollLength);
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
=> implementation.GetLength(startTime, endTime, timeRange, scrollLength);