2019-01-24 16:43:03 +08:00
|
|
|
|
// 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.
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-11-20 14:31:04 +08:00
|
|
|
|
using System;
|
2019-11-01 18:22:07 +08:00
|
|
|
|
using System.Collections.Generic;
|
2020-11-19 23:11:31 +08:00
|
|
|
|
using osu.Framework.Allocation;
|
2020-11-20 13:10:28 +08:00
|
|
|
|
using osu.Framework.Bindables;
|
2019-11-01 18:22:07 +08:00
|
|
|
|
using osu.Framework.Extensions;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
using osu.Framework.Graphics;
|
2019-11-01 14:34:24 +08:00
|
|
|
|
using osu.Framework.Graphics.Containers;
|
2020-11-19 23:11:31 +08:00
|
|
|
|
using osu.Framework.Graphics.Performance;
|
|
|
|
|
using osu.Framework.Graphics.Pooling;
|
|
|
|
|
using osu.Game.Rulesets.Objects;
|
|
|
|
|
using osuTK;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
|
|
|
|
{
|
2019-11-05 22:20:46 +08:00
|
|
|
|
/// <summary>
|
2019-11-06 15:33:42 +08:00
|
|
|
|
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
|
2019-11-05 22:20:46 +08:00
|
|
|
|
/// </summary>
|
2020-11-19 23:11:31 +08:00
|
|
|
|
public class FollowPointRenderer : CompositeDrawable
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2020-11-19 23:11:31 +08:00
|
|
|
|
public override bool RemoveCompletedTransforms => false;
|
2019-11-05 18:31:58 +08:00
|
|
|
|
|
2020-11-20 12:54:41 +08:00
|
|
|
|
public IReadOnlyList<FollowPointLifetimeEntry> Entries => lifetimeEntries;
|
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
private DrawablePool<FollowPointConnection> connectionPool;
|
|
|
|
|
private DrawablePool<FollowPoint> pointPool;
|
2019-11-01 18:22:07 +08:00
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
private readonly List<FollowPointLifetimeEntry> lifetimeEntries = new List<FollowPointLifetimeEntry>();
|
|
|
|
|
private readonly Dictionary<LifetimeEntry, FollowPointConnection> connectionsInUse = new Dictionary<LifetimeEntry, FollowPointConnection>();
|
2020-11-20 13:10:28 +08:00
|
|
|
|
private readonly Dictionary<HitObject, IBindable> startTimeMap = new Dictionary<HitObject, IBindable>();
|
2020-11-19 23:11:31 +08:00
|
|
|
|
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
|
|
|
|
|
|
|
|
|
|
public FollowPointRenderer()
|
|
|
|
|
{
|
|
|
|
|
lifetimeManager.EntryBecameAlive += onEntryBecameAlive;
|
|
|
|
|
lifetimeManager.EntryBecameDead += onEntryBecameDead;
|
|
|
|
|
}
|
2019-11-05 22:02:39 +08:00
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
[BackgroundDependencyLoader]
|
|
|
|
|
private void load()
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2020-11-19 23:11:31 +08:00
|
|
|
|
InternalChildren = new Drawable[]
|
2020-09-23 14:41:43 +08:00
|
|
|
|
{
|
2020-11-19 23:11:31 +08:00
|
|
|
|
connectionPool = new DrawablePool<FollowPointConnection>(1, 200),
|
|
|
|
|
pointPool = new DrawablePool<FollowPoint>(50, 1000)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
MakeChildAlive(connectionPool);
|
|
|
|
|
MakeChildAlive(pointPool);
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-20 14:39:25 +08:00
|
|
|
|
public void AddFollowPoints(OsuHitObject hitObject)
|
2020-11-20 13:10:28 +08:00
|
|
|
|
{
|
|
|
|
|
addEntry(hitObject);
|
|
|
|
|
|
|
|
|
|
var startTimeBindable = hitObject.StartTimeBindable.GetBoundCopy();
|
|
|
|
|
startTimeBindable.ValueChanged += _ => onStartTimeChanged(hitObject);
|
|
|
|
|
startTimeMap[hitObject] = startTimeBindable;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-20 14:39:25 +08:00
|
|
|
|
public void RemoveFollowPoints(OsuHitObject hitObject)
|
2020-11-20 13:10:28 +08:00
|
|
|
|
{
|
|
|
|
|
removeEntry(hitObject);
|
|
|
|
|
|
|
|
|
|
startTimeMap[hitObject].UnbindAll();
|
|
|
|
|
startTimeMap.Remove(hitObject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void addEntry(OsuHitObject hitObject)
|
2020-11-19 23:11:31 +08:00
|
|
|
|
{
|
|
|
|
|
var newEntry = new FollowPointLifetimeEntry(hitObject);
|
|
|
|
|
|
|
|
|
|
var index = lifetimeEntries.AddInPlace(newEntry, Comparer<FollowPointLifetimeEntry>.Create((e1, e2) =>
|
|
|
|
|
{
|
|
|
|
|
int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime);
|
2020-09-23 14:41:43 +08:00
|
|
|
|
|
|
|
|
|
if (comp != 0)
|
|
|
|
|
return comp;
|
|
|
|
|
|
|
|
|
|
// we always want to insert the new item after equal ones.
|
|
|
|
|
// this is important for beatmaps with multiple hitobjects at the same point in time.
|
|
|
|
|
// if we use standard comparison insert order, there will be a churn of connections getting re-updated to
|
|
|
|
|
// the next object at the point-in-time, adding a construction/disposal overhead (see FollowPointConnection.End implementation's ClearInternal).
|
|
|
|
|
// this is easily visible on https://osu.ppy.sh/beatmapsets/150945#osu/372245
|
|
|
|
|
return -1;
|
|
|
|
|
}));
|
2019-11-01 14:39:23 +08:00
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
if (index < lifetimeEntries.Count - 1)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-11-06 15:33:42 +08:00
|
|
|
|
// Update the connection's end point to the next connection's start point
|
2019-11-01 14:39:23 +08:00
|
|
|
|
// h1 -> -> -> h2
|
2019-11-06 15:33:42 +08:00
|
|
|
|
// connection nextGroup
|
2019-02-27 20:07:17 +08:00
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1];
|
|
|
|
|
newEntry.End = nextEntry.Start;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
2019-11-05 18:31:48 +08:00
|
|
|
|
else
|
2019-11-05 22:20:46 +08:00
|
|
|
|
{
|
|
|
|
|
// The end point may be non-null during re-ordering
|
2020-11-19 23:11:31 +08:00
|
|
|
|
newEntry.End = null;
|
2019-11-05 22:20:46 +08:00
|
|
|
|
}
|
2019-02-27 20:07:17 +08:00
|
|
|
|
|
2019-11-01 18:22:07 +08:00
|
|
|
|
if (index > 0)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-11-06 15:33:42 +08:00
|
|
|
|
// Update the previous connection's end point to the current connection's start point
|
2019-11-01 14:39:23 +08:00
|
|
|
|
// h1 -> -> -> h2
|
2019-11-06 15:33:42 +08:00
|
|
|
|
// prevGroup connection
|
2019-02-27 20:07:17 +08:00
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
|
|
|
|
|
previousEntry.End = newEntry.Start;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
2020-02-23 03:37:04 +08:00
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
lifetimeManager.AddEntry(newEntry);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-20 13:10:28 +08:00
|
|
|
|
private void removeEntry(OsuHitObject hitObject)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2020-11-19 23:11:31 +08:00
|
|
|
|
int index = lifetimeEntries.FindIndex(e => e.Start == hitObject);
|
2020-11-20 14:31:04 +08:00
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
var entry = lifetimeEntries[index];
|
2020-11-20 14:31:04 +08:00
|
|
|
|
entry.UnbindEvents();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
lifetimeEntries.RemoveAt(index);
|
|
|
|
|
lifetimeManager.RemoveEntry(entry);
|
2019-04-01 11:16:05 +08:00
|
|
|
|
|
2019-11-01 18:22:07 +08:00
|
|
|
|
if (index > 0)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-11-06 15:33:42 +08:00
|
|
|
|
// Update the previous connection's end point to the next connection's start point
|
2019-11-01 18:22:07 +08:00
|
|
|
|
// h1 -> -> -> h2 -> -> -> h3
|
2019-11-06 15:33:42 +08:00
|
|
|
|
// prevGroup connection nextGroup
|
|
|
|
|
// The current connection's end point is used since there may not be a next connection
|
2020-11-19 23:11:31 +08:00
|
|
|
|
FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1];
|
|
|
|
|
previousEntry.End = entry.End;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
2020-11-19 23:11:31 +08:00
|
|
|
|
}
|
2019-11-01 14:39:23 +08:00
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
protected override bool CheckChildrenLife() => lifetimeManager.Update(Time.Current);
|
|
|
|
|
|
|
|
|
|
private void onEntryBecameAlive(LifetimeEntry entry)
|
|
|
|
|
{
|
|
|
|
|
var connection = connectionPool.Get(c =>
|
|
|
|
|
{
|
|
|
|
|
c.Entry = (FollowPointLifetimeEntry)entry;
|
|
|
|
|
c.Pool = pointPool;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
connectionsInUse[entry] = connection;
|
|
|
|
|
|
|
|
|
|
AddInternal(connection);
|
|
|
|
|
MakeChildAlive(connection);
|
2019-11-01 14:39:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
private void onEntryBecameDead(LifetimeEntry entry)
|
2019-11-01 14:39:23 +08:00
|
|
|
|
{
|
2020-11-19 23:11:31 +08:00
|
|
|
|
RemoveInternal(connectionsInUse[entry]);
|
|
|
|
|
connectionsInUse.Remove(entry);
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-20 13:10:28 +08:00
|
|
|
|
private void onStartTimeChanged(OsuHitObject hitObject)
|
|
|
|
|
{
|
|
|
|
|
removeEntry(hitObject);
|
|
|
|
|
addEntry(hitObject);
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-20 14:31:04 +08:00
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
|
|
|
{
|
|
|
|
|
base.Dispose(isDisposing);
|
|
|
|
|
|
|
|
|
|
foreach (var entry in lifetimeEntries)
|
|
|
|
|
entry.UnbindEvents();
|
|
|
|
|
lifetimeEntries.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
public class FollowPointLifetimeEntry : LifetimeEntry
|
|
|
|
|
{
|
2020-11-20 14:31:04 +08:00
|
|
|
|
public event Action Invalidated;
|
2020-11-19 23:11:31 +08:00
|
|
|
|
public readonly OsuHitObject Start;
|
|
|
|
|
|
|
|
|
|
public FollowPointLifetimeEntry(OsuHitObject start)
|
|
|
|
|
{
|
|
|
|
|
Start = start;
|
2020-11-20 12:55:01 +08:00
|
|
|
|
LifetimeStart = Start.StartTime;
|
2020-11-20 14:31:04 +08:00
|
|
|
|
|
|
|
|
|
bindEvents();
|
2020-11-19 23:11:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private OsuHitObject end;
|
|
|
|
|
|
|
|
|
|
public OsuHitObject End
|
|
|
|
|
{
|
|
|
|
|
get => end;
|
|
|
|
|
set
|
|
|
|
|
{
|
2020-11-20 14:31:04 +08:00
|
|
|
|
UnbindEvents();
|
|
|
|
|
|
2020-11-19 23:11:31 +08:00
|
|
|
|
end = value;
|
2020-11-20 14:31:04 +08:00
|
|
|
|
|
|
|
|
|
bindEvents();
|
|
|
|
|
|
|
|
|
|
refreshLifetimes();
|
2020-11-19 23:11:31 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-20 14:31:04 +08:00
|
|
|
|
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()
|
2020-11-19 23:11:31 +08:00
|
|
|
|
{
|
2020-11-20 15:19:02 +08:00
|
|
|
|
if (End == null || End.NewCombo || Start is Spinner || End is Spinner)
|
2020-11-19 23:11:31 +08:00
|
|
|
|
{
|
|
|
|
|
LifetimeEnd = LifetimeStart;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector2 startPosition = Start.StackedEndPosition;
|
|
|
|
|
Vector2 endPosition = End.StackedPosition;
|
|
|
|
|
Vector2 distanceVector = endPosition - startPosition;
|
|
|
|
|
|
2020-11-20 15:43:07 +08:00
|
|
|
|
// 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 _);
|
2020-11-19 23:11:31 +08:00
|
|
|
|
|
|
|
|
|
LifetimeStart = fadeInTime;
|
2020-11-20 12:55:01 +08:00
|
|
|
|
LifetimeEnd = double.MaxValue; // This will be set by the connection.
|
2020-11-20 14:31:04 +08:00
|
|
|
|
|
|
|
|
|
Invalidated?.Invoke();
|
2020-11-19 23:11:31 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|