diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index 6c077eb214..3ebb747a21 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -1,276 +1,276 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; -using osu.Game.Tests.Visual; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Tests -{ - public class TestSceneFollowPoints : OsuTestScene - { - private Container hitObjectContainer; - private FollowPointRenderer followPointRenderer; - - [SetUp] - public void Setup() => Schedule(() => - { - Children = new Drawable[] - { - hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both }, - followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both } - }; - }); - - [Test] - public void TestAddObject() - { - addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); - - assertGroups(); - } - - [Test] - public void TestRemoveObject() - { - addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); - - removeObjectStep(() => getObject(0)); - - assertGroups(); - } - - [Test] - public void TestAddMultipleObjects() - { - addMultipleObjectsStep(); - - assertGroups(); - } - - [Test] - public void TestRemoveEndObject() - { - addMultipleObjectsStep(); - - removeObjectStep(() => getObject(4)); - - assertGroups(); - } - - [Test] - public void TestRemoveStartObject() - { - addMultipleObjectsStep(); - - removeObjectStep(() => getObject(0)); - - assertGroups(); - } - - [Test] - public void TestRemoveMiddleObject() - { - addMultipleObjectsStep(); - - removeObjectStep(() => getObject(2)); - - assertGroups(); - } - - [Test] - public void TestMoveObject() - { - addMultipleObjectsStep(); - - AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); - - assertGroups(); - } - - [TestCase(0, 0)] // Start -> Start - [TestCase(0, 2)] // Start -> Middle - [TestCase(0, 5)] // Start -> End - [TestCase(2, 0)] // Middle -> Start - [TestCase(1, 3)] // Middle -> Middle (forwards) - [TestCase(3, 1)] // Middle -> Middle (backwards) - [TestCase(4, 0)] // End -> Start - [TestCase(4, 2)] // End -> Middle - [TestCase(4, 4)] // End -> End - public void TestReorderObjects(int startIndex, int endIndex) - { - addMultipleObjectsStep(); - - reorderObjectStep(startIndex, endIndex); - - assertGroups(); - } - - [Test] - public void TestStackedObjects() - { - addObjectsStep(() => new OsuHitObject[] - { - new HitCircle { Position = new Vector2(300, 100) }, - new HitCircle - { - Position = new Vector2(300, 300), - StackHeight = 20 - }, - }); - - assertDirections(); - } - - private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[] - { - new HitCircle { Position = new Vector2(100, 100) }, - new HitCircle { Position = new Vector2(200, 200) }, - new HitCircle { Position = new Vector2(300, 300) }, - new HitCircle { Position = new Vector2(400, 400) }, - new HitCircle { Position = new Vector2(500, 500) }, - }); - - private void addObjectsStep(Func ctorFunc) - { - AddStep("add hitobjects", () => - { - var objects = ctorFunc(); - - for (int i = 0; i < objects.Length; i++) - { - objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1); - objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - DrawableOsuHitObject drawableObject = null; - - switch (objects[i]) - { - case HitCircle circle: - drawableObject = new DrawableHitCircle(circle); - break; - - case Slider slider: - drawableObject = new DrawableSlider(slider); - break; - - case Spinner spinner: - drawableObject = new DrawableSpinner(spinner); - break; - } - - hitObjectContainer.Add(drawableObject); - followPointRenderer.AddFollowPoints(objects[i]); - } - }); - } - - private void removeObjectStep(Func getFunc) - { - AddStep("remove hitobject", () => - { - var drawableObject = getFunc.Invoke(); - - hitObjectContainer.Remove(drawableObject); - followPointRenderer.RemoveFollowPoints(drawableObject.HitObject); - }); - } - - private void reorderObjectStep(int startIndex, int endIndex) - { - AddStep($"move object {startIndex} to {endIndex}", () => - { - DrawableOsuHitObject toReorder = getObject(startIndex); - - double targetTime; - if (endIndex < hitObjectContainer.Count) - targetTime = getObject(endIndex).HitObject.StartTime - 1; - else - targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1; - - hitObjectContainer.Remove(toReorder); - toReorder.HitObject.StartTime = targetTime; - hitObjectContainer.Add(toReorder); - }); - } - - private void assertGroups() - { - AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count); - AddAssert("group endpoints are correct", () => - { - for (int i = 0; i < hitObjectContainer.Count; i++) - { - DrawableOsuHitObject expectedStart = getObject(i); - DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; - - if (getGroup(i).Start != expectedStart.HitObject) - throw new AssertionException($"Object {i} expected to be the start of group {i}."); - - if (getGroup(i).End != expectedEnd?.HitObject) - throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); - } - - return true; - }); - } - - private void assertDirections() - { - AddAssert("group directions are correct", () => - { - for (int i = 0; i < hitObjectContainer.Count; i++) - { - DrawableOsuHitObject expectedStart = getObject(i); - DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; - - if (expectedEnd == null) - continue; - - var points = getGroup(i).ChildrenOfType().ToArray(); - if (points.Length == 0) - continue; - - float expectedDirection = MathF.Atan2(expectedStart.Position.Y - expectedEnd.Position.Y, expectedStart.Position.X - expectedEnd.Position.X); - float realDirection = MathF.Atan2(expectedStart.Position.Y - points[^1].Position.Y, expectedStart.Position.X - points[^1].Position.X); - - if (!Precision.AlmostEquals(expectedDirection, realDirection)) - throw new AssertionException($"Expected group {i} in direction {expectedDirection}, but was {realDirection}."); - } - - return true; - }); - } - - private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index]; - - private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index]; - - private class TestHitObjectContainer : Container - { - protected override int Compare(Drawable x, Drawable y) - { - var osuX = (DrawableOsuHitObject)x; - var osuY = (DrawableOsuHitObject)y; - - int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime); - - if (compare == 0) - return base.Compare(x, y); - - return compare; - } - } - } -} +// // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// // See the LICENCE file in the repository root for full licence text. +// +// using System; +// using System.Linq; +// using NUnit.Framework; +// using osu.Framework.Graphics; +// using osu.Framework.Graphics.Containers; +// using osu.Framework.Testing; +// using osu.Framework.Utils; +// using osu.Game.Beatmaps; +// using osu.Game.Beatmaps.ControlPoints; +// using osu.Game.Rulesets.Osu.Objects; +// using osu.Game.Rulesets.Osu.Objects.Drawables; +// using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; +// using osu.Game.Tests.Visual; +// using osuTK; +// +// namespace osu.Game.Rulesets.Osu.Tests +// { +// public class TestSceneFollowPoints : OsuTestScene +// { +// private Container hitObjectContainer; +// private FollowPointRenderer followPointRenderer; +// +// [SetUp] +// public void Setup() => Schedule(() => +// { +// Children = new Drawable[] +// { +// hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both }, +// followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both } +// }; +// }); +// +// [Test] +// public void TestAddObject() +// { +// addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); +// +// assertGroups(); +// } +// +// [Test] +// public void TestRemoveObject() +// { +// addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); +// +// removeObjectStep(() => getObject(0)); +// +// assertGroups(); +// } +// +// [Test] +// public void TestAddMultipleObjects() +// { +// addMultipleObjectsStep(); +// +// assertGroups(); +// } +// +// [Test] +// public void TestRemoveEndObject() +// { +// addMultipleObjectsStep(); +// +// removeObjectStep(() => getObject(4)); +// +// assertGroups(); +// } +// +// [Test] +// public void TestRemoveStartObject() +// { +// addMultipleObjectsStep(); +// +// removeObjectStep(() => getObject(0)); +// +// assertGroups(); +// } +// +// [Test] +// public void TestRemoveMiddleObject() +// { +// addMultipleObjectsStep(); +// +// removeObjectStep(() => getObject(2)); +// +// assertGroups(); +// } +// +// [Test] +// public void TestMoveObject() +// { +// addMultipleObjectsStep(); +// +// AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); +// +// assertGroups(); +// } +// +// [TestCase(0, 0)] // Start -> Start +// [TestCase(0, 2)] // Start -> Middle +// [TestCase(0, 5)] // Start -> End +// [TestCase(2, 0)] // Middle -> Start +// [TestCase(1, 3)] // Middle -> Middle (forwards) +// [TestCase(3, 1)] // Middle -> Middle (backwards) +// [TestCase(4, 0)] // End -> Start +// [TestCase(4, 2)] // End -> Middle +// [TestCase(4, 4)] // End -> End +// public void TestReorderObjects(int startIndex, int endIndex) +// { +// addMultipleObjectsStep(); +// +// reorderObjectStep(startIndex, endIndex); +// +// assertGroups(); +// } +// +// [Test] +// public void TestStackedObjects() +// { +// addObjectsStep(() => new OsuHitObject[] +// { +// new HitCircle { Position = new Vector2(300, 100) }, +// new HitCircle +// { +// Position = new Vector2(300, 300), +// StackHeight = 20 +// }, +// }); +// +// assertDirections(); +// } +// +// private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[] +// { +// new HitCircle { Position = new Vector2(100, 100) }, +// new HitCircle { Position = new Vector2(200, 200) }, +// new HitCircle { Position = new Vector2(300, 300) }, +// new HitCircle { Position = new Vector2(400, 400) }, +// new HitCircle { Position = new Vector2(500, 500) }, +// }); +// +// private void addObjectsStep(Func ctorFunc) +// { +// AddStep("add hitobjects", () => +// { +// var objects = ctorFunc(); +// +// for (int i = 0; i < objects.Length; i++) +// { +// objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1); +// objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); +// +// DrawableOsuHitObject drawableObject = null; +// +// switch (objects[i]) +// { +// case HitCircle circle: +// drawableObject = new DrawableHitCircle(circle); +// break; +// +// case Slider slider: +// drawableObject = new DrawableSlider(slider); +// break; +// +// case Spinner spinner: +// drawableObject = new DrawableSpinner(spinner); +// break; +// } +// +// hitObjectContainer.Add(drawableObject); +// followPointRenderer.AddFollowPoints(objects[i]); +// } +// }); +// } +// +// private void removeObjectStep(Func getFunc) +// { +// AddStep("remove hitobject", () => +// { +// var drawableObject = getFunc.Invoke(); +// +// hitObjectContainer.Remove(drawableObject); +// followPointRenderer.RemoveFollowPoints(drawableObject.HitObject); +// }); +// } +// +// private void reorderObjectStep(int startIndex, int endIndex) +// { +// AddStep($"move object {startIndex} to {endIndex}", () => +// { +// DrawableOsuHitObject toReorder = getObject(startIndex); +// +// double targetTime; +// if (endIndex < hitObjectContainer.Count) +// targetTime = getObject(endIndex).HitObject.StartTime - 1; +// else +// targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1; +// +// hitObjectContainer.Remove(toReorder); +// toReorder.HitObject.StartTime = targetTime; +// hitObjectContainer.Add(toReorder); +// }); +// } +// +// private void assertGroups() +// { +// AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count); +// AddAssert("group endpoints are correct", () => +// { +// for (int i = 0; i < hitObjectContainer.Count; i++) +// { +// DrawableOsuHitObject expectedStart = getObject(i); +// DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; +// +// if (getGroup(i).Start != expectedStart.HitObject) +// throw new AssertionException($"Object {i} expected to be the start of group {i}."); +// +// if (getGroup(i).End != expectedEnd?.HitObject) +// throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); +// } +// +// return true; +// }); +// } +// +// private void assertDirections() +// { +// AddAssert("group directions are correct", () => +// { +// for (int i = 0; i < hitObjectContainer.Count; i++) +// { +// DrawableOsuHitObject expectedStart = getObject(i); +// DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; +// +// if (expectedEnd == null) +// continue; +// +// var points = getGroup(i).ChildrenOfType().ToArray(); +// if (points.Length == 0) +// continue; +// +// float expectedDirection = MathF.Atan2(expectedStart.Position.Y - expectedEnd.Position.Y, expectedStart.Position.X - expectedEnd.Position.X); +// float realDirection = MathF.Atan2(expectedStart.Position.Y - points[^1].Position.Y, expectedStart.Position.X - points[^1].Position.X); +// +// if (!Precision.AlmostEquals(expectedDirection, realDirection)) +// throw new AssertionException($"Expected group {i} in direction {expectedDirection}, but was {realDirection}."); +// } +// +// return true; +// }); +// } +// +// private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index]; +// +// private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index]; +// +// private class TestHitObjectContainer : Container +// { +// protected override int Compare(Drawable x, Drawable y) +// { +// var osuX = (DrawableOsuHitObject)x; +// var osuY = (DrawableOsuHitObject)y; +// +// int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime); +// +// if (compare == 0) +// return base.Compare(x, y); +// +// return compare; +// } +// } +// } +// } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index a981648444..3e2ab65bb2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Game.Skinning; @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// A single follow point positioned between two adjacent s. /// - public class FollowPoint : Container, IAnimationTimeReference + public class FollowPoint : PoolableDrawable, IAnimationTimeReference { private const float width = 8; @@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { Origin = Anchor.Centre; - Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer + InternalChild = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer { Masking = true, AutoSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 3a9e19b361..1d82e91c0e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; -using JetBrains.Annotations; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects; using osuTK; @@ -15,150 +12,77 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// Visualises the s between two s. /// - public class FollowPointConnection : CompositeDrawable + public class FollowPointConnection : PoolableDrawable { // Todo: These shouldn't be constants - private const int spacing = 32; - private const double preempt = 800; + public const int SPACING = 32; + public const double PREEMPT = 800; - public override bool RemoveWhenNotAlive => false; + public FollowPointRenderer.FollowPointLifetimeEntry Entry; + public DrawablePool Pool; - /// - /// The start time of . - /// - public readonly Bindable StartTime = new BindableDouble(); - - /// - /// The which s will exit from. - /// - [NotNull] - public readonly OsuHitObject Start; - - /// - /// Creates a new . - /// - /// The which s will exit from. - public FollowPointConnection([NotNull] OsuHitObject start) + protected override void FreeAfterUse() { - Start = start; - - RelativeSizeAxes = Axes.Both; - - StartTime.BindTo(start.StartTimeBindable); + base.FreeAfterUse(); + ClearInternal(false); } - protected override void LoadComplete() + protected override void PrepareForUse() { - base.LoadComplete(); - bindEvents(Start); - } + base.PrepareForUse(); - private OsuHitObject end; + OsuHitObject start = Entry.Start; + OsuHitObject end = Entry.End; - /// - /// The which s will enter. - /// - [CanBeNull] - public OsuHitObject End - { - get => end; - set - { - end = value; + double startTime = start.GetEndTime(); - if (end != null) - bindEvents(end); - - if (IsLoaded) - scheduleRefresh(); - else - refresh(); - } - } - - private void bindEvents(OsuHitObject obj) - { - obj.PositionBindable.BindValueChanged(_ => scheduleRefresh()); - obj.DefaultsApplied += _ => scheduleRefresh(); - } - - private void scheduleRefresh() - { - Scheduler.AddOnce(refresh); - } - - private void refresh() - { - double startTime = Start.GetEndTime(); - - LifetimeStart = startTime; - - if (End == null || End.NewCombo || Start is Spinner || End is Spinner) - { - // ensure we always set a lifetime for full LifetimeManagementContainer benefits - LifetimeEnd = LifetimeStart; + if (end == null || end.NewCombo || start is Spinner || end is Spinner) return; - } - Vector2 startPosition = Start.StackedEndPosition; - Vector2 endPosition = End.StackedPosition; - double endTime = End.StartTime; + Vector2 startPosition = start.StackedEndPosition; + Vector2 endPosition = end.StackedPosition; + double endTime = end.StartTime; Vector2 distanceVector = endPosition - startPosition; int distance = (int)distanceVector.Length; float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI)); double duration = endTime - startTime; - double? firstTransformStartTime = null; double finalTransformEndTime = startTime; - int point = 0; - - ClearInternal(); - - for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing) + for (int d = (int)(SPACING * 1.5); d < distance - SPACING; d += SPACING) { float fraction = (float)d / distance; Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector; Vector2 pointEndPosition = startPosition + fraction * distanceVector; double fadeOutTime = startTime + fraction * duration; - double fadeInTime = fadeOutTime - preempt; + double fadeInTime = fadeOutTime - PREEMPT; FollowPoint fp; - AddInternal(fp = new FollowPoint()); - - Debug.Assert(End != null); + AddInternal(fp = Pool.Get()); + fp.ClearTransforms(); fp.Position = pointStartPosition; fp.Rotation = rotation; fp.Alpha = 0; - fp.Scale = new Vector2(1.5f * End.Scale); - - firstTransformStartTime ??= fadeInTime; + fp.Scale = new Vector2(1.5f * end.Scale); fp.AnimationStartTime = fadeInTime; using (fp.BeginAbsoluteSequence(fadeInTime)) { - fp.FadeIn(End.TimeFadeIn); - fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out); - fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out); - fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn); + fp.FadeIn(end.TimeFadeIn); + fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out); + fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out); + fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn); - finalTransformEndTime = fadeOutTime + End.TimeFadeIn; + finalTransformEndTime = fadeOutTime + end.TimeFadeIn; } - - point++; } - int excessPoints = InternalChildren.Count - point; - for (int i = 0; i < excessPoints; i++) - RemoveInternal(InternalChildren[^1]); - // todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed. - LifetimeStart = firstTransformStartTime ?? startTime; - LifetimeEnd = finalTransformEndTime; + Entry.LifetimeEnd = finalTransformEndTime; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index be1392d7c3..ac7b78a25b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -2,53 +2,57 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Performance; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { /// /// Visualises connections between s. /// - public class FollowPointRenderer : LifetimeManagementContainer + public class FollowPointRenderer : CompositeDrawable { - /// - /// All the s contained by this . - /// - internal IReadOnlyList Connections => connections; - - private readonly List connections = new List(); - public override bool RemoveCompletedTransforms => false; - /// - /// Adds the s around an . - /// This includes s leading into , and s exiting . - /// - /// The to add s for. - public void AddFollowPoints(OsuHitObject hitObject) - => addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g)))); + private DrawablePool connectionPool; + private DrawablePool pointPool; - /// - /// Removes the s around an . - /// This includes s leading into , and s exiting . - /// - /// The to remove s for. - public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject)); + private readonly List lifetimeEntries = new List(); + private readonly Dictionary connectionsInUse = new Dictionary(); + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - /// - /// Adds a to this . - /// - /// The to add. - /// The index of in . - private void addConnection(FollowPointConnection connection) + public FollowPointRenderer() { - // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections - int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => + lifetimeManager.EntryBecameAlive += onEntryBecameAlive; + lifetimeManager.EntryBecameDead += onEntryBecameDead; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] { - int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value); + connectionPool = new DrawablePool(1, 200), + pointPool = new DrawablePool(50, 1000) + }; + + MakeChildAlive(connectionPool); + MakeChildAlive(pointPool); + } + + public void AddFollowPoints2(OsuHitObject hitObject) + { + var newEntry = new FollowPointLifetimeEntry(hitObject); + + var index = lifetimeEntries.AddInPlace(newEntry, Comparer.Create((e1, e2) => + { + int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime); if (comp != 0) return comp; @@ -61,19 +65,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections return -1; })); - if (index < connections.Count - 1) + if (index < lifetimeEntries.Count - 1) { // Update the connection's end point to the next connection's start point // h1 -> -> -> h2 // connection nextGroup - FollowPointConnection nextConnection = connections[index + 1]; - connection.End = nextConnection.Start; + FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1]; + newEntry.End = nextEntry.Start; } else { // The end point may be non-null during re-ordering - connection.End = null; + newEntry.End = null; } if (index > 0) @@ -82,23 +86,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections // h1 -> -> -> h2 // prevGroup connection - FollowPointConnection previousConnection = connections[index - 1]; - previousConnection.End = connection.Start; + FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1]; + previousEntry.End = newEntry.Start; } - AddInternal(connection); + lifetimeManager.AddEntry(newEntry); } - /// - /// Removes a from this . - /// - /// The to remove. - /// Whether was removed. - private void removeGroup(FollowPointConnection connection) + public void RemoveFollowPoints2(OsuHitObject hitObject) { - RemoveInternal(connection); + int index = lifetimeEntries.FindIndex(e => e.Start == hitObject); + var entry = lifetimeEntries[index]; - int index = connections.IndexOf(connection); + lifetimeEntries.RemoveAt(index); + lifetimeManager.RemoveEntry(entry); if (index > 0) { @@ -106,18 +107,76 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections // h1 -> -> -> h2 -> -> -> h3 // prevGroup connection nextGroup // The current connection's end point is used since there may not be a next connection - FollowPointConnection previousConnection = connections[index - 1]; - previousConnection.End = connection.End; + FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1]; + previousEntry.End = entry.End; } - - connections.Remove(connection); } - private void onStartTimeChanged(FollowPointConnection connection) + protected override bool CheckChildrenLife() => lifetimeManager.Update(Time.Current); + + private void onEntryBecameAlive(LifetimeEntry entry) { - // Naive but can be improved if performance becomes an issue - removeGroup(connection); - addConnection(connection); + var connection = connectionPool.Get(c => + { + c.Entry = (FollowPointLifetimeEntry)entry; + c.Pool = pointPool; + }); + + connectionsInUse[entry] = connection; + + AddInternal(connection); + MakeChildAlive(connection); + } + + private void onEntryBecameDead(LifetimeEntry entry) + { + RemoveInternal(connectionsInUse[entry]); + connectionsInUse.Remove(entry); + } + + public class FollowPointLifetimeEntry : LifetimeEntry + { + public readonly OsuHitObject Start; + + public FollowPointLifetimeEntry(OsuHitObject start) + { + Start = start; + + LifetimeStart = LifetimeEnd = Start.StartTime; + } + + private OsuHitObject end; + + public OsuHitObject End + { + get => end; + set + { + end = value; + computeLifetimes(); + } + } + + private void computeLifetimes() + { + if (end == null) + { + LifetimeEnd = LifetimeStart; + return; + } + + Vector2 startPosition = Start.StackedEndPosition; + Vector2 endPosition = End.StackedPosition; + Vector2 distanceVector = endPosition - startPosition; + float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length; + + double duration = End.StartTime - Start.GetEndTime(); + + double fadeOutTime = Start.StartTime + fraction * duration; + double fadeInTime = fadeOutTime - FollowPointConnection.PREEMPT; + + LifetimeStart = fadeInTime; + } } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index c816502d61..a8d9423bf6 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -125,13 +125,13 @@ namespace osu.Game.Rulesets.Osu.UI protected override void OnHitObjectAdded(HitObject hitObject) { base.OnHitObjectAdded(hitObject); - followPoints.AddFollowPoints((OsuHitObject)hitObject); + followPoints.AddFollowPoints2((OsuHitObject)hitObject); } protected override void OnHitObjectRemoved(HitObject hitObject) { base.OnHitObjectRemoved(hitObject); - followPoints.RemoveFollowPoints((OsuHitObject)hitObject); + followPoints.RemoveFollowPoints2((OsuHitObject)hitObject); } public void OnHitObjectLoaded(Drawable drawable)