1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-06 11:27:24 +08:00

Merge pull request #10923 from smoogipoo/followpoint-pooling

Implement follow point pooling
This commit is contained in:
Dean Herbert 2020-11-20 20:53:20 +09:00 committed by GitHub
commit 649013728c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 298 additions and 164 deletions

View File

@ -7,6 +7,7 @@ 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.Timing;
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;
@ -94,9 +95,19 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
addMultipleObjectsStep(); addMultipleObjectsStep();
AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); AddStep("move hitobject", () =>
{
var manualClock = new ManualClock();
followPointRenderer.Clock = new FramedClock(manualClock);
manualClock.CurrentTime = getObject(1).HitObject.StartTime;
followPointRenderer.UpdateSubTree();
getObject(2).HitObject.Position = new Vector2(300, 100);
});
assertGroups(); assertGroups();
assertDirections();
} }
[TestCase(0, 0)] // Start -> Start [TestCase(0, 0)] // Start -> Start
@ -207,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void assertGroups() private void assertGroups()
{ {
AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count); AddAssert("has correct group count", () => followPointRenderer.Entries.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++)
@ -215,10 +226,10 @@ namespace osu.Game.Rulesets.Osu.Tests
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 (getEntry(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 (getEntry(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}.");
} }
@ -238,6 +249,12 @@ namespace osu.Game.Rulesets.Osu.Tests
if (expectedEnd == null) if (expectedEnd == null)
continue; continue;
var manualClock = new ManualClock();
followPointRenderer.Clock = new FramedClock(manualClock);
manualClock.CurrentTime = expectedStart.HitObject.StartTime;
followPointRenderer.UpdateSubTree();
var points = getGroup(i).ChildrenOfType<FollowPoint>().ToArray(); var points = getGroup(i).ChildrenOfType<FollowPoint>().ToArray();
if (points.Length == 0) if (points.Length == 0)
continue; continue;
@ -255,7 +272,9 @@ namespace osu.Game.Rulesets.Osu.Tests
private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index]; private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index]; private FollowPointLifetimeEntry getEntry(int index) => followPointRenderer.Entries[index];
private FollowPointConnection getGroup(int index) => followPointRenderer.ChildrenOfType<FollowPointConnection>().Single(c => c.Entry == getEntry(index));
private class TestHitObjectContainer : Container<DrawableOsuHitObject> private class TestHitObjectContainer : Container<DrawableOsuHitObject>
{ {

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,106 @@ 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 FollowPointLifetimeEntry Entry;
public DrawablePool<FollowPoint> Pool;
/// <summary> protected override void PrepareForUse()
/// 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.PrepareForUse();
RelativeSizeAxes = Axes.Both; Entry.Invalidated += onEntryInvalidated;
StartTime.BindTo(start.StartTimeBindable); refreshPoints();
} }
protected override void LoadComplete() protected override void FreeAfterUse()
{ {
base.LoadComplete(); base.FreeAfterUse();
bindEvents(Start);
Entry.Invalidated -= onEntryInvalidated;
// Return points to the pool.
ClearInternal(false);
Entry = null;
} }
private OsuHitObject end; private void onEntryInvalidated() => refreshPoints();
/// <summary> private void refreshPoints()
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will enter.
/// </summary>
[CanBeNull]
public OsuHitObject End
{ {
get => end; ClearInternal(false);
set
{
end = value;
if (end != null) OsuHitObject start = Entry.Start;
bindEvents(end); OsuHitObject end = Entry.End;
if (IsLoaded) double startTime = start.GetEndTime();
scheduleRefresh();
else
refresh();
}
}
private void bindEvents(OsuHitObject obj) Vector2 startPosition = start.StackedEndPosition;
{ Vector2 endPosition = end.StackedPosition;
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;
}
Vector2 startPosition = Start.StackedEndPosition;
Vector2 endPosition = End.StackedPosition;
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? 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 fadeInTime = fadeOutTime - preempt; GetFadeTimes(start, end, (float)d / distance, out var fadeInTime, out var fadeOutTime);
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; }
/// <summary>
/// Computes the fade time of follow point positioned between two hitobjects.
/// </summary>
/// <param name="start">The first <see cref="OsuHitObject"/>, where follow points should originate from.</param>
/// <param name="end">The second <see cref="OsuHitObject"/>, which follow points should target.</param>
/// <param name="fraction">The fractional distance along <paramref name="start"/> and <paramref name="end"/> at which the follow point is to be located.</param>
/// <param name="fadeInTime">The fade-in time of the follow point/</param>
/// <param name="fadeOutTime">The fade-out time of the follow point.</param>
public static void GetFadeTimes(OsuHitObject start, OsuHitObject end, float fraction, out double fadeInTime, out double fadeOutTime)
{
double startTime = start.GetEndTime();
double duration = end.StartTime - startTime;
fadeOutTime = startTime + fraction * duration;
fadeInTime = fadeOutTime - PREEMPT;
} }
} }
} }

View File

@ -0,0 +1,98 @@
// 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.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{
public class FollowPointLifetimeEntry : LifetimeEntry
{
public event Action Invalidated;
public readonly OsuHitObject Start;
public FollowPointLifetimeEntry(OsuHitObject start)
{
Start = start;
LifetimeStart = Start.StartTime;
bindEvents();
}
private OsuHitObject end;
public OsuHitObject End
{
get => end;
set
{
UnbindEvents();
end = value;
bindEvents();
refreshLifetimes();
}
}
private void bindEvents()
{
UnbindEvents();
// Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects.
Start.DefaultsApplied += onDefaultsApplied;
Start.PositionBindable.ValueChanged += onPositionChanged;
if (End != null)
{
End.DefaultsApplied += onDefaultsApplied;
End.PositionBindable.ValueChanged += onPositionChanged;
}
}
public void UnbindEvents()
{
if (Start != null)
{
Start.DefaultsApplied -= onDefaultsApplied;
Start.PositionBindable.ValueChanged -= onPositionChanged;
}
if (End != null)
{
End.DefaultsApplied -= onDefaultsApplied;
End.PositionBindable.ValueChanged -= onPositionChanged;
}
}
private void onDefaultsApplied(HitObject obj) => refreshLifetimes();
private void onPositionChanged(ValueChangedEvent<Vector2> obj) => refreshLifetimes();
private void refreshLifetimes()
{
if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
{
LifetimeEnd = LifetimeStart;
return;
}
Vector2 startPosition = Start.StackedEndPosition;
Vector2 endPosition = End.StackedPosition;
Vector2 distanceVector = endPosition - startPosition;
// The lifetime start will match the fade-in time of the first follow point.
float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length;
FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _);
LifetimeStart = fadeInTime;
LifetimeEnd = double.MaxValue; // This will be set by the connection.
Invalidated?.Invoke();
}
}
}

View File

@ -2,53 +2,74 @@
// 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.Bindables;
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;
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> public IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
/// Adds the <see cref="FollowPoint"/>s around an <see cref="OsuHitObject"/>.
/// 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 DrawablePool<FollowPointConnection> connectionPool;
/// Removes 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 remove <see cref="FollowPoint"/>s for.</param>
public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
/// <summary> private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>();
/// Adds a <see cref="FollowPointConnection"/> to this <see cref="FollowPointRenderer"/>. private readonly Dictionary<LifetimeEntry, FollowPointConnection> connectionsInUse = new Dictionary<LifetimeEntry, FollowPointConnection>();
/// </summary> private readonly Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>();
/// <param name="connection">The <see cref="FollowPointConnection"/> to add.</param> private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
/// <returns>The index of <paramref name="connection"/> in <see cref="connections"/>.</returns>
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 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 DrawablePoolNoLifetime<FollowPointConnection>(1, 200),
pointPool = new DrawablePoolNoLifetime<FollowPoint>(50, 1000)
};
}
public void AddFollowPoints(OsuHitObject hitObject)
{
addEntry(hitObject);
var startTimeBindable = hitObject.StartTimeBindable.GetBoundCopy();
startTimeBindable.ValueChanged += _ => onStartTimeChanged(hitObject);
startTimeMap[hitObject] = startTimeBindable;
}
public void RemoveFollowPoints(OsuHitObject hitObject)
{
removeEntry(hitObject);
startTimeMap[hitObject].UnbindAll();
startTimeMap.Remove(hitObject);
}
private void addEntry(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 +82,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 +103,22 @@ 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> private void removeEntry(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);
int index = connections.IndexOf(connection); var entry = lifetimeEntries[index];
entry.UnbindEvents();
lifetimeEntries.RemoveAt(index);
lifetimeManager.RemoveEntry(entry);
if (index > 0) if (index > 0)
{ {
@ -106,18 +126,61 @@ 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()
{ {
// Naive but can be improved if performance becomes an issue bool anyAliveChanged = base.CheckChildrenLife();
removeGroup(connection); anyAliveChanged |= lifetimeManager.Update(Time.Current);
addConnection(connection); return anyAliveChanged;
}
private void onEntryBecameAlive(LifetimeEntry entry)
{
var connection = connectionPool.Get(c =>
{
c.Entry = (FollowPointLifetimeEntry)entry;
c.Pool = pointPool;
});
connectionsInUse[entry] = connection;
AddInternal(connection);
}
private void onEntryBecameDead(LifetimeEntry entry)
{
RemoveInternal(connectionsInUse[entry]);
connectionsInUse.Remove(entry);
}
private void onStartTimeChanged(OsuHitObject hitObject)
{
removeEntry(hitObject);
addEntry(hitObject);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
foreach (var entry in lifetimeEntries)
entry.UnbindEvents();
lifetimeEntries.Clear();
}
private class DrawablePoolNoLifetime<T> : DrawablePool<T>
where T : PoolableDrawable, new()
{
public override bool RemoveWhenNotAlive => false;
public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
}
} }
} }
} }