From 4c61a13e713628518518985e109534ebee9ed2df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Nov 2016 21:29:35 +0900 Subject: [PATCH] Make slider parsing kind of exist. --- .../Tests/TestCasePlayer.cs | 11 +- .../Objects/Drawables/DrawableHitCircle.cs | 3 +- .../Objects/Drawables/DrawableSlider.cs | 37 +++- .../Objects/OsuHitObjectParser.cs | 64 +++++- osu.Game.Mode.Osu/Objects/Slider.cs | 192 +++++++++++++++++- osu.Game.Mode.Osu/UI/OsuHitRenderer.cs | 9 +- 6 files changed, 309 insertions(+), 7 deletions(-) diff --git a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs index de285aa6bb..5a89d4bc8d 100644 --- a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs +++ b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs @@ -10,9 +10,11 @@ using OpenTK; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.Objects; using osu.Game.Screens.Play; +using OpenTK.Graphics; namespace osu.Desktop.VisualTests.Tests { @@ -41,7 +43,8 @@ public override void Reset() objects.Add(new HitCircle() { StartTime = time, - Position = new Vector2(RNG.Next(0, 512), RNG.Next(0, 384)), + Position = new Vector2(i % 4 == 0 || i % 4 == 2 ? 0 : 512, + i % 4 < 2 ? 0 : 384), NewCombo = i % 4 == 0 }); @@ -57,6 +60,12 @@ public override void Reset() decoder.Process(b); + Add(new Box + { + RelativeSizeAxes = Framework.Graphics.Axes.Both, + Colour = Color4.Gray, + }); + Add(new Player { Beatmap = new WorkingBeatmap(b) diff --git a/osu.Game.Mode.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Mode.Osu/Objects/Drawables/DrawableHitCircle.cs index c06a96aaab..9d6d918344 100644 --- a/osu.Game.Mode.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Mode.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -30,8 +30,7 @@ public DrawableHitCircle(HitCircle h) : base(h) this.h = h; Origin = Anchor.Centre; - RelativePositionAxes = Axes.Both; - Position = new Vector2(h.Position.X / 512, h.Position.Y / 384); + Position = h.Position; Children = new Drawable[] { diff --git a/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs index 0e75aacc84..274b5d4ef8 100644 --- a/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs @@ -1,4 +1,9 @@ -using osu.Game.Modes.Objects.Drawables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Modes.Objects.Drawables; +using osu.Game.Modes.Osu.Objects.Drawables.Pieces; +using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Modes.Osu.Objects.Drawables { @@ -6,12 +11,42 @@ class DrawableSlider : DrawableHitObject { public DrawableSlider(Slider h) : base(h) { + Origin = Anchor.Centre; + RelativePositionAxes = Axes.Both; + Position = new Vector2(h.Position.X / 512, h.Position.Y / 384); + for (float i = 0; i <= 1; i += 0.1f) + { + Add(new CirclePiece + { + Colour = h.Colour, + Hit = Hit, + Position = h.Curve.PositionAt(i) - h.Position //non-relative? + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + //force application of the state that was set before we loaded. + UpdateState(State); } protected override void UpdateState(ArmedState state) { + if (!IsLoaded) return; + Flush(true); //move to DrawableHitObject + + Alpha = 0; + + Delay(HitObject.StartTime - 200 - Time.Current, true); + + FadeIn(200); + Delay(200 + HitObject.Duration); + FadeOut(200); } } } diff --git a/osu.Game.Mode.Osu/Objects/OsuHitObjectParser.cs b/osu.Game.Mode.Osu/Objects/OsuHitObjectParser.cs index b6865c0651..966b8b2f64 100644 --- a/osu.Game.Mode.Osu/Objects/OsuHitObjectParser.cs +++ b/osu.Game.Mode.Osu/Objects/OsuHitObjectParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -25,7 +26,68 @@ public override HitObject Parse(string text) result = new HitCircle(); break; case OsuBaseHit.HitObjectType.Slider: - result = new Slider(); + Slider s = new Slider(); + + CurveTypes curveType = CurveTypes.Catmull; + int repeatCount = 0; + double length = 0; + List points = new List(); + + points.Add(new Vector2(int.Parse(split[0]), int.Parse(split[1]))); + + string[] pointsplit = split[5].Split('|'); + for (int i = 0; i < pointsplit.Length; i++) + { + if (pointsplit[i].Length == 1) + { + switch (pointsplit[i]) + { + case @"C": + curveType = CurveTypes.Catmull; + break; + case @"B": + curveType = CurveTypes.Bezier; + break; + case @"L": + curveType = CurveTypes.Linear; + break; + case @"P": + curveType = CurveTypes.PerfectCurve; + break; + } + continue; + } + + string[] temp = pointsplit[i].Split(':'); + Vector2 v = new Vector2( + (int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), + (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture) + ); + points.Add(v); + } + + repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture); + + if (repeatCount > 9000) + { + throw new ArgumentOutOfRangeException("wacky man"); + } + + if (split.Length > 7) + length = Convert.ToDouble(split[7], CultureInfo.InvariantCulture); + + s.RepeatCount = repeatCount; + + s.Curve = new SliderCurve + { + Path = points, + Length = length, + CurveType = curveType + }; + + s.Curve.Calculate(); + + result = s; break; case OsuBaseHit.HitObjectType.Spinner: result = new Spinner(); diff --git a/osu.Game.Mode.Osu/Objects/Slider.cs b/osu.Game.Mode.Osu/Objects/Slider.cs index 5ebe513556..ee0cad8d15 100644 --- a/osu.Game.Mode.Osu/Objects/Slider.cs +++ b/osu.Game.Mode.Osu/Objects/Slider.cs @@ -8,8 +8,198 @@ namespace osu.Game.Modes.Osu.Objects { public class Slider : OsuBaseHit { - public List Path; + public override double EndTime => StartTime + (RepeatCount + 1) * Curve.Length; public int RepeatCount; + + public SliderCurve Curve; + } + + public class SliderCurve + { + public double Length; + + public List Path; + + public CurveTypes CurveType; + + private List calculatedPath; + + public void Calculate() + { + switch (CurveType) + { + case CurveTypes.Linear: + calculatedPath = Path; + break; + default: + var bezier = new BezierApproximator(Path); + calculatedPath = bezier.CreateBezier(); + break; + } + } + + public Vector2 PositionAt(double progress) + { + int index = (int)(progress * (calculatedPath.Count - 1)); + + Vector2 pos = calculatedPath[index]; + if (index != progress) + pos += (calculatedPath[index + 1] - pos) * (float)(progress - index); + + return pos; + } + } + + public class BezierApproximator + { + private int count; + private List controlPoints; + private Vector2[] subdivisionBuffer1; + private Vector2[] subdivisionBuffer2; + + private const float TOLERANCE = 0.5f; + private const float TOLERANCE_SQ = TOLERANCE * TOLERANCE; + + public BezierApproximator(List controlPoints) + { + this.controlPoints = controlPoints; + count = controlPoints.Count; + + subdivisionBuffer1 = new Vector2[count]; + subdivisionBuffer2 = new Vector2[count * 2 - 1]; + } + + /// + /// Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds. + /// NOTE: The 2nd order derivative of a 2d curve represents its curvature, so intuitively this function + /// checks (as the name suggests) whether our approximation is _locally_ "flat". More curvy parts + /// need to have a denser approximation to be more "flat". + /// + /// The control points to check for flatness. + /// Whether the control points are flat enough. + private static bool IsFlatEnough(Vector2[] controlPoints) + { + for (int i = 1; i < controlPoints.Length - 1; i++) + if ((controlPoints[i - 1] - 2 * controlPoints[i] + controlPoints[i + 1]).LengthSquared > TOLERANCE_SQ) + return false; + + return true; + } + + /// + /// Subdivides n control points representing a bezier curve into 2 sets of n control points, each + /// describing a bezier curve equivalent to a half of the original curve. Effectively this splits + /// the original curve into 2 curves which result in the original curve when pieced back together. + /// + /// The control points to split. + /// Output: The control points corresponding to the left half of the curve. + /// Output: The control points corresponding to the right half of the curve. + private void Subdivide(Vector2[] controlPoints, Vector2[] l, Vector2[] r) + { + Vector2[] midpoints = subdivisionBuffer1; + + for (int i = 0; i < count; ++i) + midpoints[i] = controlPoints[i]; + + for (int i = 0; i < count; i++) + { + l[i] = midpoints[0]; + r[count - i - 1] = midpoints[count - i - 1]; + + for (int j = 0; j < count - i - 1; j++) + midpoints[j] = (midpoints[j] + midpoints[j + 1]) / 2; + } + } + + /// + /// This uses De Casteljau's algorithm to obtain an optimal + /// piecewise-linear approximation of the bezier curve with the same amount of points as there are control points. + /// + /// The control points describing the bezier curve to be approximated. + /// The points representing the resulting piecewise-linear approximation. + private void Approximate(Vector2[] controlPoints, List output) + { + Vector2[] l = subdivisionBuffer2; + Vector2[] r = subdivisionBuffer1; + + Subdivide(controlPoints, l, r); + + for (int i = 0; i < count - 1; ++i) + l[count + i] = r[i + 1]; + + output.Add(controlPoints[0]); + for (int i = 1; i < count - 1; ++i) + { + int index = 2 * i; + Vector2 p = 0.25f * (l[index - 1] + 2 * l[index] + l[index + 1]); + output.Add(p); + } + } + + /// + /// Creates a piecewise-linear approximation of a bezier curve, by adaptively repeatedly subdividing + /// the control points until their approximation error vanishes below a given threshold. + /// + /// The control points describing the curve. + /// A list of vectors representing the piecewise-linear approximation. + public List CreateBezier() + { + List output = new List(); + + if (count == 0) + return output; + + Stack toFlatten = new Stack(); + Stack freeBuffers = new Stack(); + + // "toFlatten" contains all the curves which are not yet approximated well enough. + // We use a stack to emulate recursion without the risk of running into a stack overflow. + // (More specifically, we iteratively and adaptively refine our curve with a + // Depth-first search + // over the tree resulting from the subdivisions we make.) + toFlatten.Push(controlPoints.ToArray()); + + Vector2[] leftChild = subdivisionBuffer2; + + while (toFlatten.Count > 0) + { + Vector2[] parent = toFlatten.Pop(); + if (IsFlatEnough(parent)) + { + // If the control points we currently operate on are sufficiently "flat", we use + // an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation + // of the bezier curve represented by our control points, consisting of the same amount + // of points as there are control points. + Approximate(parent, output); + freeBuffers.Push(parent); + continue; + } + + // If we do not yet have a sufficiently "flat" (in other words, detailed) approximation we keep + // subdividing the curve we are currently operating on. + Vector2[] rightChild = freeBuffers.Count > 0 ? freeBuffers.Pop() : new Vector2[count]; + Subdivide(parent, leftChild, rightChild); + + // We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration. + for (int i = 0; i < count; ++i) + parent[i] = leftChild[i]; + + toFlatten.Push(rightChild); + toFlatten.Push(parent); + } + + output.Add(controlPoints[count - 1]); + return output; + } + } + + public enum CurveTypes + { + Catmull, + Bezier, + Linear, + PerfectCurve + }; } diff --git a/osu.Game.Mode.Osu/UI/OsuHitRenderer.cs b/osu.Game.Mode.Osu/UI/OsuHitRenderer.cs index c8f8b7b3e7..50826f3d4e 100644 --- a/osu.Game.Mode.Osu/UI/OsuHitRenderer.cs +++ b/osu.Game.Mode.Osu/UI/OsuHitRenderer.cs @@ -18,6 +18,13 @@ public class OsuHitRenderer : HitRenderer protected override Playfield CreatePlayfield() => new OsuPlayfield(); protected override DrawableHitObject GetVisualRepresentation(OsuBaseHit h) - => h is HitCircle ? new DrawableHitCircle(h as HitCircle) : null; + { + if (h is HitCircle) + return new DrawableHitCircle(h as HitCircle); + if (h is Slider) + return new DrawableSlider(h as Slider); + + return null; + } } }