diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 33c422adb8..87a04312ab 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -41,9 +41,8 @@ namespace osu.Game.Rulesets.UI public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter where TObject : HitObject { - public override event Action OnNewResult; - - public override event Action OnRevertResult; + public override event Action NewResult; + public override event Action RevertResult; /// /// The selected variant. @@ -125,7 +124,11 @@ namespace osu.Game.Rulesets.UI RelativeSizeAxes = Axes.Both; KeyBindingInputManager = CreateInputManager(); - playfield = new Lazy(CreatePlayfield); + playfield = new Lazy(() => CreatePlayfield().With(p => + { + p.NewResult += (_, r) => NewResult?.Invoke(r); + p.RevertResult += (_, r) => RevertResult?.Invoke(r); + })); IsPaused.ValueChanged += paused => { @@ -183,7 +186,7 @@ namespace osu.Game.Rulesets.UI RegenerateAutoplay(); - loadObjects(cancellationToken); + loadObjects(cancellationToken ?? default); } public void RegenerateAutoplay() @@ -196,15 +199,15 @@ namespace osu.Game.Rulesets.UI /// /// Creates and adds drawable representations of hit objects to the play field. /// - private void loadObjects(CancellationToken? cancellationToken) + private void loadObjects(CancellationToken cancellationToken) { foreach (TObject h in Beatmap.HitObjects) { - cancellationToken?.ThrowIfCancellationRequested(); - addHitObject(h); + cancellationToken.ThrowIfCancellationRequested(); + AddHitObject(h); } - cancellationToken?.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); Playfield.PostProcess(); @@ -230,21 +233,24 @@ namespace osu.Game.Rulesets.UI ResumeOverlay?.Hide(); } - /// - /// Creates and adds the visual representation of a to this . - /// - /// The to add the visual representation for. - private void addHitObject(TObject hitObject) + public void AddHitObject(TObject hitObject) { - var drawableObject = CreateDrawableRepresentation(hitObject); + if (PoolHitObjects) + Playfield.Add(GetLifetimeEntry(hitObject)); + else + Playfield.Add(CreateDrawableRepresentation(hitObject)); + } - if (drawableObject == null) - return; - - drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r); - drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r); - - Playfield.Add(drawableObject); + public void RemoveHitObject(TObject hitObject) + { + if (PoolHitObjects) + Playfield.Remove(GetLifetimeEntry(hitObject)); + else + { + var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject); + if (drawableObject != null) + Playfield.Remove(drawableObject); + } } protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) @@ -382,12 +388,12 @@ namespace osu.Game.Rulesets.UI /// /// Invoked when a has been applied by a . /// - public abstract event Action OnNewResult; + public abstract event Action NewResult; /// /// Invoked when a is being reverted by a . /// - public abstract event Action OnRevertResult; + public abstract event Action RevertResult; /// /// Whether a replay is currently loaded. @@ -524,10 +530,6 @@ namespace osu.Game.Rulesets.UI /// /// /// Pools must be registered with this via in order for s to be retrieved. - /// - /// If true, hitobjects will be added to the via . - /// If false, will be used instead. - /// /// protected virtual bool PoolHitObjects => false; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 97604f62c8..3a5e0b64ed 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -31,7 +31,14 @@ namespace osu.Game.Rulesets.UI /// public IEnumerable AliveObjects => AliveInternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); + /// + /// Invoked when a is judged. + /// public event Action NewResult; + + /// + /// Invoked when a judgement is reverted. + /// public event Action RevertResult; /// @@ -79,7 +86,7 @@ namespace osu.Game.Rulesets.UI public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry); - public void Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry); + public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry); private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d92ba210db..2bd2bb9e06 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -10,13 +10,41 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.UI { public abstract class Playfield : CompositeDrawable { + /// + /// Invoked when a is judged. + /// + public event Action NewResult; + + /// + /// Invoked when a judgement is reverted. + /// + public event Action RevertResult; + + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + public event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + public event Action HitObjectUsageFinished; + /// /// The contained in this Playfield. /// @@ -72,7 +100,13 @@ namespace osu.Game.Rulesets.UI { RelativeSizeAxes = Axes.Both; - hitObjectContainerLazy = new Lazy(CreateHitObjectContainer); + hitObjectContainerLazy = new Lazy(() => CreateHitObjectContainer().With(h => + { + h.NewResult += (d, r) => NewResult?.Invoke(d, r); + h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); + h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); + })); } [Resolved(CanBeNull = true)] @@ -101,13 +135,103 @@ namespace osu.Game.Rulesets.UI /// Adds a DrawableHitObject to this Playfield. /// /// The DrawableHitObject to add. - public virtual void Add(DrawableHitObject h) => HitObjectContainer.Add(h); + public virtual void Add(DrawableHitObject h) + { + HitObjectContainer.Add(h); + + h.OnNewResult += (d, r) => NewResult?.Invoke(d, r); + h.OnRevertResult += (d, r) => RevertResult?.Invoke(d, r); + + OnHitObjectAdded(h.HitObject); + } /// /// Remove a DrawableHitObject from this Playfield. /// /// The DrawableHitObject to remove. - public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h); + public virtual bool Remove(DrawableHitObject h) + { + if (!HitObjectContainer.Remove(h)) + return false; + + OnHitObjectRemoved(h.HitObject); + return false; + } + + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + + /// + /// Adds a to this . + /// + /// The controlling the lifetime of the . + public void Add(HitObjectLifetimeEntry entry) + { + HitObjectContainer.Add(entry); + lifetimeEntryMap[entry.HitObject] = entry; + OnHitObjectAdded(entry.HitObject); + } + + /// + /// Removes a to this . + /// + /// The controlling the lifetime of the . + public void Remove(HitObjectLifetimeEntry entry) + { + if (HitObjectContainer.Remove(entry)) + OnHitObjectRemoved(entry.HitObject); + lifetimeEntryMap.Remove(entry.HitObject); + } + + /// + /// Invoked when a is added to this . + /// + /// The added . + protected virtual void OnHitObjectAdded(HitObject hitObject) + { + } + + /// + /// Invoked when a is removed from this . + /// + /// The removed . + protected virtual void OnHitObjectRemoved(HitObject hitObject) + { + } + + /// + /// Sets whether to keep a given always alive within this or any nested . + /// + /// The to set. + /// Whether to keep always alive. + public void SetKeepAlive(HitObject hitObject, bool keepAlive) + { + if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + { + entry.KeepAlive = keepAlive; + return; + } + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.SetKeepAlive(hitObject, keepAlive); + } + + /// + /// Keeps all s alive within this and all nested s. + /// + public void KeepAllAlive() + { + foreach (var (_, entry) in lifetimeEntryMap) + entry.KeepAlive = true; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.KeepAllAlive(); + } /// /// The cursor currently being used by this . May be null if no cursor is provided. @@ -131,6 +255,12 @@ namespace osu.Game.Rulesets.UI protected void AddNested(Playfield otherPlayfield) { otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); + + otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); + otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); + otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); + nestedPlayfields.Value.Add(otherPlayfield); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f9af1818d0..ee4f835c6f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -261,14 +261,14 @@ namespace osu.Game.Screens.Play // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); - DrawableRuleset.OnNewResult += r => + DrawableRuleset.NewResult += r => { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); gameplayBeatmap.ApplyResult(r); }; - DrawableRuleset.OnRevertResult += r => + DrawableRuleset.RevertResult += r => { HealthProcessor.RevertResult(r); ScoreProcessor.RevertResult(r);