diff --git a/osu.Game.Rulesets.Tau.Tests/NonVisual/PolarSliderPathTests.cs b/osu.Game.Rulesets.Tau.Tests/NonVisual/PolarSliderPathTests.cs index dcec2bb3..ed95fbb1 100644 --- a/osu.Game.Rulesets.Tau.Tests/NonVisual/PolarSliderPathTests.cs +++ b/osu.Game.Rulesets.Tau.Tests/NonVisual/PolarSliderPathTests.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using osu.Game.Rulesets.Tau.Objects; +using System.Linq; namespace osu.Game.Rulesets.Tau.Tests.NonVisual { @@ -84,6 +85,35 @@ public void TestCalculatedDistance() Assert.AreEqual(0, path.Version.Value); Assert.AreEqual(40, path.CalculatedDistance); Assert.AreEqual(0, path.Version.Value); + Assert.AreEqual(20, path.CalculateLazyDistance(10)); + Assert.AreEqual(0, path.CalculateLazyDistance(20)); + } + + [Test] + public void TestSegments() + { + var path = new PolarSliderPath(new SliderNode[] + { + new(0, 50), + new(200, 70), + new(400, 50), + }); + + var segments = path.SegmentsBetween(100, 300).ToArray(); + Assert.AreEqual(2, segments.Length); + Assert.AreEqual(60, segments[0].From.Angle); + Assert.AreEqual(60, segments[1].To.Angle); + + segments = path.SegmentsBetween(50, 100).ToArray(); + Assert.AreEqual(1, segments.Length); + Assert.AreEqual(55, segments[0].From.Angle); + Assert.AreEqual(60, segments[0].To.Angle); + + segments = path.SegmentsBetween(0, 400).ToArray(); + Assert.AreEqual(2, segments.Length); + Assert.AreEqual(50, segments[0].From.Angle); + Assert.AreEqual(70, segments[0].To.Angle); + Assert.AreEqual(50, segments[1].To.Angle); } } } diff --git a/osu.Game.Rulesets.Tau/Beatmaps/TauBeatmapConverter.cs b/osu.Game.Rulesets.Tau/Beatmaps/TauBeatmapConverter.cs index d466ee87..ba30833d 100644 --- a/osu.Game.Rulesets.Tau/Beatmaps/TauBeatmapConverter.cs +++ b/osu.Game.Rulesets.Tau/Beatmaps/TauBeatmapConverter.cs @@ -137,7 +137,7 @@ private TauHitObject convertToSlider(HitObject original, IHasCombo comboData, IH NodeSamples = data.NodeSamples, RepeatCount = data.RepeatCount, Angle = firstAngle, - Path = new PolarSliderPath(nodes.ToArray()), + Path = new PolarSliderPath(nodes), NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, }; diff --git a/osu.Game.Rulesets.Tau/Objects/Drawables/DrawableSlider.Calculations.cs b/osu.Game.Rulesets.Tau/Objects/Drawables/DrawableSlider.Calculations.cs index 4b11c4e0..be7a7e9d 100644 --- a/osu.Game.Rulesets.Tau/Objects/Drawables/DrawableSlider.Calculations.cs +++ b/osu.Game.Rulesets.Tau/Objects/Drawables/DrawableSlider.Calculations.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using osu.Game.Rulesets.Tau.UI; using osuTK; namespace osu.Game.Rulesets.Tau.Objects.Drawables @@ -13,74 +14,46 @@ private void updatePath() if (nodes.Count == 0) return; - var time = Time.Current - HitObject.StartTime + HitObject.TimePreempt; - var startTime = Math.Max(time - HitObject.TimePreempt - FadeTime, nodes[0].Time); - var midTime = Math.Max(time - HitObject.TimePreempt, nodes[0].Time); - var endTime = Math.Min(time, nodes[^1].Time); + double time = Time.Current - HitObject.StartTime + HitObject.TimePreempt; + double startTime = Math.Max(time - HitObject.TimePreempt - FadeTime, nodes[0].Time); + double midTime = Math.Max(time - HitObject.TimePreempt, nodes[0].Time); + double endTime = Math.Min(time, nodes[^1].Time); if (time < startTime) return; - int nodeIndex = 0; bool capAdded = false; + var polarPath = HitObject.Path; - generatePathSegmnt(ref nodeIndex, ref capAdded, time, startTime, midTime); - nodeIndex--; - var pos = path.Vertices.Any() ? path.Vertices[^1].Xy : Vector2.Zero; - generatePathSegmnt(ref nodeIndex, ref capAdded, time, midTime, endTime); - - path.Position = pos; - path.OriginPosition = path.PositionInBoundingBox(pos); - } - - private void generatePathSegmnt(ref int nodeIndex, ref bool capAdded, double time, double startTime, double endTime) - { - var nodes = HitObject.Path.Nodes; - if (nodeIndex >= nodes.Count) - return; - - while (nodeIndex + 1 < nodes.Count && nodes[nodeIndex + 1].Time <= startTime) - nodeIndex++; - - const double delta_time = 20; - const double max_angle_per_ms = 5; + float radius = TauPlayfield.BaseSize.X / 2; float distanceAt(double t) => inversed - ? (float)(2 * PathDistance - (time - t) / HitObject.TimePreempt * PathDistance) - : (float)((time - t) / HitObject.TimePreempt * PathDistance); + ? (float)(2 * radius - (time - t) / HitObject.TimePreempt * radius) + : (float)((time - t) / HitObject.TimePreempt * radius); void addVertex(double t, double angle) { var p = Extensions.FromPolarCoordinates(distanceAt(t), (float)angle); - var index = (int)(t / trackingCheckpointInterval); + int index = (int)(t / trackingCheckpointInterval); + path.AddVertex(new Vector3(p.X, p.Y, trackingCheckpoints.ValueAtOrLastOr(index, true) ? 1 : 0)); } - do + foreach (var segment in polarPath.SegmentsBetween((float)startTime, (float)endTime)) { - var prevNode = nodes[nodeIndex]; - var nextNode = nodeIndex + 1 < nodes.Count ? nodes[nodeIndex + 1] : prevNode; - - var from = Math.Max(startTime, prevNode.Time); - var to = Math.Min(endTime, nextNode.Time); - var duration = nextNode.Time - prevNode.Time; - - var deltaAngle = Extensions.GetDeltaAngle(nextNode.Angle, prevNode.Angle); - var anglePerMs = duration != 0 ? deltaAngle / duration : 0; - var timeStep = Math.Min(delta_time, Math.Abs(max_angle_per_ms / anglePerMs)); - - if (!capAdded) - addVertex(from, prevNode.Angle + anglePerMs * (from - prevNode.Time)); - for (var t = from + timeStep; t < to; t += timeStep) - addVertex(t, prevNode.Angle + anglePerMs * (t - prevNode.Time)); - if (duration != 0) - addVertex(to, prevNode.Angle + anglePerMs * (to - prevNode.Time)); - else - addVertex(to, nextNode.Angle); + foreach (var node in segment.Split(excludeFirst: capAdded)) + { + addVertex(node.Time, node.Angle); + } capAdded = true; - nodeIndex++; - } while (nodeIndex < nodes.Count && nodes[nodeIndex].Time < endTime); + } + + var midNode = polarPath.NodeAt((float)midTime); + var pos = Extensions.FromPolarCoordinates(distanceAt(midNode.Time), midNode.Angle); + + path.Position = pos; + path.OriginPosition = path.PositionInBoundingBox(pos); } private bool checkIfTracking() diff --git a/osu.Game.Rulesets.Tau/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Tau/Objects/Drawables/DrawableSliderRepeat.cs index c3e97950..a0b7a55f 100644 --- a/osu.Game.Rulesets.Tau/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Tau/Objects/Drawables/DrawableSliderRepeat.cs @@ -31,21 +31,21 @@ protected override void Update() AlwaysPresent = true; } - public Drawable InnerDrawableBox; + public Drawable InnerDrawableBox = new Container + { + RelativePositionAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + AlwaysPresent = true, + Child = new BeatPiece() + }; protected override void LoadComplete() { base.LoadComplete(); - AddInternal(InnerDrawableBox = new Container - { - RelativePositionAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - AlwaysPresent = true, - Size = DrawableBox.Size, - Child = new BeatPiece() - }); + InnerDrawableBox.Size = DrawableBox.Size; + AddInternal(InnerDrawableBox); DrawableBox.Size = Vector2.Multiply(DrawableBox.Size, 15f / 16f); DrawableBox.Rotation = 45; diff --git a/osu.Game.Rulesets.Tau/Objects/PolarSliderPath.cs b/osu.Game.Rulesets.Tau/Objects/PolarSliderPath.cs index d584d43b..62a630c4 100644 --- a/osu.Game.Rulesets.Tau/Objects/PolarSliderPath.cs +++ b/osu.Game.Rulesets.Tau/Objects/PolarSliderPath.cs @@ -1,7 +1,7 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -14,24 +14,28 @@ public class PolarSliderPath public IBindable Version => version; private readonly Bindable version = new(); - public readonly Bindable ExpectedDistance = new(); - public double Duration => Nodes.Max(n => n.Time); - public SliderNode EndNode => Nodes.LastOrDefault(); + public double Duration => EndNode.Time - StartNode.Time; + public SliderNode EndNode => Nodes.Any() ? Nodes[^1] : default; + public SliderNode StartNode => Nodes.Any() ? Nodes[0] : default; - public readonly List Nodes = new(); + public readonly BindableList Nodes = new(); private readonly Cached pathCache = new(); - private double calculatedLength; - - public PolarSliderPath(SliderNode[] nodes, double? expectedDistance = null) + public PolarSliderPath(IEnumerable nodes) { Nodes.AddRange(nodes); - ExpectedDistance.Value = expectedDistance; + Nodes.BindCollectionChanged((_, _) => + { + // TODO ensure the list is sorted + invalidate(); + }); } + private double calculatedLength; + /// - /// The distance of the path prior to lengthening/shortening to account for . + /// The distance of the path (in degrees) /// public double CalculatedDistance { @@ -42,67 +46,64 @@ public double CalculatedDistance } } + // we are expecting the inputs to be similar, and as such caching the current seeker position speeds the process up + private int nodeIndex; + /// - /// Gets a new list of between the start and end time. + /// Seeks the such that the node at is before or at + /// and the next node is after unless there are no more nodes to seek in a given direction. /// - /// The start time to collect nodes. - /// The end time to collect nodes. - public Span NodesBetween(float start, float end) + private void seekTo(float time) { - int? index = null; - int? length = null; - - for (var i = 0; i < Nodes.Count; i++) - { - var node = Nodes[i]; - if (node.Time < start) - continue; - - if (index == null) - { - index = i; - length = 0; - } - - if (node.Time > end) - break; - - length = (i + 1) - index; - } + while (nodeIndex > 0 && Nodes[nodeIndex - 1].Time > time) + nodeIndex--; + while (nodeIndex + 1 < Nodes.Count && Nodes[nodeIndex + 1].Time <= time) + nodeIndex++; + } - if (index is null or 0) - return Span.Empty; + public NodesEnumerable NodesBetween(float start, float end) + { + seekTo(start); + return new(nodeIndex + 1, end, this); + } - return CollectionsMarshal.AsSpan(Nodes).Slice((int)index, (int)length); + public SegmentsEnumerable SegmentsBetween(float start, float end) + { + seekTo(start); + return new(Math.Max(nodeIndex - 1, 0), start, end, this); } /// - /// Interpolates the list of and returns the current angle at the set time. + /// Returns an interpolated node at a given time /// - /// The time to get the angle at. - public float AngleAt(float time) + public SliderNode NodeAt(float time) { - var last = Nodes.LastOrDefault(); + if (!Nodes.Any()) + return default; - if (time <= 0) - return Nodes.First().Angle; + if (time <= Nodes[0].Time) + return Nodes[0]; - if (time >= last.Time) - return last.Angle; + if (time >= Nodes[^1].Time) + return Nodes[^1]; - var closest = Nodes.OrderBy(n => Math.Abs(time - n.Time)).ToArray(); - var start = closest[0]; - var end = closest[1]; + seekTo(time); + var from = Nodes[nodeIndex]; + var to = Nodes[nodeIndex + 1]; + float delta = Extensions.GetDeltaAngle(to.Angle, from.Angle); - var index = Nodes.BinarySearch(start); - - if (index == Nodes.Count) - return start.Angle; + // no need to check for div by 0 because the seek skips over 0-duration nodes + return new(time, from.Angle + delta * (time - from.Time) / (to.Time - from.Time)); + } - var deltaAngle = Extensions.GetDeltaAngle(end.Angle, start.Angle); - var duration = end.Time - start.Time; + public float AngleAt(float time) + => NodeAt(time).Angle; - return start.Angle + deltaAngle * (time - start.Time) / duration; + private void invalidate() + { + pathCache.Invalidate(); + version.Value++; + nodeIndex = 0; } private void ensureValid() @@ -122,15 +123,35 @@ private void calculateLength() if (Nodes.Count <= 0) return; - (float angle, float sum) result = (angle: Nodes[0].Angle, sum: 0f); + float lastAngle = Nodes[0].Angle; foreach (var node in Nodes) { - result.sum += Math.Abs(Extensions.GetDeltaAngle(result.angle, node.Angle)); - result.angle = node.Angle; + calculatedLength += Math.Abs(Extensions.GetDeltaAngle(node.Angle, lastAngle)); + lastAngle = node.Angle; } + } + + public float CalculateLazyDistance(float halfTolerance) + { + if (Nodes.Count <= 0) + return 0; + + float length = 0f; + float lastAngle = Nodes[0].Angle; + + foreach (var node in Nodes) + { + float delta = Extensions.GetDeltaAngle(node.Angle, lastAngle); - calculatedLength = result.sum; + if (MathF.Abs(delta) > halfTolerance) + { + lastAngle += delta - (delta > 0 ? halfTolerance : -halfTolerance); + length += MathF.Abs(delta); + } + } + + return length; } } @@ -150,4 +171,232 @@ public SliderNode(float time, float angle) public override string ToString() => $"T: {Time} | A: {Angle}"; } + + public readonly struct SliderSegment + { + public SliderNode From { get; } + public SliderNode To { get; } + public float DeltaAngle => Extensions.GetDeltaAngle(To.Angle, From.Angle); + public float Duration => To.Time - From.Time; + + public SliderSegment(SliderNode from, SliderNode to) + { + From = from; + To = to; + } + + public SegmentNodesEnumerable Split(float timeStep = 20, float maxAnglePerMs = 5, bool excludeFirst = false, bool excludeLast = false) + { + float duration = Duration; + float delta = DeltaAngle; + int steps; + + if (duration == 0) + { + steps = (int)MathF.Ceiling(MathF.Abs(delta) / maxAnglePerMs); + } + else + { + float anglePerMs = delta / duration; + timeStep = Math.Min(timeStep, Math.Abs(maxAnglePerMs / anglePerMs)); + steps = (int)MathF.Ceiling(duration / timeStep); + } + + steps += 2; + return new( + excludeFirst ? new(From.Time + duration / steps, From.Angle + delta / steps) : From, + excludeLast ? new(To.Time - duration / steps, To.Angle - delta / steps) : To, + steps - (excludeFirst ? 1 : 0) - (excludeLast ? 1 : 0) + ); + } + + public override string ToString() => $"({From}) -> ({To})"; + } + + public readonly struct NodesEnumerable : IEnumerable + { + private readonly int index; + private readonly float endTime; + private readonly PolarSliderPath path; + + public NodesEnumerable(int index, float endTime, PolarSliderPath path) + { + this.index = index; + this.endTime = endTime; + this.path = path; + } + + public NodesEnumerator GetEnumerator() => new(index, endTime, path); + + IEnumerator IEnumerable.GetEnumerator() + { + foreach (var i in this) + yield return i; + } + + IEnumerator IEnumerable.GetEnumerator() + => ((IEnumerable)this).GetEnumerator(); + + public struct NodesEnumerator + { + private int index; + private readonly float endTime; + private readonly PolarSliderPath path; + + public NodesEnumerator(int index, float endTime, PolarSliderPath path) + { + this.index = index - 1; + this.endTime = endTime; + this.path = path; + } + + public bool MoveNext() + { + if (index + 1 < path.Nodes.Count && path.Nodes[index + 1].Time < endTime) + { + index++; + return true; + } + + return false; + } + + public SliderNode Current => path.Nodes[index]; + } + } + + public readonly struct SegmentsEnumerable : IEnumerable + { + private readonly int index; + private readonly float startTime; + private readonly float endTime; + private readonly PolarSliderPath path; + + public SegmentsEnumerable(int index, float startTime, float endTime, PolarSliderPath path) + { + this.index = index; + this.startTime = startTime; + this.endTime = endTime; + this.path = path; + } + + IEnumerator IEnumerable.GetEnumerator() + { + foreach (var i in this) + yield return i; + } + + IEnumerator IEnumerable.GetEnumerator() + => ((IEnumerable)this).GetEnumerator(); + + public SegmentsEnumerator GetEnumerator() => new(index, startTime, endTime, path); + + public struct SegmentsEnumerator + { + private int index; + private readonly float startTime; + private readonly float endTime; + private readonly PolarSliderPath path; + + public SegmentsEnumerator(int index, float startTime, float endTime, PolarSliderPath path) + { + this.index = index - 1; + this.startTime = startTime; + this.endTime = endTime; + this.path = path; + } + + public bool MoveNext() + { + if (index + 2 < path.Nodes.Count && path.Nodes[index + 1].Time <= endTime) + { + index++; + return true; + } + + return false; + } + + public SliderSegment Current + { + get + { + var from = path.Nodes[index]; + var to = path.Nodes[index + 1]; + float deltaAngle = Extensions.GetDeltaAngle(to.Angle, from.Angle); + float duration = to.Time - from.Time; + + if (to.Time > endTime && duration != 0) + { + to = new(endTime, from.Angle + deltaAngle * (endTime - from.Time) / duration); + } + + if (from.Time < startTime && duration != 0) + { + from = new(startTime, from.Angle + deltaAngle * (startTime - from.Time) / duration); + } + + return new(from, to); + } + } + } + } + + public readonly struct SegmentNodesEnumerable : IEnumerable + { + private readonly SliderNode from; + private readonly SliderNode to; + private readonly int steps; + + public SegmentNodesEnumerable(SliderNode from, SliderNode to, int steps) + { + this.from = from; + this.to = to; + this.steps = steps; + } + + public SegmentNodesEnumerator GetEnumerator() => new(from, to, steps); + + IEnumerator IEnumerable.GetEnumerator() + { + foreach (var i in this) + yield return i; + } + + IEnumerator IEnumerable.GetEnumerator() + => ((IEnumerable)this).GetEnumerator(); + + public struct SegmentNodesEnumerator + { + private int current = -1; + private readonly SliderNode from; + private readonly int steps; + private readonly float span; + private readonly float timeSpan; + + public SegmentNodesEnumerator(SliderNode from, SliderNode to, int steps) + { + this.from = from; + this.steps = steps; + + if (steps <= 1) + { + span = 0; + timeSpan = 0; + } + else + { + span = Extensions.GetDeltaAngle(to.Angle, from.Angle) / (steps - 1); + timeSpan = (to.Time - from.Time) / (steps - 1); + } + } + + public bool MoveNext() + { + return ++current < steps; + } + + public SliderNode Current => new(from.Time + timeSpan * current, from.Angle + span * current); + } + } } diff --git a/osu.Game.Rulesets.Tau/Objects/Slider.cs b/osu.Game.Rulesets.Tau/Objects/Slider.cs index 49081713..4a7951d0 100644 --- a/osu.Game.Rulesets.Tau/Objects/Slider.cs +++ b/osu.Game.Rulesets.Tau/Objects/Slider.cs @@ -88,7 +88,7 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok foreach (var e in sliderEvents) { - var currentAngle = Path.AngleAt((float)(e.Time - StartTime)); + float currentAngle = Path.AngleAt((float)(e.Time - StartTime)); switch (e.Type) {