Limit distance snap between two adjacent hit objects (#6740)

Limit distance snap between two adjacent hit objects
This commit is contained in:
Dean Herbert
2019-11-06 17:18:00 +09:00
committed by GitHub
6 changed files with 122 additions and 43 deletions

View File

@ -42,11 +42,19 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached(typeof(IDistanceSnapProvider))] [Cached(typeof(IDistanceSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider(); private readonly SnapProvider snapProvider = new SnapProvider();
private readonly TestOsuDistanceSnapGrid grid; private TestOsuDistanceSnapGrid grid;
public TestSceneOsuDistanceSnapGrid() public TestSceneOsuDistanceSnapGrid()
{ {
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap()); editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
}
[SetUp]
public void Setup() => Schedule(() =>
{
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
Children = new Drawable[] Children = new Drawable[]
{ {
@ -58,14 +66,6 @@ namespace osu.Game.Rulesets.Osu.Tests
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }), grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position } new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
}; };
}
[SetUp]
public void Setup() => Schedule(() =>
{
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
}); });
[TestCase(1)] [TestCase(1)]
@ -102,6 +102,27 @@ namespace osu.Game.Rulesets.Osu.Tests
assertSnappedDistance((float)beat_length * 2); assertSnappedDistance((float)beat_length * 2);
} }
[Test]
public void TestLimitedDistance()
{
AddStep("create limited grid", () =>
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
};
});
AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 3f)));
assertSnappedDistance((float)beat_length * 2);
}
private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () => private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
{ {
Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position; Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position;
@ -152,8 +173,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public new float DistanceSpacing => base.DistanceSpacing; public new float DistanceSpacing => base.DistanceSpacing;
public TestOsuDistanceSnapGrid(OsuHitObject hitObject) public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
: base(hitObject) : base(hitObject, nextHitObject)
{ {
} }
} }
@ -164,9 +185,9 @@ namespace osu.Game.Rulesets.Osu.Tests
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
public float DurationToDistance(double referenceTime, double duration) => 0; public float DurationToDistance(double referenceTime, double duration) => (float)duration;
public double DistanceToDuration(double referenceTime, float distance) => 0; public double DistanceToDuration(double referenceTime, float distance) => distance;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;

View File

@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{ {
public OsuDistanceSnapGrid(OsuHitObject hitObject) public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
: base(hitObject, hitObject.StackedEndPosition) : base(hitObject, nextHitObject, hitObject.StackedEndPosition)
{ {
Masking = true; Masking = true;
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -60,25 +61,40 @@ namespace osu.Game.Rulesets.Osu.Edit
var objects = selectedHitObjects.ToList(); var objects = selectedHitObjects.ToList();
if (objects.Count == 0) if (objects.Count == 0)
return createGrid(h => h.StartTime <= EditorClock.CurrentTime);
double minTime = objects.Min(h => h.StartTime);
return createGrid(h => h.StartTime < minTime, objects.Count + 1);
}
/// <summary>
/// Creates a grid from the last <see cref="HitObject"/> matching a predicate to a target <see cref="HitObject"/>.
/// </summary>
/// <param name="sourceSelector">A predicate that matches <see cref="HitObject"/>s where the grid can start from.
/// Only the last <see cref="HitObject"/> matching the predicate is used.</param>
/// <param name="targetOffset">An offset from the <see cref="HitObject"/> selected via <paramref name="sourceSelector"/> at which the grid should stop.</param>
/// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns>
private OsuDistanceSnapGrid createGrid(Func<HitObject, bool> sourceSelector, int targetOffset = 1)
{
if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
int sourceIndex = -1;
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
{ {
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime <= EditorClock.CurrentTime); if (!sourceSelector(EditorBeatmap.HitObjects[i]))
break;
if (lastObject == null) sourceIndex = i;
return null;
return new OsuDistanceSnapGrid(lastObject);
} }
else
{
double minTime = objects.Min(h => h.StartTime);
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime < minTime); if (sourceIndex == -1)
return null;
if (lastObject == null) OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
return null; OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null;
return new OsuDistanceSnapGrid(lastObject); return new OsuDistanceSnapGrid(sourceObject, targetObject);
}
} }
} }
} }

View File

@ -32,7 +32,11 @@ namespace osu.Game.Tests.Visual.Editor
{ {
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap()); editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
}
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Box
@ -42,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editor
}, },
new TestDistanceSnapGrid(new HitObject(), grid_position) new TestDistanceSnapGrid(new HitObject(), grid_position)
}; };
} });
[TestCase(1)] [TestCase(1)]
[TestCase(2)] [TestCase(2)]
@ -57,12 +61,29 @@ namespace osu.Game.Tests.Visual.Editor
AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor); AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor);
} }
[Test]
public void TestLimitedDistance()
{
AddStep("create limited grid", () =>
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
new TestDistanceSnapGrid(new HitObject(), grid_position, new HitObject { StartTime = 100 })
};
});
}
private class TestDistanceSnapGrid : DistanceSnapGrid private class TestDistanceSnapGrid : DistanceSnapGrid
{ {
public new float DistanceSpacing => base.DistanceSpacing; public new float DistanceSpacing => base.DistanceSpacing;
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition) public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
: base(hitObject, centrePosition) : base(hitObject, nextHitObject, centrePosition)
{ {
} }
@ -77,7 +98,7 @@ namespace osu.Game.Tests.Visual.Editor
int beatIndex = 0; int beatIndex = 0;
for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth; s += DistanceSpacing, beatIndex++) for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{ {
AddInternal(new Circle AddInternal(new Circle
{ {
@ -90,7 +111,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0; beatIndex = 0;
for (float s = centrePosition.X - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++) for (float s = centrePosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{ {
AddInternal(new Circle AddInternal(new Circle
{ {
@ -103,7 +124,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0; beatIndex = 0;
for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight; s += DistanceSpacing, beatIndex++) for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{ {
AddInternal(new Circle AddInternal(new Circle
{ {
@ -116,7 +137,7 @@ namespace osu.Game.Tests.Visual.Editor
beatIndex = 0; beatIndex = 0;
for (float s = centrePosition.Y - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++) for (float s = centrePosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{ {
AddInternal(new Circle AddInternal(new Circle
{ {
@ -138,9 +159,9 @@ namespace osu.Game.Tests.Visual.Editor
public float GetBeatSnapDistanceAt(double referenceTime) => 10; public float GetBeatSnapDistanceAt(double referenceTime) => 10;
public float DurationToDistance(double referenceTime, double duration) => 0; public float DurationToDistance(double referenceTime, double duration) => (float)duration;
public double DistanceToDuration(double referenceTime, float distance) => 0; public double DistanceToDuration(double referenceTime, float distance) => distance;
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;

View File

@ -12,8 +12,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
public abstract class CircularDistanceSnapGrid : DistanceSnapGrid public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
{ {
protected CircularDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition) protected CircularDistanceSnapGrid(HitObject hitObject, HitObject nextHitObject, Vector2 centrePosition)
: base(hitObject, centrePosition) : base(hitObject, nextHitObject, centrePosition)
{ {
} }
@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
float dx = Math.Max(centrePosition.X, DrawWidth - centrePosition.X); float dx = Math.Max(centrePosition.X, DrawWidth - centrePosition.X);
float dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y); float dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y);
float maxDistance = new Vector2(dx, dy).Length; float maxDistance = new Vector2(dx, dy).Length;
int requiredCircles = (int)(maxDistance / DistanceSpacing); int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceSpacing));
for (int i = 0; i < requiredCircles; i++) for (int i = 0; i < requiredCircles; i++)
{ {
@ -65,15 +65,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position) public override (Vector2 position, double time) GetSnappedPosition(Vector2 position)
{ {
Vector2 direction = position - CentrePosition; if (MaxIntervals == 0)
return (CentrePosition, StartTime);
Vector2 direction = position - CentrePosition;
if (direction == Vector2.Zero) if (direction == Vector2.Zero)
direction = new Vector2(0.001f, 0.001f); direction = new Vector2(0.001f, 0.001f);
float distance = direction.Length; float distance = direction.Length;
float radius = DistanceSpacing; float radius = DistanceSpacing;
int radialCount = Math.Max(1, (int)Math.Round(distance / radius)); int radialCount = MathHelper.Clamp((int)Math.Round(distance / radius), 1, MaxIntervals);
Vector2 normalisedDirection = direction * new Vector2(1f / distance); Vector2 normalisedDirection = direction * new Vector2(1f / distance);
Vector2 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius; Vector2 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -29,6 +30,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
protected double StartTime { get; private set; } protected double StartTime { get; private set; }
/// <summary>
/// The maximum number of distance snapping intervals allowed.
/// </summary>
protected int MaxIntervals { get; private set; }
/// <summary> /// <summary>
/// The position which the grid is centred on. /// The position which the grid is centred on.
/// The first beat snapping tick is located at <see cref="CentrePosition"/> + <see cref="DistanceSpacing"/> in the desired direction. /// The first beat snapping tick is located at <see cref="CentrePosition"/> + <see cref="DistanceSpacing"/> in the desired direction.
@ -49,12 +55,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly Cached gridCache = new Cached(); private readonly Cached gridCache = new Cached();
private readonly HitObject hitObject; private readonly HitObject hitObject;
private readonly HitObject nextHitObject;
protected DistanceSnapGrid(HitObject hitObject, Vector2 centrePosition) protected DistanceSnapGrid(HitObject hitObject, [CanBeNull] HitObject nextHitObject, Vector2 centrePosition)
{ {
this.hitObject = hitObject; this.hitObject = hitObject;
this.nextHitObject = nextHitObject;
CentrePosition = centrePosition; CentrePosition = centrePosition;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
@ -74,6 +83,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing() private void updateSpacing()
{ {
DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime); DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime);
if (nextHitObject == null)
MaxIntervals = int.MaxValue;
else
{
// +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors
double maxDuration = nextHitObject.StartTime - StartTime + 1;
MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing));
}
gridCache.Invalidate(); gridCache.Invalidate();
} }