mirror of
https://github.com/ppy/osu
synced 2024-12-14 19:06:07 +00:00
Merge pull request #12500 from ekrctb/drawable-object
Factor out pooling and lifetime management logic of DHO to a base class
This commit is contained in:
commit
47925de7ae
@ -19,13 +19,14 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Drawables
|
||||
{
|
||||
[Cached(typeof(DrawableHitObject))]
|
||||
public abstract class DrawableHitObject : SkinReloadableDrawable
|
||||
public abstract class DrawableHitObject : PoolableDrawableWithLifetime<HitObjectLifetimeEntry>
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked after this <see cref="DrawableHitObject"/>'s applied <see cref="HitObject"/> has had its defaults applied.
|
||||
@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <summary>
|
||||
/// The <see cref="HitObject"/> currently represented by this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
public HitObject HitObject => lifetimeEntry?.HitObject;
|
||||
public HitObject HitObject => Entry?.HitObject;
|
||||
|
||||
/// <summary>
|
||||
/// The parenting <see cref="DrawableHitObject"/>, if any.
|
||||
@ -109,7 +110,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <summary>
|
||||
/// The scoring result of this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
public JudgementResult Result => lifetimeEntry?.Result;
|
||||
public JudgementResult Result => Entry?.Result;
|
||||
|
||||
/// <summary>
|
||||
/// The relative X position of this hit object for sample playback balance adjustment.
|
||||
@ -125,8 +126,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
private readonly Bindable<bool> userPositionalHitSounds = new Bindable<bool>();
|
||||
private readonly Bindable<int> comboIndexBindable = new Bindable<int>();
|
||||
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
public override bool RemoveCompletedTransforms => false;
|
||||
protected override bool RequiresChildrenUpdate => true;
|
||||
|
||||
public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart);
|
||||
@ -141,18 +140,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </remarks>
|
||||
public IBindable<ArmedState> State => state;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitObjectLifetimeEntry"/> is currently applied.
|
||||
/// </summary>
|
||||
private bool hasEntryApplied;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the currently-attached <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Even if it is not null, it may not be fully applied until loaded (<see cref="hasEntryApplied"/> is false).</remarks>
|
||||
[CanBeNull]
|
||||
private HitObjectLifetimeEntry lifetimeEntry;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
|
||||
|
||||
@ -166,32 +153,25 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </summary>
|
||||
/// <param name="initialHitObject">
|
||||
/// The <see cref="HitObject"/> to be initially applied to this <see cref="DrawableHitObject"/>.
|
||||
/// If <c>null</c>, a hitobject is expected to be later applied via <see cref="Apply(osu.Game.Rulesets.Objects.HitObjectLifetimeEntry)"/> (or automatically via pooling).
|
||||
/// If <c>null</c>, a hitobject is expected to be later applied via <see cref="PoolableDrawableWithLifetime{TEntry}.Apply"/> (or automatically via pooling).
|
||||
/// </param>
|
||||
protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
|
||||
: base(initialHitObject != null ? new SyntheticHitObjectEntry(initialHitObject) : null)
|
||||
{
|
||||
if (initialHitObject != null)
|
||||
{
|
||||
lifetimeEntry = new SyntheticHitObjectEntry(initialHitObject);
|
||||
if (Entry != null)
|
||||
ensureEntryHasResult();
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
private void load(OsuConfigManager config, ISkinSource skinSource)
|
||||
{
|
||||
config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds);
|
||||
|
||||
// Explicit non-virtual function call.
|
||||
base.AddInternal(Samples = new PausableSkinnableSound());
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
|
||||
if (lifetimeEntry != null && !hasEntryApplied)
|
||||
Apply(lifetimeEntry);
|
||||
CurrentSkin = skinSource;
|
||||
CurrentSkin.SourceChanged += onSkinSourceChanged;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -227,22 +207,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
Apply(new SyntheticHitObjectEntry(hitObject));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a new <see cref="HitObjectLifetimeEntry"/> to be represented by this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
public void Apply([NotNull] HitObjectLifetimeEntry newEntry)
|
||||
protected sealed override void OnApply(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
free();
|
||||
|
||||
lifetimeEntry = newEntry;
|
||||
|
||||
// LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset.
|
||||
// We override this with DHO's InitialLifetimeOffset for a non-pooled DHO.
|
||||
if (newEntry is SyntheticHitObjectEntry)
|
||||
lifetimeEntry.LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
|
||||
|
||||
LifetimeStart = lifetimeEntry.LifetimeStart;
|
||||
LifetimeEnd = lifetimeEntry.LifetimeEnd;
|
||||
if (entry is SyntheticHitObjectEntry)
|
||||
LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
|
||||
|
||||
ensureEntryHasResult();
|
||||
|
||||
@ -293,17 +263,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
else
|
||||
updateState(ArmedState.Idle, true);
|
||||
}
|
||||
|
||||
hasEntryApplied = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the currently applied <see cref="lifetimeEntry"/>
|
||||
/// </summary>
|
||||
private void free()
|
||||
protected sealed override void OnFree(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
if (!hasEntryApplied) return;
|
||||
|
||||
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
|
||||
if (HitObject is IHasComboInformation combo)
|
||||
comboIndexBindable.UnbindFrom(combo.ComboIndexBindable);
|
||||
@ -335,22 +298,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
OnFree();
|
||||
|
||||
ParentHitObject = null;
|
||||
lifetimeEntry = null;
|
||||
|
||||
clearExistingStateTransforms();
|
||||
|
||||
hasEntryApplied = false;
|
||||
}
|
||||
|
||||
protected sealed override void FreeAfterUse()
|
||||
{
|
||||
base.FreeAfterUse();
|
||||
|
||||
// Freeing while not in a pool would cause the DHO to not be usable elsewhere in the hierarchy without being re-applied.
|
||||
if (!IsInPool)
|
||||
return;
|
||||
|
||||
free();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -398,8 +347,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
private void onDefaultsApplied(HitObject hitObject)
|
||||
{
|
||||
Debug.Assert(lifetimeEntry != null);
|
||||
Apply(lifetimeEntry);
|
||||
Debug.Assert(Entry != null);
|
||||
Apply(Entry);
|
||||
|
||||
DefaultsApplied?.Invoke(this);
|
||||
}
|
||||
@ -482,7 +431,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
/// <summary>
|
||||
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
|
||||
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> for convenience.
|
||||
/// The local drawable hierarchy is recursively delayed to <see cref="HitObjectLifetimeEntry.LifetimeStart"/> for convenience.
|
||||
///
|
||||
/// By default this will fade in the object from zero with no duration.
|
||||
/// </summary>
|
||||
@ -536,17 +485,19 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
#endregion
|
||||
|
||||
protected sealed override void SkinChanged(ISkinSource skin, bool allowFallback)
|
||||
{
|
||||
base.SkinChanged(skin, allowFallback);
|
||||
#region Skinning
|
||||
|
||||
protected ISkinSource CurrentSkin { get; private set; }
|
||||
|
||||
private void onSkinSourceChanged() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
UpdateComboColour();
|
||||
|
||||
ApplySkin(skin, allowFallback);
|
||||
ApplySkin(CurrentSkin, true);
|
||||
|
||||
if (IsLoaded)
|
||||
updateState(State.Value, true);
|
||||
}
|
||||
});
|
||||
|
||||
protected void UpdateComboColour()
|
||||
{
|
||||
@ -616,6 +567,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
Samples.Stop();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -653,30 +606,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// </remarks>
|
||||
protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
|
||||
|
||||
public override double LifetimeStart
|
||||
{
|
||||
get => base.LifetimeStart;
|
||||
set => setLifetime(value, LifetimeEnd);
|
||||
}
|
||||
|
||||
public override double LifetimeEnd
|
||||
{
|
||||
get => base.LifetimeEnd;
|
||||
set => setLifetime(LifetimeStart, value);
|
||||
}
|
||||
|
||||
private void setLifetime(double lifetimeStart, double lifetimeEnd)
|
||||
{
|
||||
base.LifetimeStart = lifetimeStart;
|
||||
base.LifetimeEnd = lifetimeEnd;
|
||||
|
||||
if (lifetimeEntry != null)
|
||||
{
|
||||
lifetimeEntry.LifetimeStart = lifetimeStart;
|
||||
lifetimeEntry.LifetimeEnd = lifetimeEnd;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A safe offset prior to the start time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> may begin displaying contents.
|
||||
/// By default, <see cref="DrawableHitObject"/>s are assumed to display their contents within 10 seconds prior to the start time of <see cref="HitObject"/>.
|
||||
@ -684,7 +613,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <remarks>
|
||||
/// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required.
|
||||
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
|
||||
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
|
||||
/// A more accurate <see cref="HitObjectLifetimeEntry.LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
|
||||
/// <para>
|
||||
/// Only has an effect if this <see cref="DrawableHitObject"/> is not being pooled.
|
||||
/// For pooled <see cref="DrawableHitObject"/>s, use <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/> instead.
|
||||
@ -800,9 +729,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
private void ensureEntryHasResult()
|
||||
{
|
||||
Debug.Assert(lifetimeEntry != null);
|
||||
lifetimeEntry.Result ??= CreateResult(HitObject.CreateJudgement())
|
||||
?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
|
||||
Debug.Assert(Entry != null);
|
||||
Entry.Result ??= CreateResult(HitObject.CreateJudgement())
|
||||
?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
@ -811,6 +740,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
if (HitObject != null)
|
||||
HitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
|
||||
CurrentSkin.SourceChanged -= onSkinSourceChanged;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,122 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="PoolableDrawable"/> that is controlled by <see cref="Entry"/> to implement drawable pooling and replay rewinding.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntry">The <see cref="LifetimeEntry"/> type storing state and controlling this drawable.</typeparam>
|
||||
public abstract class PoolableDrawableWithLifetime<TEntry> : PoolableDrawable where TEntry : LifetimeEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The entry holding essential state of this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
/// </summary>
|
||||
protected TEntry? Entry { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="Entry"/> is applied to this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
/// When an initial entry is specified in the constructor, <see cref="Entry"/> is set but not applied until loading is completed.
|
||||
/// </summary>
|
||||
protected bool HasEntryApplied { get; private set; }
|
||||
|
||||
public override double LifetimeStart
|
||||
{
|
||||
get => base.LifetimeStart;
|
||||
set => setLifetime(value, LifetimeEnd);
|
||||
}
|
||||
|
||||
public override double LifetimeEnd
|
||||
{
|
||||
get => base.LifetimeEnd;
|
||||
set => setLifetime(LifetimeStart, value);
|
||||
}
|
||||
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
public override bool RemoveCompletedTransforms => false;
|
||||
|
||||
protected PoolableDrawableWithLifetime(TEntry? initialEntry = null)
|
||||
{
|
||||
Entry = initialEntry;
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
|
||||
// Apply the initial entry given in the constructor.
|
||||
if (Entry != null && !HasEntryApplied)
|
||||
Apply(Entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a new entry to be represented by this drawable.
|
||||
/// If there is an existing entry applied, the entry will be replaced.
|
||||
/// </summary>
|
||||
public void Apply(TEntry entry)
|
||||
{
|
||||
if (HasEntryApplied)
|
||||
free();
|
||||
|
||||
setLifetime(entry.LifetimeStart, entry.LifetimeEnd);
|
||||
Entry = entry;
|
||||
|
||||
OnApply(entry);
|
||||
|
||||
HasEntryApplied = true;
|
||||
}
|
||||
|
||||
protected sealed override void FreeAfterUse()
|
||||
{
|
||||
base.FreeAfterUse();
|
||||
|
||||
// We preserve the existing entry in case we want to move a non-pooled drawable between different parent drawables.
|
||||
if (HasEntryApplied && IsInPool)
|
||||
free();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked to apply a new entry to this drawable.
|
||||
/// </summary>
|
||||
protected virtual void OnApply(TEntry entry)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked to revert application of the entry to this drawable.
|
||||
/// </summary>
|
||||
protected virtual void OnFree(TEntry entry)
|
||||
{
|
||||
}
|
||||
|
||||
private void setLifetime(double start, double end)
|
||||
{
|
||||
base.LifetimeStart = start;
|
||||
base.LifetimeEnd = end;
|
||||
|
||||
if (Entry != null)
|
||||
{
|
||||
Entry.LifetimeStart = start;
|
||||
Entry.LifetimeEnd = end;
|
||||
}
|
||||
}
|
||||
|
||||
private void free()
|
||||
{
|
||||
Debug.Assert(Entry != null && HasEntryApplied);
|
||||
|
||||
OnFree(Entry);
|
||||
|
||||
Entry = null;
|
||||
setLifetime(double.MaxValue, double.MaxValue);
|
||||
|
||||
HasEntryApplied = false;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user