mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 16:27:26 +08:00
Limit distance snap between two adjacent hit objects (#6740)
Limit distance snap between two adjacent hit objects
This commit is contained in:
commit
a2963b66f6
@ -42,11 +42,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Cached(typeof(IDistanceSnapProvider))]
|
||||
private readonly SnapProvider snapProvider = new SnapProvider();
|
||||
|
||||
private readonly TestOsuDistanceSnapGrid grid;
|
||||
private TestOsuDistanceSnapGrid grid;
|
||||
|
||||
public TestSceneOsuDistanceSnapGrid()
|
||||
{
|
||||
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[]
|
||||
{
|
||||
@ -58,14 +66,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_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)]
|
||||
@ -102,6 +102,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
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}", () =>
|
||||
{
|
||||
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 TestOsuDistanceSnapGrid(OsuHitObject hitObject)
|
||||
: base(hitObject)
|
||||
public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
|
||||
: base(hitObject, nextHitObject)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -164,9 +185,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
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;
|
||||
|
||||
|
@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
|
||||
{
|
||||
public OsuDistanceSnapGrid(OsuHitObject hitObject)
|
||||
: base(hitObject, hitObject.StackedEndPosition)
|
||||
public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
|
||||
: base(hitObject, nextHitObject, hitObject.StackedEndPosition)
|
||||
{
|
||||
Masking = true;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
@ -60,25 +61,40 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
var objects = selectedHitObjects.ToList();
|
||||
|
||||
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)
|
||||
return null;
|
||||
|
||||
return new OsuDistanceSnapGrid(lastObject);
|
||||
sourceIndex = i;
|
||||
}
|
||||
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)
|
||||
return null;
|
||||
OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
|
||||
OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null;
|
||||
|
||||
return new OsuDistanceSnapGrid(lastObject);
|
||||
}
|
||||
return new OsuDistanceSnapGrid(sourceObject, targetObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,11 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
{
|
||||
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
|
||||
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
@ -42,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
},
|
||||
new TestDistanceSnapGrid(new HitObject(), grid_position)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
@ -57,12 +61,29 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
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
|
||||
{
|
||||
public new float DistanceSpacing => base.DistanceSpacing;
|
||||
|
||||
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
|
||||
: base(hitObject, centrePosition)
|
||||
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
|
||||
: base(hitObject, nextHitObject, centrePosition)
|
||||
{
|
||||
}
|
||||
|
||||
@ -77,7 +98,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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
|
||||
{
|
||||
@ -90,7 +111,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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
|
||||
{
|
||||
@ -103,7 +124,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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
|
||||
{
|
||||
@ -116,7 +137,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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
|
||||
{
|
||||
@ -138,9 +159,9 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
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;
|
||||
|
||||
|
@ -12,8 +12,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
|
||||
{
|
||||
protected CircularDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
|
||||
: base(hitObject, centrePosition)
|
||||
protected CircularDistanceSnapGrid(HitObject hitObject, HitObject nextHitObject, Vector2 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 dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y);
|
||||
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++)
|
||||
{
|
||||
@ -65,15 +65,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
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)
|
||||
direction = new Vector2(0.001f, 0.001f);
|
||||
|
||||
float distance = direction.Length;
|
||||
|
||||
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 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius;
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
@ -29,6 +30,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
protected double StartTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of distance snapping intervals allowed.
|
||||
/// </summary>
|
||||
protected int MaxIntervals { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
@ -49,12 +55,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private readonly Cached gridCache = new Cached();
|
||||
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.nextHitObject = nextHitObject;
|
||||
|
||||
CentrePosition = centrePosition;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
@ -74,6 +83,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private void updateSpacing()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user