diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 1d32313f2b..dcf350cbd4 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.UI /// /// All alive entries and s used by the entries. /// - public IEnumerable<(HitObjectLifetimeEntry Entry, DrawableHitObject Drawable)> AliveEntries => drawableMap.Select(x => (x.Key, x.Value)); + public IEnumerable<(HitObjectLifetimeEntry Entry, DrawableHitObject Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value)); public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.UI private readonly Dictionary startTimeMap = new Dictionary(); - private readonly Dictionary drawableMap = new Dictionary(); + private readonly Dictionary aliveDrawableMap = new Dictionary(); private readonly Dictionary nonPooledDrawableMap = new Dictionary(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); @@ -108,55 +108,70 @@ namespace osu.Game.Rulesets.UI public bool Remove(HitObjectLifetimeEntry entry) { if (!lifetimeManager.RemoveEntry(entry)) return false; - // The entry has to be removed from the non-pooled map here because non-pooled entry may be removed by specifying its entry. - nonPooledDrawableMap.Remove(entry); + + // This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry. + if (nonPooledDrawableMap.Remove(entry, out var drawable)) + removeDrawable(drawable); + allEntries.Remove(entry); return true; } - private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry); - - private void entryBecameDead(LifetimeEntry entry) => removeDrawable((HitObjectLifetimeEntry)entry); - - private void addDrawable(HitObjectLifetimeEntry entry) + private void entryBecameAlive(LifetimeEntry lifetimeEntry) { - Debug.Assert(!drawableMap.ContainsKey(entry)); + var entry = (HitObjectLifetimeEntry)lifetimeEntry; + Debug.Assert(!aliveDrawableMap.ContainsKey(entry)); - nonPooledDrawableMap.TryGetValue(entry, out var drawable); + bool isNonPooled = nonPooledDrawableMap.TryGetValue(entry, out var drawable); drawable ??= pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null); if (drawable == null) throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); + aliveDrawableMap[entry] = drawable; + OnAdd(drawable); + + if (isNonPooled) return; + + addDrawable(drawable); + HitObjectUsageBegan?.Invoke(entry.HitObject); + } + + private void entryBecameDead(LifetimeEntry lifetimeEntry) + { + var entry = (HitObjectLifetimeEntry)lifetimeEntry; + Debug.Assert(aliveDrawableMap.ContainsKey(entry)); + + var drawable = aliveDrawableMap[entry]; + bool isNonPooled = nonPooledDrawableMap.ContainsKey(entry); + + drawable.OnKilled(); + aliveDrawableMap.Remove(entry); + OnRemove(drawable); + + if (isNonPooled) return; + + removeDrawable(drawable); + // The hit object is not freed when the DHO was not pooled. + HitObjectUsageFinished?.Invoke(entry.HitObject); + } + + private void addDrawable(DrawableHitObject drawable) + { drawable.OnNewResult += onNewResult; drawable.OnRevertResult += onRevertResult; bindStartTime(drawable); - AddInternal(drawableMap[entry] = drawable); - OnAdd(drawable); - - HitObjectUsageBegan?.Invoke(entry.HitObject); + AddInternal(drawable); } - private void removeDrawable(HitObjectLifetimeEntry entry) + private void removeDrawable(DrawableHitObject drawable) { - Debug.Assert(drawableMap.ContainsKey(entry)); - - var drawable = drawableMap[entry]; - - // OnKilled can potentially change the hitobject's result, so it needs to run first before unbinding. - drawable.OnKilled(); drawable.OnNewResult -= onNewResult; drawable.OnRevertResult -= onRevertResult; - drawableMap.Remove(entry); - - OnRemove(drawable); unbindStartTime(drawable); - RemoveInternal(drawable); - // The hit object is not freed when the DHO was not pooled. - if (!nonPooledDrawableMap.ContainsKey(entry)) - HitObjectUsageFinished?.Invoke(entry.HitObject); + RemoveInternal(drawable); } #endregion @@ -169,6 +184,7 @@ namespace osu.Game.Rulesets.UI throw new InvalidOperationException($"May not add a {nameof(DrawableHitObject)} without {nameof(HitObject)} associated"); nonPooledDrawableMap.Add(drawable.Entry, drawable); + addDrawable(drawable); Add(drawable.Entry); } @@ -217,8 +233,10 @@ namespace osu.Game.Rulesets.UI public virtual void Clear() { lifetimeManager.ClearEntries(); + foreach (var drawable in nonPooledDrawableMap.Values) + removeDrawable(drawable); nonPooledDrawableMap.Clear(); - Debug.Assert(InternalChildren.Count == 0 && startTimeMap.Count == 0 && drawableMap.Count == 0, "All hit objects should have been removed"); + Debug.Assert(InternalChildren.Count == 0 && startTimeMap.Count == 0 && aliveDrawableMap.Count == 0, "All hit objects should have been removed"); } protected override bool CheckChildrenLife()