diff --git a/.gitignore b/.gitignore index 8f011deabe..f95a04e517 100644 --- a/.gitignore +++ b/.gitignore @@ -252,7 +252,11 @@ paket-files/ .fake/ # JetBrains Rider -.idea/ +.idea/.idea.osu/.idea/*.xml +.idea/.idea.osu/.idea/codeStyles/*.xml +.idea/.idea.osu/.idea/dataSources/*.xml +.idea/.idea.osu/.idea/dictionaries/*.xml +.idea/.idea.osu/*.iml *.sln.iml # CodeRush diff --git a/osu-resources b/osu-resources index 9ee64e369f..651e598b01 160000 --- a/osu-resources +++ b/osu-resources @@ -1 +1 @@ -Subproject commit 9ee64e369fe6fdafc6aed40f5a35b5f01eb82c53 +Subproject commit 651e598b016b43e31ab1c1b29d5b30c92361b8d9 diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs index cac1356c81..bea64302c3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs @@ -5,6 +5,7 @@ using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; @@ -37,13 +38,11 @@ namespace osu.Game.Rulesets.Catch.Tests beatmap.HitObjects.Add(new JuiceStream { X = 0.5f - width / 2, - ControlPoints = new[] + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(width * CatchPlayfield.BASE_WIDTH, 0) - }, - PathType = PathType.Linear, - Distance = width * CatchPlayfield.BASE_WIDTH, + }), StartTime = i * 2000, NewCombo = i % 8 == 0 }); diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index fed65c42af..c3dc9499c2 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -34,9 +34,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - ControlPoints = curveData.ControlPoints, - PathType = curveData.PathType, - Distance = curveData.Distance, + Path = curveData.Path, NodeSamples = curveData.NodeSamples, RepeatCount = curveData.RepeatCount, X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH, diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index f1e131932b..d8bd3e0edc 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -10,7 +10,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; -using OpenTK; namespace osu.Game.Rulesets.Catch.Objects { @@ -138,28 +137,18 @@ namespace osu.Game.Rulesets.Catch.Objects public double Duration => EndTime - StartTime; - public double Distance + private SliderPath path; + + public SliderPath Path { - get { return Path.Distance; } - set { Path.Distance = value; } + get => path; + set => path = value; } - public SliderPath Path { get; } = new SliderPath(); - - public Vector2[] ControlPoints - { - get { return Path.ControlPoints; } - set { Path.ControlPoints = value; } - } + public double Distance => Path.Distance; public List> NodeSamples { get; set; } = new List>(); - public PathType PathType - { - get { return Path.PathType; } - set { Path.PathType = value; } - } - public double? LegacyLastTickOffset { get; set; } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 925e7aaac9..08b7684677 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI protected override bool UserScrollSpeedAdjustment => false; - protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Constant; + protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Constant; public CatchPlayfield(BeatmapDifficulty difficulty, Func> getVisualRepresentation) { diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs index 0bd6bb5abc..5b638782fb 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs @@ -18,6 +18,7 @@ using System.Linq; using NUnit.Framework; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; @@ -108,13 +109,12 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000, Position = new Vector2(239, 176), - ControlPoints = new[] + Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, new Vector2(154, 28), new Vector2(52, -34) - }, - Distance = 700, + }, 700), RepeatCount = repeats, NodeSamples = createEmptySamples(repeats), StackHeight = 10 @@ -141,12 +141,11 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000, Position = new Vector2(-(distance / 2), 0), - ControlPoints = new[] + Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, new Vector2(distance, 0), - }, - Distance = distance, + }, distance), RepeatCount = repeats, NodeSamples = createEmptySamples(repeats), StackHeight = stackHeight @@ -161,13 +160,12 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000, Position = new Vector2(-200, 0), - ControlPoints = new[] + Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, new Vector2(200, 200), new Vector2(400, 0) - }, - Distance = 600, + }, 600), RepeatCount = repeats, NodeSamples = createEmptySamples(repeats) }; @@ -181,10 +179,9 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - PathType = PathType.Linear, StartTime = Time.Current + 1000, Position = new Vector2(-200, 0), - ControlPoints = new[] + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(150, 75), @@ -192,8 +189,7 @@ namespace osu.Game.Rulesets.Osu.Tests new Vector2(300, -200), new Vector2(400, 0), new Vector2(430, 0) - }, - Distance = 793.4417, + }), RepeatCount = repeats, NodeSamples = createEmptySamples(repeats) }; @@ -207,18 +203,16 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - PathType = PathType.Bezier, StartTime = Time.Current + 1000, Position = new Vector2(-200, 0), - ControlPoints = new[] + Path = new SliderPath(PathType.Bezier, new[] { Vector2.Zero, new Vector2(150, 75), new Vector2(200, 100), new Vector2(300, -200), new Vector2(430, 0) - }, - Distance = 480, + }), RepeatCount = repeats, NodeSamples = createEmptySamples(repeats) }; @@ -232,10 +226,9 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - PathType = PathType.Linear, StartTime = Time.Current + 1000, Position = new Vector2(0, 0), - ControlPoints = new[] + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(-200, 0), @@ -243,8 +236,7 @@ namespace osu.Game.Rulesets.Osu.Tests new Vector2(0, -200), new Vector2(-200, -200), new Vector2(0, -200) - }, - Distance = 1000, + }), RepeatCount = repeats, NodeSamples = createEmptySamples(repeats) }; @@ -264,15 +256,13 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000, Position = new Vector2(-100, 0), - PathType = PathType.Catmull, - ControlPoints = new[] + Path = new SliderPath(PathType.Catmull, new[] { Vector2.Zero, new Vector2(50, -50), new Vector2(150, 50), new Vector2(200, 0) - }, - Distance = 300, + }), RepeatCount = repeats, NodeSamples = repeatSamples }; diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionBlueprint.cs index 78e3d76313..cacbcb2cd6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionBlueprint.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; @@ -35,14 +36,12 @@ namespace osu.Game.Rulesets.Osu.Tests var slider = new Slider { Position = new Vector2(256, 192), - ControlPoints = new[] + Path = new SliderPath(PathType.Bezier, new[] { Vector2.Zero, new Vector2(150, 150), new Vector2(300, 0) - }, - PathType = PathType.Bezier, - Distance = 350 + }) }; slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 }); diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 87c81cdd3b..4fc4f3edc3 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -35,9 +35,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { StartTime = original.StartTime, Samples = original.Samples, - ControlPoints = curveData.ControlPoints, - PathType = curveData.PathType, - Distance = curveData.Distance, + Path = curveData.Path, NodeSamples = curveData.NodeSamples, RepeatCount = curveData.RepeatCount, Position = positionData?.Position ?? Vector2.Zero, diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 175e9d79f4..7100d9443e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -9,6 +8,7 @@ using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using OpenTK; @@ -55,16 +55,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { base.Update(); - Position = slider.StackedPosition + slider.ControlPoints[index]; + Position = slider.StackedPosition + slider.Path.ControlPoints[index]; marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow; path.ClearVertices(); - if (index != slider.ControlPoints.Length - 1) + if (index != slider.Path.ControlPoints.Length - 1) { path.AddVertex(Vector2.Zero); - path.AddVertex(slider.ControlPoints[index + 1] - slider.ControlPoints[index]); + path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]); } path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnDrag(DragEvent e) { - var newControlPoints = slider.ControlPoints.ToArray(); + var newControlPoints = slider.Path.ControlPoints.ToArray(); if (index == 0) { @@ -96,8 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (isSegmentSeparatorWithPrevious) newControlPoints[index - 1] = newControlPoints[index]; - slider.ControlPoints = newControlPoints; - slider.Path.Calculate(true); + slider.Path = new SliderPath(slider.Path.Type, newControlPoints); return true; } @@ -106,8 +105,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious; - private bool isSegmentSeparatorWithNext => index < slider.ControlPoints.Length - 1 && slider.ControlPoints[index + 1] == slider.ControlPoints[index]; + private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index]; - private bool isSegmentSeparatorWithPrevious => index > 0 && slider.ControlPoints[index - 1] == slider.ControlPoints[index]; + private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index]; } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index ffe32a9a72..a317965e57 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -30,9 +30,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private void updatePathControlPoints() { - while (slider.ControlPoints.Length > pieces.Count) + while (slider.Path.ControlPoints.Length > pieces.Count) pieces.Add(new PathControlPointPiece(slider, pieces.Count)); - while (slider.ControlPoints.Length < pieces.Count) + while (slider.Path.ControlPoints.Length < pieces.Count) pieces.Remove(pieces[pieces.Count - 1]); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index bd55009374..f301dff61a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -43,8 +43,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { base.Update(); - slider.Path.Calculate(); - var vertices = new List(); slider.Path.GetPathToProgress(vertices, 0, 1); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index add9cb69f3..d59cd35f19 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -1,16 +1,15 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Framework.MathUtils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using OpenTK; @@ -119,12 +118,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updateSlider() { - for (int i = 0; i < segments.Count; i++) - segments[i].Calculate(i == segments.Count - 1 ? (Vector2?)cursor : null); - - HitObject.ControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray(); - HitObject.PathType = HitObject.ControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear; - HitObject.Distance = segments.Sum(s => s.Distance); + var newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray(); + HitObject.Path = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints); } private void setState(PlacementState newState) @@ -140,41 +135,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private class Segment { - public float Distance { get; private set; } - public readonly List ControlPoints = new List(); public Segment(Vector2 offset) { ControlPoints.Add(offset); } - - public void Calculate(Vector2? cursor = null) - { - Span allControlPoints = stackalloc Vector2[ControlPoints.Count + (cursor.HasValue ? 1 : 0)]; - - for (int i = 0; i < ControlPoints.Count; i++) - allControlPoints[i] = ControlPoints[i]; - if (cursor.HasValue) - allControlPoints[allControlPoints.Length - 1] = cursor.Value; - - List result; - - switch (allControlPoints.Length) - { - case 1: - case 2: - result = PathApproximator.ApproximateLinear(allControlPoints); - break; - default: - result = PathApproximator.ApproximateBezier(allControlPoints); - break; - } - - Distance = 0; - for (int i = 0; i < result.Count - 1; i++) - Distance += Vector2.Distance(result[i], result[i + 1]); - } } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index e01d71e1f8..223e4df844 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -32,12 +32,11 @@ namespace osu.Game.Rulesets.Osu.Mods slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); - var newControlPoints = new Vector2[slider.ControlPoints.Length]; - for (int i = 0; i < slider.ControlPoints.Length; i++) - newControlPoints[i] = new Vector2(slider.ControlPoints[i].X, -slider.ControlPoints[i].Y); + var newControlPoints = new Vector2[slider.Path.ControlPoints.Length]; + for (int i = 0; i < slider.Path.ControlPoints.Length; i++) + newControlPoints[i] = new Vector2(slider.Path.ControlPoints[i].X, -slider.Path.ControlPoints[i].Y); - slider.ControlPoints = newControlPoints; - slider.Path?.Calculate(); // Recalculate the slider curve + slider.Path = new SliderPath(slider.Path.Type, newControlPoints, slider.Path.ExpectedDistance); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 133a2e57ab..026a44e74b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -51,34 +51,22 @@ namespace osu.Game.Rulesets.Osu.Objects } } - public SliderPath Path { get; } = new SliderPath(); + private SliderPath path; - public readonly Bindable ControlPointsBindable = new Bindable(Array.Empty()); - - public Vector2[] ControlPoints + public SliderPath Path { - get => ControlPointsBindable; + get => path; set { - ControlPointsBindable.Value = value; - Path.ControlPoints = value; + path = value; + PathChanged?.Invoke(value); if (TailCircle != null) TailCircle.Position = EndPosition; } } - public PathType PathType - { - get { return Path.PathType; } - set { Path.PathType = value; } - } - - public double Distance - { - get { return Path.Distance; } - set { Path.Distance = value; } - } + public double Distance => Path.Distance; public override Vector2 Position { diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 40ed659bd6..824c1f817a 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected override bool UserScrollSpeedAdjustment => false; - protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Overlapping; + protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; diff --git a/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs b/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs index 2629b29c6c..d894d2738e 100644 --- a/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs @@ -11,6 +11,7 @@ using OpenTK; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -53,12 +54,11 @@ namespace osu.Game.Tests.Visual new Slider { Position = new Vector2(128, 256), - ControlPoints = new[] + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(216, 0), - }, - Distance = 216, + }), Scale = 0.5f, } }, diff --git a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs index b254325472..eb322df185 100644 --- a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs @@ -114,11 +114,11 @@ namespace osu.Game.Tests.Visual private class TestPlayfield : ScrollingPlayfield { - public new readonly ScrollingDirection Direction; + public new ScrollingDirection Direction => base.Direction; public TestPlayfield(ScrollingDirection direction) { - Direction = direction; + base.Direction.Value = direction; Padding = new MarginPadding(2); diff --git a/osu.Game/Configuration/SpeedChangeVisualisationMethod.cs b/osu.Game/Configuration/ScrollVisualisationMethod.cs similarity index 89% rename from osu.Game/Configuration/SpeedChangeVisualisationMethod.cs rename to osu.Game/Configuration/ScrollVisualisationMethod.cs index 39c6e5649c..cc7dcdbc0e 100644 --- a/osu.Game/Configuration/SpeedChangeVisualisationMethod.cs +++ b/osu.Game/Configuration/ScrollVisualisationMethod.cs @@ -5,7 +5,7 @@ using System.ComponentModel; namespace osu.Game.Configuration { - public enum SpeedChangeVisualisationMethod + public enum ScrollVisualisationMethod { [Description("Sequential")] Sequential, diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index b32fd265cb..f282b757cd 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -245,10 +245,10 @@ namespace osu.Game.Overlays { base.Update(); - if (current?.TrackLoaded ?? false) - { - var track = current.Track; + var track = current?.TrackLoaded ?? false ? current.Track : null; + if (track?.IsDummyDevice == false) + { progressBar.EndTime = track.Length; progressBar.CurrentTime = track.CurrentTime; @@ -258,7 +258,11 @@ namespace osu.Game.Overlays next(); } else + { + progressBar.CurrentTime = 0; + progressBar.EndTime = 1; playButton.Icon = FontAwesome.fa_play_circle_o; + } } private void play() diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index c805c55ed1..b167812c1d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -50,9 +50,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch X = position.X, NewCombo = FirstObject || newCombo, ComboOffset = comboOffset, - ControlPoints = controlPoints, - Distance = length, - PathType = pathType, + Path = new SliderPath(pathType, controlPoints, length), NodeSamples = nodeSamples, RepeatCount = repeatCount }; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index b3d9f3c40c..0512a97354 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -3,7 +3,6 @@ using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; -using OpenTK; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -20,11 +19,9 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// s don't need a curve since they're converted to ruleset-specific hitobjects. /// - public SliderPath Path { get; } = null; - public Vector2[] ControlPoints { get; set; } - public PathType PathType { get; set; } + public SliderPath Path { get; set; } - public double Distance { get; set; } + public double Distance => Path.Distance; public List> NodeSamples { get; set; } public int RepeatCount { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs index 90b7f3d554..fa5e769d3c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs @@ -31,9 +31,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania return new ConvertSlider { X = position.X, - ControlPoints = controlPoints, - Distance = length, - PathType = pathType, + Path = new SliderPath(pathType, controlPoints, length), NodeSamples = nodeSamples, RepeatCount = repeatCount }; diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index bb41a147b0..e21903dc6d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -51,9 +51,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu Position = position, NewCombo = FirstObject || newCombo, ComboOffset = comboOffset, - ControlPoints = controlPoints, - Distance = Math.Max(0, length), - PathType = pathType, + Path = new SliderPath(pathType, controlPoints, Math.Max(0, length)), NodeSamples = nodeSamples, RepeatCount = repeatCount }; diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs index ae913b3bef..8e1e01a9fd 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs @@ -27,9 +27,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko { return new ConvertSlider { - ControlPoints = controlPoints, - Distance = length, - PathType = pathType, + Path = new SliderPath(pathType, controlPoints, length), NodeSamples = nodeSamples, RepeatCount = repeatCount }; diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 423cd3b069..74a312698c 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -4,28 +4,140 @@ using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Types; using OpenTK; namespace osu.Game.Rulesets.Objects { - public class SliderPath + public struct SliderPath : IEquatable { - public double Distance; + /// + /// The user-set distance of the path. If non-null, will match this value, + /// and the path will be shortened/lengthened to match this length. + /// + public readonly double? ExpectedDistance; - public Vector2[] ControlPoints = Array.Empty(); + /// + /// The type of path. + /// + public readonly PathType Type; - public PathType PathType = PathType.PerfectCurve; + [JsonProperty] + private Vector2[] controlPoints; - public Vector2 Offset; + private List calculatedPath; + private List cumulativeLength; - private readonly List calculatedPath = new List(); - private readonly List cumulativeLength = new List(); + private bool isInitialised; + + /// + /// Creates a new . + /// + /// The type of path. + /// The control points of the path. + /// A user-set distance of the path that may be shorter or longer than the true distance between all + /// . The path will be shortened/lengthened to match this length. + /// If null, the path will use the true distance between all . + [JsonConstructor] + public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null) + { + this = default; + this.controlPoints = controlPoints; + + Type = type; + ExpectedDistance = expectedDistance; + + ensureInitialised(); + } + + /// + /// The control points of the path. + /// + [JsonIgnore] + public ReadOnlySpan ControlPoints + { + get + { + ensureInitialised(); + return controlPoints.AsSpan(); + } + } + + /// + /// The distance of the path after lengthening/shortening to account for . + /// + [JsonIgnore] + public double Distance + { + get + { + ensureInitialised(); + return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1]; + } + } + + /// + /// Computes the slider path until a given progress that ranges from 0 (beginning of the slider) + /// to 1 (end of the slider) and stores the generated path in the given list. + /// + /// The list to be filled with the computed path. + /// Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider). + /// End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider). + public void GetPathToProgress(List path, double p0, double p1) + { + ensureInitialised(); + + double d0 = progressToDistance(p0); + double d1 = progressToDistance(p1); + + path.Clear(); + + int i = 0; + for (; i < calculatedPath.Count && cumulativeLength[i] < d0; ++i) + { + } + + path.Add(interpolateVertices(i, d0)); + + for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i) + path.Add(calculatedPath[i]); + + path.Add(interpolateVertices(i, d1)); + } + + /// + /// Computes the position on the slider at a given progress that ranges from 0 (beginning of the path) + /// to 1 (end of the path). + /// + /// Ranges from 0 (beginning of the path) to 1 (end of the path). + /// + public Vector2 PositionAt(double progress) + { + ensureInitialised(); + + double d = progressToDistance(progress); + return interpolateVertices(indexOfDistance(d), d); + } + + private void ensureInitialised() + { + if (isInitialised) + return; + isInitialised = true; + + controlPoints = controlPoints ?? Array.Empty(); + calculatedPath = new List(); + cumulativeLength = new List(); + + calculatePath(); + calculateCumulativeLength(); + } private List calculateSubpath(ReadOnlySpan subControlPoints) { - switch (PathType) + switch (Type) { case PathType.Linear: return PathApproximator.ApproximateLinear(subControlPoints); @@ -66,7 +178,7 @@ namespace osu.Game.Rulesets.Objects if (i == ControlPoints.Length - 1 || ControlPoints[i] == ControlPoints[i + 1]) { - ReadOnlySpan cpSpan = ControlPoints.AsSpan().Slice(start, end - start); + ReadOnlySpan cpSpan = ControlPoints.Slice(start, end - start); foreach (Vector2 t in calculateSubpath(cpSpan)) if (calculatedPath.Count == 0 || calculatedPath.Last() != t) @@ -77,49 +189,6 @@ namespace osu.Game.Rulesets.Objects } } - private void calculateCumulativeLengthAndTrimPath() - { - double l = 0; - - cumulativeLength.Clear(); - cumulativeLength.Add(l); - - for (int i = 0; i < calculatedPath.Count - 1; ++i) - { - Vector2 diff = calculatedPath[i + 1] - calculatedPath[i]; - double d = diff.Length; - - // Shorten slider paths that are too long compared to what's - // in the .osu file. - if (Distance - l < d) - { - calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((Distance - l) / d); - calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i); - - l = Distance; - cumulativeLength.Add(l); - break; - } - - l += d; - cumulativeLength.Add(l); - } - - // Lengthen slider paths that are too short compared to what's - // in the .osu file. - if (l < Distance && calculatedPath.Count > 1) - { - Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2]; - double d = diff.Length; - - if (d <= 0) - return; - - calculatedPath[calculatedPath.Count - 1] += diff * (float)((Distance - l) / d); - cumulativeLength[calculatedPath.Count - 1] = Distance; - } - } - private void calculateCumulativeLength() { double l = 0; @@ -132,21 +201,33 @@ namespace osu.Game.Rulesets.Objects Vector2 diff = calculatedPath[i + 1] - calculatedPath[i]; double d = diff.Length; + // Shorted slider paths that are too long compared to the expected distance + if (ExpectedDistance.HasValue && ExpectedDistance - l < d) + { + calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((ExpectedDistance - l) / d); + calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i); + + l = ExpectedDistance.Value; + cumulativeLength.Add(l); + break; + } + l += d; cumulativeLength.Add(l); } - Distance = l; - } + // Lengthen slider paths that are too short compared to the expected distance + if (ExpectedDistance.HasValue && l < ExpectedDistance && calculatedPath.Count > 1) + { + Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2]; + double d = diff.Length; - public void Calculate(bool updateDistance = false) - { - calculatePath(); + if (d <= 0) + return; - if (!updateDistance) - calculateCumulativeLengthAndTrimPath(); - else - calculateCumulativeLength(); + calculatedPath[calculatedPath.Count - 1] += diff * (float)((ExpectedDistance - l) / d); + cumulativeLength[calculatedPath.Count - 1] = ExpectedDistance.Value; + } } private int indexOfDistance(double d) @@ -169,7 +250,7 @@ namespace osu.Game.Rulesets.Objects if (i <= 0) return calculatedPath.First(); - else if (i >= calculatedPath.Count) + if (i >= calculatedPath.Count) return calculatedPath.Last(); Vector2 p0 = calculatedPath[i - 1]; @@ -186,47 +267,20 @@ namespace osu.Game.Rulesets.Objects return p0 + (p1 - p0) * (float)w; } - /// - /// Computes the slider path until a given progress that ranges from 0 (beginning of the slider) - /// to 1 (end of the slider) and stores the generated path in the given list. - /// - /// The list to be filled with the computed path. - /// Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider). - /// End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider). - public void GetPathToProgress(List path, double p0, double p1) + public bool Equals(SliderPath other) { - if (calculatedPath.Count == 0 && ControlPoints.Length > 0) - Calculate(); + if (ControlPoints == null && other.ControlPoints != null) + return false; + if (other.ControlPoints == null && ControlPoints != null) + return false; - double d0 = progressToDistance(p0); - double d1 = progressToDistance(p1); - - path.Clear(); - - int i = 0; - for (; i < calculatedPath.Count && cumulativeLength[i] < d0; ++i) { } - - path.Add(interpolateVertices(i, d0) + Offset); - - for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i) - path.Add(calculatedPath[i] + Offset); - - path.Add(interpolateVertices(i, d1) + Offset); + return ControlPoints.SequenceEqual(other.ControlPoints) && ExpectedDistance.Equals(other.ExpectedDistance) && Type == other.Type; } - /// - /// Computes the position on the slider at a given progress that ranges from 0 (beginning of the path) - /// to 1 (end of the path). - /// - /// Ranges from 0 (beginning of the path) to 1 (end of the path). - /// - public Vector2 PositionAt(double progress) + public override bool Equals(object obj) { - if (calculatedPath.Count == 0 && ControlPoints.Length > 0) - Calculate(); - - double d = progressToDistance(progress); - return interpolateVertices(indexOfDistance(d), d) + Offset; + if (ReferenceEquals(null, obj)) return false; + return obj is SliderPath other && Equals(other); } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs index 2a0d495e94..a097b62851 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs @@ -14,16 +14,6 @@ namespace osu.Game.Rulesets.Objects.Types /// The curve. /// SliderPath Path { get; } - - /// - /// The control points that shape the curve. - /// - Vector2[] ControlPoints { get; } - - /// - /// The type of curve. - /// - PathType PathType { get; } } public static class HasCurveExtensions diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs new file mode 100644 index 0000000000..ed61ed7022 --- /dev/null +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.UI.Scrolling.Algorithms +{ + public class ConstantScrollAlgorithm : IScrollAlgorithm + { + public double GetDisplayStartTime(double time, double timeRange) => time - timeRange; + + public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) + { + // At the hitobject's end time, the hitobject will be positioned such that its end rests at the origin. + // This results in a negative-position value, and the absolute of it indicates the length of the hitobject. + return -PositionAt(startTime, endTime, timeRange, scrollLength); + } + + public float PositionAt(double time, double currentTime, double timeRange, float scrollLength) + => (float)((time - currentTime) / timeRange * scrollLength); + + public void Reset() + { + } + } +} diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs new file mode 100644 index 0000000000..43bc1b2a2a --- /dev/null +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.UI.Scrolling.Algorithms +{ + public interface IScrollAlgorithm + { + /// + /// Given a point in time, computes the time at which it enters the time range. + /// + /// + /// E.g. For a constant time range of 5000ms, the time at which t=7000ms enters the time range is 2000ms. + /// + /// The point in time. + /// The amount of visible time. + /// The time at which enters . + double GetDisplayStartTime(double time, double timeRange); + + /// + /// Computes the spatial length within a start and end time. + /// + /// The start time. + /// The end time. + /// The amount of visible time. + /// The absolute spatial length through . + /// The absolute spatial length. + float GetLength(double startTime, double endTime, double timeRange, float scrollLength); + + /// + /// Given the current time, computes the spatial position of a point in time. + /// + /// The time to compute the spatial position of. + /// The current time. + /// The amount of visible time. + /// The absolute spatial length through . + /// The absolute spatial position. + float PositionAt(double time, double currentTime, double timeRange, float scrollLength); + + /// + /// Resets this to a default state. + /// + void Reset(); + } +} diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs new file mode 100644 index 0000000000..f7c097e81d --- /dev/null +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Lists; +using osu.Game.Rulesets.Timing; + +namespace osu.Game.Rulesets.UI.Scrolling.Algorithms +{ + public class OverlappingScrollAlgorithm : IScrollAlgorithm + { + private readonly MultiplierControlPoint searchPoint; + + private readonly SortedList controlPoints; + + public OverlappingScrollAlgorithm(SortedList controlPoints) + { + this.controlPoints = controlPoints; + + searchPoint = new MultiplierControlPoint(); + } + + public double GetDisplayStartTime(double time, double timeRange) + { + // 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; + } + + public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) + { + // At the hitobject's end time, the hitobject will be positioned such that its end rests at the origin. + // This results in a negative-position value, and the absolute of it indicates the length of the hitobject. + return -PositionAt(startTime, endTime, timeRange, scrollLength); + } + + public float PositionAt(double time, double currentTime, double timeRange, float scrollLength) + => (float)((time - currentTime) / timeRange * controlPointAt(time).Multiplier * scrollLength); + + public void Reset() + { + } + + /// + /// Finds the which affects the speed of hitobjects at a specific time. + /// + /// The time which the should affect. + /// The . + private MultiplierControlPoint controlPointAt(double time) + { + if (controlPoints.Count == 0) + return new MultiplierControlPoint(double.NegativeInfinity); + + if (time < controlPoints[0].StartTime) + return controlPoints[0]; + + searchPoint.StartTime = time; + int index = controlPoints.BinarySearch(searchPoint); + + if (index < 0) + index = ~index - 1; + + return controlPoints[index]; + } + } +} diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs new file mode 100644 index 0000000000..54494cfe63 --- /dev/null +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Timing; + +namespace osu.Game.Rulesets.UI.Scrolling.Algorithms +{ + public class SequentialScrollAlgorithm : IScrollAlgorithm + { + private readonly Dictionary positionCache; + + private readonly IReadOnlyList controlPoints; + + public SequentialScrollAlgorithm(IReadOnlyList controlPoints) + { + this.controlPoints = controlPoints; + + positionCache = new Dictionary(); + } + + public double GetDisplayStartTime(double time, double timeRange) => time - timeRange - 1000; + + public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) + { + var objectLength = relativePositionAtCached(endTime, timeRange) - relativePositionAtCached(startTime, timeRange); + return (float)(objectLength * scrollLength); + } + + public float PositionAt(double time, double currentTime, double timeRange, float scrollLength) + { + // Caching is not used here as currentTime is unlikely to have been previously cached + double timelinePosition = relativePositionAt(currentTime, timeRange); + return (float)((relativePositionAtCached(time, timeRange) - timelinePosition) * scrollLength); + } + + private double relativePositionAtCached(double time, double timeRange) + { + if (!positionCache.TryGetValue(time, out double existing)) + positionCache[time] = existing = relativePositionAt(time, timeRange); + return existing; + } + + public void Reset() => positionCache.Clear(); + + /// + /// Finds the position which corresponds to a point in time. + /// This is a non-linear operation that depends on all the control points up to and including the one active at the time value. + /// + /// The time to find the position at. + /// The amount of time visualised by the scrolling area. + /// A positive value indicating the position at . + private double relativePositionAt(double time, double timeRange) + { + if (controlPoints.Count == 0) + return time / timeRange; + + double length = 0; + + // We need to consider all timing points until the specified time and not just the currently-active one, + // since each timing point individually affects the positions of _all_ hitobjects after its start time + for (int i = 0; i < controlPoints.Count; i++) + { + var current = controlPoints[i]; + var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null; + + // We don't need to consider any control points beyond the current time, since it will not yet + // affect any hitobjects + if (i > 0 && current.StartTime > time) + continue; + + // Duration of the current control point + var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime; + + // We want to consider the minimal amount of time that this control point has affected, + // which may be either its duration, or the amount of time that has passed within it + var durationInCurrent = Math.Min(currentDuration, time - current.StartTime); + + // Figure out how much of the time range the duration represents, and adjust it by the speed multiplier + length += durationInCurrent / timeRange * current.Multiplier; + } + + return length; + } + } +} diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 7307fc0ead..489604afc9 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -7,8 +7,9 @@ using osu.Framework.Graphics; using osu.Framework.Lists; using osu.Game.Configuration; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Timing; -using osu.Game.Rulesets.UI.Scrolling.Visualisers; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; namespace osu.Game.Rulesets.UI.Scrolling { @@ -30,11 +31,11 @@ namespace osu.Game.Rulesets.UI.Scrolling public readonly Bindable Direction = new Bindable(); + private readonly IScrollAlgorithm algorithm; + private Cached initialStateCache = new Cached(); - private readonly ISpeedChangeVisualiser speedChangeVisualiser; - - public ScrollingHitObjectContainer(SpeedChangeVisualisationMethod visualisationMethod) + public ScrollingHitObjectContainer(ScrollVisualisationMethod visualisationMethod) { RelativeSizeAxes = Axes.Both; @@ -43,14 +44,14 @@ namespace osu.Game.Rulesets.UI.Scrolling switch (visualisationMethod) { - case SpeedChangeVisualisationMethod.Sequential: - speedChangeVisualiser = new SequentialSpeedChangeVisualiser(ControlPoints); + case ScrollVisualisationMethod.Sequential: + algorithm = new SequentialScrollAlgorithm(ControlPoints); break; - case SpeedChangeVisualisationMethod.Overlapping: - speedChangeVisualiser = new OverlappingSpeedChangeVisualiser(ControlPoints); + case ScrollVisualisationMethod.Overlapping: + algorithm = new OverlappingScrollAlgorithm(ControlPoints); break; - case SpeedChangeVisualisationMethod.Constant: - speedChangeVisualiser = new ConstantSpeedChangeVisualiser(); + case ScrollVisualisationMethod.Constant: + algorithm = new ConstantScrollAlgorithm(); break; } } @@ -91,23 +92,87 @@ namespace osu.Game.Rulesets.UI.Scrolling return base.Invalidate(invalidation, source, shallPropagate); } + private float scrollLength; + protected override void Update() { base.Update(); if (!initialStateCache.IsValid) { - speedChangeVisualiser.ComputeInitialStates(Objects, Direction, TimeRange, DrawSize); + switch (Direction.Value) + { + case ScrollingDirection.Up: + case ScrollingDirection.Down: + scrollLength = DrawSize.Y; + break; + default: + scrollLength = DrawSize.X; + break; + } + + algorithm.Reset(); + + foreach (var obj in Objects) + computeInitialStateRecursive(obj); initialStateCache.Validate(); } } + private void computeInitialStateRecursive(DrawableHitObject hitObject) + { + hitObject.LifetimeStart = algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, TimeRange); + + if (hitObject.HitObject is IHasEndTime endTime) + { + switch (Direction.Value) + { + case ScrollingDirection.Up: + case ScrollingDirection.Down: + hitObject.Height = algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, TimeRange, scrollLength); + break; + case ScrollingDirection.Left: + case ScrollingDirection.Right: + hitObject.Width = algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, TimeRange, scrollLength); + break; + } + } + + foreach (var obj in hitObject.NestedHitObjects) + { + computeInitialStateRecursive(obj); + + // Nested hitobjects don't need to scroll, but they do need accurate positions + updatePosition(obj, hitObject.HitObject.StartTime); + } + } + protected override void UpdateAfterChildrenLife() { base.UpdateAfterChildrenLife(); - // We need to calculate this as soon as possible after lifetimes so that hitobjects get the final say in their positions - speedChangeVisualiser.UpdatePositions(AliveObjects, Direction, Time.Current, TimeRange, DrawSize); + // We need to calculate hitobject positions as soon as possible after lifetimes so that hitobjects get the final say in their positions + foreach (var obj in AliveObjects) + updatePosition(obj, Time.Current); + } + + private void updatePosition(DrawableHitObject hitObject, double currentTime) + { + switch (Direction.Value) + { + case ScrollingDirection.Up: + hitObject.Y = algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, TimeRange, scrollLength); + break; + case ScrollingDirection.Down: + hitObject.Y = -algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, TimeRange, scrollLength); + break; + case ScrollingDirection.Left: + hitObject.X = algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, TimeRange, scrollLength); + break; + case ScrollingDirection.Right: + hitObject.X = -algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, TimeRange, scrollLength); + break; + } } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index a1fc13ce4d..5e2704c9ee 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// protected readonly Bindable Direction = new Bindable(); - protected virtual SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Sequential; + protected virtual ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Sequential; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/ConstantSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/ConstantSpeedChangeVisualiser.cs deleted file mode 100644 index 9e910d6b11..0000000000 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/ConstantSpeedChangeVisualiser.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Objects.Types; -using OpenTK; - -namespace osu.Game.Rulesets.UI.Scrolling.Visualisers -{ - public class ConstantSpeedChangeVisualiser : ISpeedChangeVisualiser - { - public void ComputeInitialStates(IEnumerable hitObjects, ScrollingDirection direction, double timeRange, Vector2 length) - { - foreach (var obj in hitObjects) - { - obj.LifetimeStart = obj.HitObject.StartTime - timeRange; - - if (obj.HitObject is IHasEndTime endTime) - { - var hitObjectLength = (endTime.EndTime - obj.HitObject.StartTime) / timeRange; - - switch (direction) - { - case ScrollingDirection.Up: - case ScrollingDirection.Down: - obj.Height = (float)(hitObjectLength * length.Y); - break; - case ScrollingDirection.Left: - case ScrollingDirection.Right: - obj.Width = (float)(hitObjectLength * length.X); - break; - } - } - - ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); - - // Nested hitobjects don't need to scroll, but they do need accurate positions - UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); - } - } - - public void UpdatePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length) - { - foreach (var obj in hitObjects) - { - var position = (obj.HitObject.StartTime - currentTime) / timeRange; - - switch (direction) - { - case ScrollingDirection.Up: - obj.Y = (float)(position * length.Y); - break; - case ScrollingDirection.Down: - obj.Y = (float)(-position * length.Y); - break; - case ScrollingDirection.Left: - obj.X = (float)(position * length.X); - break; - case ScrollingDirection.Right: - obj.X = (float)(-position * length.X); - break; - } - } - } - } -} diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/ISpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/ISpeedChangeVisualiser.cs deleted file mode 100644 index 097e28b2dc..0000000000 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/ISpeedChangeVisualiser.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; - -namespace osu.Game.Rulesets.UI.Scrolling.Visualisers -{ - public interface ISpeedChangeVisualiser - { - /// - /// Computes the states of s that remain constant while scrolling, such as lifetime and spatial length. - /// This is invoked once whenever or changes. - /// - /// The s whose states should be computed. - /// The scrolling direction. - /// The duration required to scroll through one length of the screen before any speed adjustments. - /// The length of the screen that is scrolled through. - void ComputeInitialStates(IEnumerable hitObjects, ScrollingDirection direction, double timeRange, Vector2 length); - - /// - /// Updates the positions of s, depending on the current time. This is invoked once per frame. - /// - /// The s whose positions should be computed. - /// The scrolling direction. - /// The current time. - /// The duration required to scroll through one length of the screen before any speed adjustments. - /// The length of the screen that is scrolled through. - void UpdatePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length); - } -} diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs deleted file mode 100644 index d2b79e2fa7..0000000000 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Lists; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Timing; -using OpenTK; - -namespace osu.Game.Rulesets.UI.Scrolling.Visualisers -{ - public class OverlappingSpeedChangeVisualiser : ISpeedChangeVisualiser - { - private readonly SortedList controlPoints; - - public OverlappingSpeedChangeVisualiser(SortedList controlPoints) - { - this.controlPoints = controlPoints; - } - - public void ComputeInitialStates(IEnumerable hitObjects, ScrollingDirection direction, double timeRange, Vector2 length) - { - foreach (var obj in hitObjects) - { - // 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(obj.HitObject.StartTime).Multiplier; - - obj.LifetimeStart = obj.HitObject.StartTime - visibleDuration; - - if (obj.HitObject is IHasEndTime endTime) - { - // At the hitobject's end time, the hitobject will be positioned such that its end rests at the origin. - // This results in a negative-position value, and the absolute of it indicates the length of the hitobject. - var hitObjectLength = -hitObjectPositionAt(obj, endTime.EndTime, timeRange); - - switch (direction) - { - case ScrollingDirection.Up: - case ScrollingDirection.Down: - obj.Height = (float)(hitObjectLength * length.Y); - break; - case ScrollingDirection.Left: - case ScrollingDirection.Right: - obj.Width = (float)(hitObjectLength * length.X); - break; - } - } - - ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); - - // Nested hitobjects don't need to scroll, but they do need accurate positions - UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); - } - } - - public void UpdatePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length) - { - foreach (var obj in hitObjects) - { - var position = hitObjectPositionAt(obj, currentTime, timeRange); - - switch (direction) - { - case ScrollingDirection.Up: - obj.Y = (float)(position * length.Y); - break; - case ScrollingDirection.Down: - obj.Y = (float)(-position * length.Y); - break; - case ScrollingDirection.Left: - obj.X = (float)(position * length.X); - break; - case ScrollingDirection.Right: - obj.X = (float)(-position * length.X); - break; - } - } - } - - /// - /// Computes the position of a at a point in time. - /// - /// At t < startTime, position > 0.
- /// At t = startTime, position = 0.
- /// At t > startTime, position < 0. - ///
- ///
- /// The . - /// The time to find the position of at. - /// The amount of time visualised by the scrolling area. - /// The position of in the scrolling area at time = . - private double hitObjectPositionAt(DrawableHitObject obj, double time, double timeRange) - => (obj.HitObject.StartTime - time) / timeRange * controlPointAt(obj.HitObject.StartTime).Multiplier; - - private readonly MultiplierControlPoint searchPoint = new MultiplierControlPoint(); - - /// - /// Finds the which affects the speed of hitobjects at a specific time. - /// - /// The time which the should affect. - /// The . - private MultiplierControlPoint controlPointAt(double time) - { - if (controlPoints.Count == 0) - return new MultiplierControlPoint(double.NegativeInfinity); - - if (time < controlPoints[0].StartTime) - return controlPoints[0]; - - searchPoint.StartTime = time; - int index = controlPoints.BinarySearch(searchPoint); - - if (index < 0) - index = ~index - 1; - - return controlPoints[index]; - } - } -} diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs deleted file mode 100644 index 25dea8dfbf..0000000000 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Timing; -using OpenTK; - -namespace osu.Game.Rulesets.UI.Scrolling.Visualisers -{ - public class SequentialSpeedChangeVisualiser : ISpeedChangeVisualiser - { - private readonly Dictionary hitObjectPositions = new Dictionary(); - - private readonly IReadOnlyList controlPoints; - - public SequentialSpeedChangeVisualiser(IReadOnlyList controlPoints) - { - this.controlPoints = controlPoints; - } - - public void ComputeInitialStates(IEnumerable hitObjects, ScrollingDirection direction, double timeRange, Vector2 length) - { - foreach (var obj in hitObjects) - { - // To reduce iterations when updating hitobject positions later on, their initial positions are cached - var startPosition = hitObjectPositions[obj] = positionAt(obj.HitObject.StartTime, timeRange); - - // Todo: This is approximate and will be incorrect in the case of extreme speed changes - obj.LifetimeStart = obj.HitObject.StartTime - timeRange - 1000; - - if (obj.HitObject is IHasEndTime endTime) - { - var hitObjectLength = positionAt(endTime.EndTime, timeRange) - startPosition; - - switch (direction) - { - case ScrollingDirection.Up: - case ScrollingDirection.Down: - obj.Height = (float)(hitObjectLength * length.Y); - break; - case ScrollingDirection.Left: - case ScrollingDirection.Right: - obj.Width = (float)(hitObjectLength * length.X); - break; - } - } - - ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); - - // Nested hitobjects don't need to scroll, but they do need accurate positions - UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); - } - } - - public void UpdatePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length) - { - var timelinePosition = positionAt(currentTime, timeRange); - - foreach (var obj in hitObjects) - { - var finalPosition = hitObjectPositions[obj] - timelinePosition; - - switch (direction) - { - case ScrollingDirection.Up: - obj.Y = (float)(finalPosition * length.Y); - break; - case ScrollingDirection.Down: - obj.Y = (float)(-finalPosition * length.Y); - break; - case ScrollingDirection.Left: - obj.X = (float)(finalPosition * length.X); - break; - case ScrollingDirection.Right: - obj.X = (float)(-finalPosition * length.X); - break; - } - } - } - - /// - /// Finds the position which corresponds to a point in time. - /// This is a non-linear operation that depends on all the control points up to and including the one active at the time value. - /// - /// The time to find the position at. - /// The amount of time visualised by the scrolling area. - /// A positive value indicating the position at . - private double positionAt(double time, double timeRange) - { - if (controlPoints.Count == 0) - return time / timeRange; - - double length = 0; - - // We need to consider all timing points until the specified time and not just the currently-active one, - // since each timing point individually affects the positions of _all_ hitobjects after its start time - for (int i = 0; i < controlPoints.Count; i++) - { - var current = controlPoints[i]; - var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null; - - // We don't need to consider any control points beyond the current time, since it will not yet - // affect any hitobjects - if (i > 0 && current.StartTime > time) - continue; - - // Duration of the current control point - var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime; - - // We want to consider the minimal amount of time that this control point has affected, - // which may be either its duration, or the amount of time that has passed within it - var durationInCurrent = Math.Min(currentDuration, time - current.StartTime); - - // Figure out how much of the time range the duration represents, and adjust it by the speed multiplier - length += durationInCurrent / timeRange * current.Multiplier; - } - - return length; - } - } -} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c9461ea504..9f7996a5fd 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - +