Add initial followpoint pooling implementation

This commit is contained in:
smoogipoo 2020-11-20 00:11:31 +09:00
parent 7f3c8ad744
commit d467a00eea
5 changed files with 423 additions and 439 deletions

View File

@ -1,276 +1,276 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // // See the LICENCE file in the repository root for full licence text.
//
using System; // using System;
using System.Linq; // using System.Linq;
using NUnit.Framework; // using NUnit.Framework;
using osu.Framework.Graphics; // using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; // using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; // using osu.Framework.Testing;
using osu.Framework.Utils; // using osu.Framework.Utils;
using osu.Game.Beatmaps; // using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; // using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects; // using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; // using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; // using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Tests.Visual; // using osu.Game.Tests.Visual;
using osuTK; // using osuTK;
//
namespace osu.Game.Rulesets.Osu.Tests // namespace osu.Game.Rulesets.Osu.Tests
{ // {
public class TestSceneFollowPoints : OsuTestScene // public class TestSceneFollowPoints : OsuTestScene
{ // {
private Container<DrawableOsuHitObject> hitObjectContainer; // private Container<DrawableOsuHitObject> hitObjectContainer;
private FollowPointRenderer followPointRenderer; // private FollowPointRenderer followPointRenderer;
//
[SetUp] // [SetUp]
public void Setup() => Schedule(() => // public void Setup() => Schedule(() =>
{ // {
Children = new Drawable[] // Children = new Drawable[]
{ // {
hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both }, // hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both },
followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both } // followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }
}; // };
}); // });
//
[Test] // [Test]
public void TestAddObject() // public void TestAddObject()
{ // {
addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); // addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
//
assertGroups(); // assertGroups();
} // }
//
[Test] // [Test]
public void TestRemoveObject() // public void TestRemoveObject()
{ // {
addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); // addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
//
removeObjectStep(() => getObject(0)); // removeObjectStep(() => getObject(0));
//
assertGroups(); // assertGroups();
} // }
//
[Test] // [Test]
public void TestAddMultipleObjects() // public void TestAddMultipleObjects()
{ // {
addMultipleObjectsStep(); // addMultipleObjectsStep();
//
assertGroups(); // assertGroups();
} // }
//
[Test] // [Test]
public void TestRemoveEndObject() // public void TestRemoveEndObject()
{ // {
addMultipleObjectsStep(); // addMultipleObjectsStep();
//
removeObjectStep(() => getObject(4)); // removeObjectStep(() => getObject(4));
//
assertGroups(); // assertGroups();
} // }
//
[Test] // [Test]
public void TestRemoveStartObject() // public void TestRemoveStartObject()
{ // {
addMultipleObjectsStep(); // addMultipleObjectsStep();
//
removeObjectStep(() => getObject(0)); // removeObjectStep(() => getObject(0));
//
assertGroups(); // assertGroups();
} // }
//
[Test] // [Test]
public void TestRemoveMiddleObject() // public void TestRemoveMiddleObject()
{ // {
addMultipleObjectsStep(); // addMultipleObjectsStep();
//
removeObjectStep(() => getObject(2)); // removeObjectStep(() => getObject(2));
//
assertGroups(); // assertGroups();
} // }
//
[Test] // [Test]
public void TestMoveObject() // public void TestMoveObject()
{ // {
addMultipleObjectsStep(); // addMultipleObjectsStep();
//
AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); // AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100));
//
assertGroups(); // assertGroups();
} // }
//
[TestCase(0, 0)] // Start -> Start // [TestCase(0, 0)] // Start -> Start
[TestCase(0, 2)] // Start -> Middle // [TestCase(0, 2)] // Start -> Middle
[TestCase(0, 5)] // Start -> End // [TestCase(0, 5)] // Start -> End
[TestCase(2, 0)] // Middle -> Start // [TestCase(2, 0)] // Middle -> Start
[TestCase(1, 3)] // Middle -> Middle (forwards) // [TestCase(1, 3)] // Middle -> Middle (forwards)
[TestCase(3, 1)] // Middle -> Middle (backwards) // [TestCase(3, 1)] // Middle -> Middle (backwards)
[TestCase(4, 0)] // End -> Start // [TestCase(4, 0)] // End -> Start
[TestCase(4, 2)] // End -> Middle // [TestCase(4, 2)] // End -> Middle
[TestCase(4, 4)] // End -> End // [TestCase(4, 4)] // End -> End
public void TestReorderObjects(int startIndex, int endIndex) // public void TestReorderObjects(int startIndex, int endIndex)
{ // {
addMultipleObjectsStep(); // addMultipleObjectsStep();
//
reorderObjectStep(startIndex, endIndex); // reorderObjectStep(startIndex, endIndex);
//
assertGroups(); // assertGroups();
} // }
//
[Test] // [Test]
public void TestStackedObjects() // public void TestStackedObjects()
{ // {
addObjectsStep(() => new OsuHitObject[] // addObjectsStep(() => new OsuHitObject[]
{ // {
new HitCircle { Position = new Vector2(300, 100) }, // new HitCircle { Position = new Vector2(300, 100) },
new HitCircle // new HitCircle
{ // {
Position = new Vector2(300, 300), // Position = new Vector2(300, 300),
StackHeight = 20 // StackHeight = 20
}, // },
}); // });
//
assertDirections(); // assertDirections();
} // }
//
private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[] // private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[]
{ // {
new HitCircle { Position = new Vector2(100, 100) }, // new HitCircle { Position = new Vector2(100, 100) },
new HitCircle { Position = new Vector2(200, 200) }, // new HitCircle { Position = new Vector2(200, 200) },
new HitCircle { Position = new Vector2(300, 300) }, // new HitCircle { Position = new Vector2(300, 300) },
new HitCircle { Position = new Vector2(400, 400) }, // new HitCircle { Position = new Vector2(400, 400) },
new HitCircle { Position = new Vector2(500, 500) }, // new HitCircle { Position = new Vector2(500, 500) },
}); // });
//
private void addObjectsStep(Func<OsuHitObject[]> ctorFunc) // private void addObjectsStep(Func<OsuHitObject[]> ctorFunc)
{ // {
AddStep("add hitobjects", () => // AddStep("add hitobjects", () =>
{ // {
var objects = ctorFunc(); // var objects = ctorFunc();
//
for (int i = 0; i < objects.Length; i++) // for (int i = 0; i < objects.Length; i++)
{ // {
objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1); // objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1);
objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); // objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
//
DrawableOsuHitObject drawableObject = null; // DrawableOsuHitObject drawableObject = null;
//
switch (objects[i]) // switch (objects[i])
{ // {
case HitCircle circle: // case HitCircle circle:
drawableObject = new DrawableHitCircle(circle); // drawableObject = new DrawableHitCircle(circle);
break; // break;
//
case Slider slider: // case Slider slider:
drawableObject = new DrawableSlider(slider); // drawableObject = new DrawableSlider(slider);
break; // break;
//
case Spinner spinner: // case Spinner spinner:
drawableObject = new DrawableSpinner(spinner); // drawableObject = new DrawableSpinner(spinner);
break; // break;
} // }
//
hitObjectContainer.Add(drawableObject); // hitObjectContainer.Add(drawableObject);
followPointRenderer.AddFollowPoints(objects[i]); // followPointRenderer.AddFollowPoints(objects[i]);
} // }
}); // });
} // }
//
private void removeObjectStep(Func<DrawableOsuHitObject> getFunc) // private void removeObjectStep(Func<DrawableOsuHitObject> getFunc)
{ // {
AddStep("remove hitobject", () => // AddStep("remove hitobject", () =>
{ // {
var drawableObject = getFunc.Invoke(); // var drawableObject = getFunc.Invoke();
//
hitObjectContainer.Remove(drawableObject); // hitObjectContainer.Remove(drawableObject);
followPointRenderer.RemoveFollowPoints(drawableObject.HitObject); // followPointRenderer.RemoveFollowPoints(drawableObject.HitObject);
}); // });
} // }
//
private void reorderObjectStep(int startIndex, int endIndex) // private void reorderObjectStep(int startIndex, int endIndex)
{ // {
AddStep($"move object {startIndex} to {endIndex}", () => // AddStep($"move object {startIndex} to {endIndex}", () =>
{ // {
DrawableOsuHitObject toReorder = getObject(startIndex); // DrawableOsuHitObject toReorder = getObject(startIndex);
//
double targetTime; // double targetTime;
if (endIndex < hitObjectContainer.Count) // if (endIndex < hitObjectContainer.Count)
targetTime = getObject(endIndex).HitObject.StartTime - 1; // targetTime = getObject(endIndex).HitObject.StartTime - 1;
else // else
targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1; // targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1;
//
hitObjectContainer.Remove(toReorder); // hitObjectContainer.Remove(toReorder);
toReorder.HitObject.StartTime = targetTime; // toReorder.HitObject.StartTime = targetTime;
hitObjectContainer.Add(toReorder); // hitObjectContainer.Add(toReorder);
}); // });
} // }
//
private void assertGroups() // private void assertGroups()
{ // {
AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count); // AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count);
AddAssert("group endpoints are correct", () => // AddAssert("group endpoints are correct", () =>
{ // {
for (int i = 0; i < hitObjectContainer.Count; i++) // for (int i = 0; i < hitObjectContainer.Count; i++)
{ // {
DrawableOsuHitObject expectedStart = getObject(i); // DrawableOsuHitObject expectedStart = getObject(i);
DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; // DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
//
if (getGroup(i).Start != expectedStart.HitObject) // if (getGroup(i).Start != expectedStart.HitObject)
throw new AssertionException($"Object {i} expected to be the start of group {i}."); // throw new AssertionException($"Object {i} expected to be the start of group {i}.");
//
if (getGroup(i).End != expectedEnd?.HitObject) // if (getGroup(i).End != expectedEnd?.HitObject)
throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); // throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
} // }
//
return true; // return true;
}); // });
} // }
//
private void assertDirections() // private void assertDirections()
{ // {
AddAssert("group directions are correct", () => // AddAssert("group directions are correct", () =>
{ // {
for (int i = 0; i < hitObjectContainer.Count; i++) // for (int i = 0; i < hitObjectContainer.Count; i++)
{ // {
DrawableOsuHitObject expectedStart = getObject(i); // DrawableOsuHitObject expectedStart = getObject(i);
DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; // DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
//
if (expectedEnd == null) // if (expectedEnd == null)
continue; // continue;
//
var points = getGroup(i).ChildrenOfType<FollowPoint>().ToArray(); // var points = getGroup(i).ChildrenOfType<FollowPoint>().ToArray();
if (points.Length == 0) // if (points.Length == 0)
continue; // continue;
//
float expectedDirection = MathF.Atan2(expectedStart.Position.Y - expectedEnd.Position.Y, expectedStart.Position.X - expectedEnd.Position.X); // 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); // float realDirection = MathF.Atan2(expectedStart.Position.Y - points[^1].Position.Y, expectedStart.Position.X - points[^1].Position.X);
//
if (!Precision.AlmostEquals(expectedDirection, realDirection)) // if (!Precision.AlmostEquals(expectedDirection, realDirection))
throw new AssertionException($"Expected group {i} in direction {expectedDirection}, but was {realDirection}."); // throw new AssertionException($"Expected group {i} in direction {expectedDirection}, but was {realDirection}.");
} // }
//
return true; // return true;
}); // });
} // }
//
private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index]; // private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
//
private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index]; // private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
//
private class TestHitObjectContainer : Container<DrawableOsuHitObject> // private class TestHitObjectContainer : Container<DrawableOsuHitObject>
{ // {
protected override int Compare(Drawable x, Drawable y) // protected override int Compare(Drawable x, Drawable y)
{ // {
var osuX = (DrawableOsuHitObject)x; // var osuX = (DrawableOsuHitObject)x;
var osuY = (DrawableOsuHitObject)y; // var osuY = (DrawableOsuHitObject)y;
//
int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime); // int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime);
//
if (compare == 0) // if (compare == 0)
return base.Compare(x, y); // return base.Compare(x, y);
//
return compare; // return compare;
} // }
} // }
} // }
} // }

View File

@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary> /// <summary>
/// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s. /// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPoint : Container, IAnimationTimeReference public class FollowPoint : PoolableDrawable, IAnimationTimeReference
{ {
private const float width = 8; private const float width = 8;
@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
Origin = Anchor.Centre; Origin = Anchor.Centre;
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer InternalChild = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer
{ {
Masking = true, Masking = true,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,

View File

@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osuTK; using osuTK;
@ -15,150 +12,77 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
/// <summary> /// <summary>
/// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s. /// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointConnection : CompositeDrawable public class FollowPointConnection : PoolableDrawable
{ {
// Todo: These shouldn't be constants // Todo: These shouldn't be constants
private const int spacing = 32; public const int SPACING = 32;
private const double preempt = 800; public const double PREEMPT = 800;
public override bool RemoveWhenNotAlive => false; public FollowPointRenderer.FollowPointLifetimeEntry Entry;
public DrawablePool<FollowPoint> Pool;
/// <summary> protected override void FreeAfterUse()
/// The start time of <see cref="Start"/>.
/// </summary>
public readonly Bindable<double> StartTime = new BindableDouble();
/// <summary>
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.
/// </summary>
[NotNull]
public readonly OsuHitObject Start;
/// <summary>
/// Creates a new <see cref="FollowPointConnection"/>.
/// </summary>
/// <param name="start">The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.</param>
public FollowPointConnection([NotNull] OsuHitObject start)
{ {
Start = start; base.FreeAfterUse();
ClearInternal(false);
RelativeSizeAxes = Axes.Both;
StartTime.BindTo(start.StartTimeBindable);
} }
protected override void LoadComplete() protected override void PrepareForUse()
{ {
base.LoadComplete(); base.PrepareForUse();
bindEvents(Start);
}
private OsuHitObject end; OsuHitObject start = Entry.Start;
OsuHitObject end = Entry.End;
/// <summary> double startTime = start.GetEndTime();
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will enter.
/// </summary>
[CanBeNull]
public OsuHitObject End
{
get => end;
set
{
end = value;
if (end != null) if (end == null || end.NewCombo || start is Spinner || end is Spinner)
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;
return; return;
}
Vector2 startPosition = Start.StackedEndPosition; Vector2 startPosition = start.StackedEndPosition;
Vector2 endPosition = End.StackedPosition; Vector2 endPosition = end.StackedPosition;
double endTime = End.StartTime; double endTime = end.StartTime;
Vector2 distanceVector = endPosition - startPosition; Vector2 distanceVector = endPosition - startPosition;
int distance = (int)distanceVector.Length; int distance = (int)distanceVector.Length;
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI)); float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
double duration = endTime - startTime; double duration = endTime - startTime;
double? firstTransformStartTime = null;
double finalTransformEndTime = startTime; double finalTransformEndTime = startTime;
int point = 0; for (int d = (int)(SPACING * 1.5); d < distance - SPACING; d += SPACING)
ClearInternal();
for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
{ {
float fraction = (float)d / distance; float fraction = (float)d / distance;
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector; Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
Vector2 pointEndPosition = startPosition + fraction * distanceVector; Vector2 pointEndPosition = startPosition + fraction * distanceVector;
double fadeOutTime = startTime + fraction * duration; double fadeOutTime = startTime + fraction * duration;
double fadeInTime = fadeOutTime - preempt; double fadeInTime = fadeOutTime - PREEMPT;
FollowPoint fp; FollowPoint fp;
AddInternal(fp = new FollowPoint()); AddInternal(fp = Pool.Get());
Debug.Assert(End != null);
fp.ClearTransforms();
fp.Position = pointStartPosition; fp.Position = pointStartPosition;
fp.Rotation = rotation; fp.Rotation = rotation;
fp.Alpha = 0; fp.Alpha = 0;
fp.Scale = new Vector2(1.5f * End.Scale); fp.Scale = new Vector2(1.5f * end.Scale);
firstTransformStartTime ??= fadeInTime;
fp.AnimationStartTime = fadeInTime; fp.AnimationStartTime = fadeInTime;
using (fp.BeginAbsoluteSequence(fadeInTime)) using (fp.BeginAbsoluteSequence(fadeInTime))
{ {
fp.FadeIn(End.TimeFadeIn); fp.FadeIn(end.TimeFadeIn);
fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out); fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out);
fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out); fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out);
fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn); 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. // todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed.
LifetimeStart = firstTransformStartTime ?? startTime; Entry.LifetimeEnd = finalTransformEndTime;
LifetimeEnd = finalTransformEndTime;
} }
} }
} }

View File

@ -2,53 +2,57 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; 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 namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
/// <summary> /// <summary>
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s. /// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
/// </summary> /// </summary>
public class FollowPointRenderer : LifetimeManagementContainer public class FollowPointRenderer : CompositeDrawable
{ {
/// <summary>
/// All the <see cref="FollowPointConnection"/>s contained by this <see cref="FollowPointRenderer"/>.
/// </summary>
internal IReadOnlyList<FollowPointConnection> Connections => connections;
private readonly List<FollowPointConnection> connections = new List<FollowPointConnection>();
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
/// <summary> private DrawablePool<FollowPointConnection> connectionPool;
/// Adds the <see cref="FollowPoint"/>s around an <see cref="OsuHitObject"/>. private DrawablePool<FollowPoint> pointPool;
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="OsuHitObject"/> to add <see cref="FollowPoint"/>s for.</param>
public void AddFollowPoints(OsuHitObject hitObject)
=> addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
/// <summary> private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>();
/// Removes the <see cref="FollowPoint"/>s around an <see cref="OsuHitObject"/>. private readonly Dictionary<LifetimeEntry, FollowPointConnection> connectionsInUse = new Dictionary<LifetimeEntry, FollowPointConnection>();
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>. private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
/// </summary>
/// <param name="hitObject">The <see cref="OsuHitObject"/> to remove <see cref="FollowPoint"/>s for.</param>
public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
/// <summary> public FollowPointRenderer()
/// Adds a <see cref="FollowPointConnection"/> to this <see cref="FollowPointRenderer"/>.
/// </summary>
/// <param name="connection">The <see cref="FollowPointConnection"/> to add.</param>
/// <returns>The index of <paramref name="connection"/> in <see cref="connections"/>.</returns>
private void addConnection(FollowPointConnection connection)
{ {
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) => lifetimeManager.EntryBecameDead += onEntryBecameDead;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{ {
int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value); connectionPool = new DrawablePool<FollowPointConnection>(1, 200),
pointPool = new DrawablePool<FollowPoint>(50, 1000)
};
MakeChildAlive(connectionPool);
MakeChildAlive(pointPool);
}
public void AddFollowPoints2(OsuHitObject hitObject)
{
var newEntry = new FollowPointLifetimeEntry(hitObject);
var index = lifetimeEntries.AddInPlace(newEntry, Comparer<FollowPointLifetimeEntry>.Create((e1, e2) =>
{
int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime);
if (comp != 0) if (comp != 0)
return comp; return comp;
@ -61,19 +65,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
return -1; 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 // Update the connection's end point to the next connection's start point
// h1 -> -> -> h2 // h1 -> -> -> h2
// connection nextGroup // connection nextGroup
FollowPointConnection nextConnection = connections[index + 1]; FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1];
connection.End = nextConnection.Start; newEntry.End = nextEntry.Start;
} }
else else
{ {
// The end point may be non-null during re-ordering // The end point may be non-null during re-ordering
connection.End = null; newEntry.End = null;
} }
if (index > 0) if (index > 0)
@ -82,23 +86,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2 // h1 -> -> -> h2
// prevGroup connection // prevGroup connection
FollowPointConnection previousConnection = connections[index - 1]; FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
previousConnection.End = connection.Start; previousEntry.End = newEntry.Start;
} }
AddInternal(connection); lifetimeManager.AddEntry(newEntry);
} }
/// <summary> public void RemoveFollowPoints2(OsuHitObject hitObject)
/// Removes a <see cref="FollowPointConnection"/> from this <see cref="FollowPointRenderer"/>.
/// </summary>
/// <param name="connection">The <see cref="FollowPointConnection"/> to remove.</param>
/// <returns>Whether <paramref name="connection"/> was removed.</returns>
private void removeGroup(FollowPointConnection connection)
{ {
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) if (index > 0)
{ {
@ -106,18 +107,76 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
// h1 -> -> -> h2 -> -> -> h3 // h1 -> -> -> h2 -> -> -> h3
// prevGroup connection nextGroup // prevGroup connection nextGroup
// The current connection's end point is used since there may not be a next connection // The current connection's end point is used since there may not be a next connection
FollowPointConnection previousConnection = connections[index - 1]; FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
previousConnection.End = connection.End; 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 var connection = connectionPool.Get(c =>
removeGroup(connection); {
addConnection(connection); 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;
}
} }
} }
} }

View File

@ -125,13 +125,13 @@ namespace osu.Game.Rulesets.Osu.UI
protected override void OnHitObjectAdded(HitObject hitObject) protected override void OnHitObjectAdded(HitObject hitObject)
{ {
base.OnHitObjectAdded(hitObject); base.OnHitObjectAdded(hitObject);
followPoints.AddFollowPoints((OsuHitObject)hitObject); followPoints.AddFollowPoints2((OsuHitObject)hitObject);
} }
protected override void OnHitObjectRemoved(HitObject hitObject) protected override void OnHitObjectRemoved(HitObject hitObject)
{ {
base.OnHitObjectRemoved(hitObject); base.OnHitObjectRemoved(hitObject);
followPoints.RemoveFollowPoints((OsuHitObject)hitObject); followPoints.RemoveFollowPoints2((OsuHitObject)hitObject);
} }
public void OnHitObjectLoaded(Drawable drawable) public void OnHitObjectLoaded(Drawable drawable)