mirror of
https://github.com/ppy/osu.git
synced 2024-12-30 09:22:54 +08:00
580 lines
22 KiB
C#
580 lines
22 KiB
C#
// 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 disable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using JetBrains.Annotations;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Pooling;
|
|
using osu.Game.Audio;
|
|
using osu.Game.Rulesets.Judgements;
|
|
using osu.Game.Rulesets.Mods;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Rulesets.Objects.Drawables;
|
|
using osu.Game.Skinning;
|
|
using osuTK;
|
|
using osu.Game.Rulesets.Objects.Pooling;
|
|
using osu.Framework.Extensions.ObjectExtensions;
|
|
using osu.Framework.Graphics.Primitives;
|
|
|
|
namespace osu.Game.Rulesets.UI
|
|
{
|
|
[Cached(typeof(IPooledHitObjectProvider))]
|
|
[Cached(typeof(IPooledSampleProvider))]
|
|
[Cached]
|
|
public abstract partial class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider
|
|
{
|
|
/// <summary>
|
|
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
|
|
/// </summary>
|
|
public event Action<DrawableHitObject, JudgementResult> NewResult;
|
|
|
|
/// <summary>
|
|
/// Invoked when a judgement result is reverted.
|
|
/// </summary>
|
|
public event Action<JudgementResult> RevertResult;
|
|
|
|
/// <summary>
|
|
/// The <see cref="DrawableHitObject"/> contained in this Playfield.
|
|
/// </summary>
|
|
public HitObjectContainer HitObjectContainer => hitObjectContainerLazy.Value;
|
|
|
|
private readonly Lazy<HitObjectContainer> hitObjectContainerLazy;
|
|
|
|
/// <summary>
|
|
/// A function that converts gamefield coordinates to screen space.
|
|
/// </summary>
|
|
public Func<Vector2, Vector2> GamefieldToScreenSpace => HitObjectContainer.ToScreenSpace;
|
|
|
|
/// <summary>
|
|
/// A function that converts screen space coordinates to gamefield.
|
|
/// </summary>
|
|
public Func<Vector2, Vector2> ScreenSpaceToGamefield => HitObjectContainer.ToLocalSpace;
|
|
|
|
/// <summary>
|
|
/// All the <see cref="DrawableHitObject"/>s contained in this <see cref="Playfield"/> and all <see cref="NestedPlayfields"/>.
|
|
/// </summary>
|
|
public IEnumerable<DrawableHitObject> AllHitObjects
|
|
{
|
|
get
|
|
{
|
|
if (HitObjectContainer == null)
|
|
return Enumerable.Empty<DrawableHitObject>();
|
|
|
|
var enumerable = HitObjectContainer.Objects;
|
|
|
|
if (nestedPlayfields.Count != 0)
|
|
enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects));
|
|
|
|
return enumerable;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>.
|
|
/// </summary>
|
|
public IEnumerable<Playfield> NestedPlayfields => nestedPlayfields;
|
|
|
|
private readonly List<Playfield> nestedPlayfields = new List<Playfield>();
|
|
|
|
/// <summary>
|
|
/// Whether this <see cref="Playfield"/> is nested in another <see cref="Playfield"/>.
|
|
/// </summary>
|
|
public bool IsNested { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Whether judgements should be displayed by this and and all nested <see cref="Playfield"/>s.
|
|
/// </summary>
|
|
public readonly BindableBool DisplayJudgements = new BindableBool(true);
|
|
|
|
/// <summary>
|
|
/// A screen space draw quad which resembles the edges of the playfield for skinning purposes.
|
|
/// This will allow users / components to snap objects to the "edge" of the playfield.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Rulesets which reduce the visible area further than the full relative playfield space itself
|
|
/// should retarget this to the ScreenSpaceDrawQuad of the appropriate container.
|
|
/// </remarks>
|
|
public virtual Quad SkinnableComponentScreenSpaceDrawQuad => ScreenSpaceDrawQuad;
|
|
|
|
[Resolved(CanBeNull = true)]
|
|
[CanBeNull]
|
|
protected IReadOnlyList<Mod> Mods { get; private set; }
|
|
|
|
private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager();
|
|
|
|
private readonly Stack<HitObjectLifetimeEntry> judgedEntries;
|
|
|
|
/// <summary>
|
|
/// Creates a new <see cref="Playfield"/>.
|
|
/// </summary>
|
|
protected Playfield()
|
|
{
|
|
RelativeSizeAxes = Axes.Both;
|
|
|
|
hitObjectContainerLazy = new Lazy<HitObjectContainer>(() => CreateHitObjectContainer().With(h =>
|
|
{
|
|
h.NewResult += onNewResult;
|
|
h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o);
|
|
h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o);
|
|
}));
|
|
|
|
entryManager.OnEntryAdded += onEntryAdded;
|
|
entryManager.OnEntryRemoved += onEntryRemoved;
|
|
|
|
judgedEntries = new Stack<HitObjectLifetimeEntry>();
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
Cursor = CreateCursor();
|
|
|
|
if (Cursor != null)
|
|
{
|
|
// initial showing of the cursor will be handed by MenuCursorContainer (via DrawableRuleset's IProvideCursor implementation).
|
|
Cursor.Hide();
|
|
|
|
AddInternal(Cursor);
|
|
}
|
|
}
|
|
|
|
private void onNewDrawableHitObject(DrawableHitObject d)
|
|
{
|
|
d.OnNestedDrawableCreated += onNewDrawableHitObject;
|
|
|
|
OnNewDrawableHitObject(d);
|
|
|
|
Debug.Assert(!d.IsInitialized);
|
|
d.IsInitialized = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield.
|
|
/// </summary>
|
|
public virtual void PostProcess() => NestedPlayfields.ForEach(p => p.PostProcess());
|
|
|
|
/// <summary>
|
|
/// Adds a DrawableHitObject to this Playfield.
|
|
/// </summary>
|
|
/// <param name="h">The DrawableHitObject to add.</param>
|
|
public virtual void Add(DrawableHitObject h)
|
|
{
|
|
if (!h.IsInitialized)
|
|
onNewDrawableHitObject(h);
|
|
|
|
HitObjectContainer.Add(h);
|
|
OnHitObjectAdded(h.HitObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove a DrawableHitObject from this Playfield.
|
|
/// </summary>
|
|
/// <param name="h">The DrawableHitObject to remove.</param>
|
|
public virtual bool Remove(DrawableHitObject h)
|
|
{
|
|
if (!HitObjectContainer.Remove(h))
|
|
return false;
|
|
|
|
OnHitObjectRemoved(h.HitObject);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="Playfield"/>.
|
|
/// </summary>
|
|
/// <param name="hitObject">The added <see cref="HitObject"/>.</param>
|
|
protected virtual void OnHitObjectAdded(HitObject hitObject)
|
|
{
|
|
preloadSamples(hitObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when a <see cref="HitObject"/> is removed from this <see cref="Playfield"/>.
|
|
/// </summary>
|
|
/// <param name="hitObject">The removed <see cref="HitObject"/>.</param>
|
|
protected virtual void OnHitObjectRemoved(HitObject hitObject)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked before a new <see cref="DrawableHitObject"/> is added to this <see cref="Playfield"/>.
|
|
/// It is invoked only once even if the drawable is pooled and used multiple times for different <see cref="HitObject"/>s.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is also invoked for nested <see cref="DrawableHitObject"/>s.
|
|
/// </remarks>
|
|
protected virtual void OnNewDrawableHitObject(DrawableHitObject drawableHitObject)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided.
|
|
/// </summary>
|
|
[CanBeNull]
|
|
public GameplayCursorContainer Cursor { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Provide a cursor which is to be used for gameplay.
|
|
/// </summary>
|
|
/// <returns>The cursor, or null to show the menu cursor.</returns>
|
|
protected virtual GameplayCursorContainer CreateCursor() => null;
|
|
|
|
/// <summary>
|
|
/// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>.
|
|
/// This does not add the <see cref="Playfield"/> to the draw hierarchy.
|
|
/// </summary>
|
|
/// <param name="otherPlayfield">The <see cref="Playfield"/> to add.</param>
|
|
protected void AddNested(Playfield otherPlayfield)
|
|
{
|
|
otherPlayfield.IsNested = true;
|
|
|
|
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements);
|
|
|
|
otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r);
|
|
otherPlayfield.RevertResult += r => RevertResult?.Invoke(r);
|
|
otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h);
|
|
otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
|
|
|
|
nestedPlayfields.Add(otherPlayfield);
|
|
}
|
|
|
|
private Mod[] mods;
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
mods = Mods?.ToArray();
|
|
|
|
// in the case a consumer forgets to add the HitObjectContainer, we will add it here.
|
|
if (HitObjectContainer.Parent == null)
|
|
AddInternal(HitObjectContainer);
|
|
}
|
|
|
|
protected override void Update()
|
|
{
|
|
base.Update();
|
|
|
|
if (!IsNested && mods != null)
|
|
{
|
|
foreach (Mod mod in mods)
|
|
{
|
|
if (mod is IUpdatableByPlayfield updatable)
|
|
updatable.Update(this);
|
|
}
|
|
}
|
|
|
|
// When rewinding, revert future judgements in the reverse order.
|
|
while (judgedEntries.Count > 0)
|
|
{
|
|
var result = judgedEntries.Peek().Result;
|
|
Debug.Assert(result?.RawTime != null);
|
|
|
|
if (Time.Current >= result.RawTime.Value)
|
|
break;
|
|
|
|
revertResult(judgedEntries.Pop());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
|
|
/// </summary>
|
|
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
|
|
|
|
#region Pooling support
|
|
|
|
private readonly Dictionary<Type, IDrawablePool> pools = new Dictionary<Type, IDrawablePool>();
|
|
|
|
/// <summary>
|
|
/// Adds a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> to this <see cref="Playfield"/>.
|
|
/// </summary>
|
|
/// <param name="hitObject"></param>
|
|
public virtual void Add(HitObject hitObject)
|
|
{
|
|
var entry = CreateLifetimeEntry(hitObject);
|
|
entryManager.Add(entry, null);
|
|
}
|
|
|
|
private void preloadSamples(HitObject hitObject)
|
|
{
|
|
// prepare sample pools ahead of time so we're not initialising at runtime.
|
|
foreach (var sample in hitObject.Samples)
|
|
prepareSamplePool(sample);
|
|
|
|
foreach (var sample in hitObject.AuxiliarySamples)
|
|
prepareSamplePool(sample);
|
|
|
|
foreach (var nestedObject in hitObject.NestedHitObjects)
|
|
preloadSamples(nestedObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> from this <see cref="Playfield"/>.
|
|
/// </summary>
|
|
/// <param name="hitObject"></param>
|
|
/// <returns>Whether the <see cref="HitObject"/> was successfully removed.</returns>
|
|
public virtual bool Remove(HitObject hitObject)
|
|
{
|
|
if (entryManager.TryGet(hitObject, out var entry))
|
|
{
|
|
entryManager.Remove(entry);
|
|
return true;
|
|
}
|
|
|
|
return nestedPlayfields.Any(p => p.Remove(hitObject));
|
|
}
|
|
|
|
private void onEntryAdded(HitObjectLifetimeEntry entry, [CanBeNull] HitObject parentHitObject)
|
|
{
|
|
if (parentHitObject != null) return;
|
|
|
|
HitObjectContainer.Add(entry);
|
|
OnHitObjectAdded(entry.HitObject);
|
|
}
|
|
|
|
private void onEntryRemoved(HitObjectLifetimeEntry entry, [CanBeNull] HitObject parentHitObject)
|
|
{
|
|
if (parentHitObject != null) return;
|
|
|
|
HitObjectContainer.Remove(entry);
|
|
OnHitObjectRemoved(entry.HitObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the <see cref="HitObjectLifetimeEntry"/> for a given <see cref="HitObject"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This may be overridden to provide custom lifetime control (e.g. via <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/>.
|
|
/// </remarks>
|
|
/// <param name="hitObject">The <see cref="HitObject"/> to create the entry for.</param>
|
|
/// <returns>The <see cref="HitObjectLifetimeEntry"/>.</returns>
|
|
[NotNull]
|
|
protected virtual HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject) => new HitObjectLifetimeEntry(hitObject);
|
|
|
|
/// <summary>
|
|
/// Registers a default <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
|
|
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type.
|
|
/// </summary>
|
|
/// <param name="initialSize">The number of <see cref="DrawableHitObject"/>s to be initially stored in the pool.</param>
|
|
/// <param name="maximumSize">
|
|
/// The maximum number of <see cref="DrawableHitObject"/>s that can be stored in the pool.
|
|
/// If this limit is exceeded, every subsequent <see cref="DrawableHitObject"/> will be created anew instead of being retrieved from the pool,
|
|
/// until some of the existing <see cref="DrawableHitObject"/>s are returned to the pool.
|
|
/// </param>
|
|
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
|
|
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
|
|
public void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
|
|
where TObject : HitObject
|
|
where TDrawable : DrawableHitObject, new()
|
|
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
|
|
|
|
/// <summary>
|
|
/// Registers a custom <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
|
|
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type.
|
|
/// </summary>
|
|
/// <param name="pool">The <see cref="DrawablePool{T}"/> to register.</param>
|
|
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
|
|
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
|
|
protected void RegisterPool<TObject, TDrawable>([NotNull] DrawablePool<TDrawable> pool)
|
|
where TObject : HitObject
|
|
where TDrawable : DrawableHitObject, new()
|
|
{
|
|
pools[typeof(TObject)] = pool;
|
|
AddInternal(pool);
|
|
}
|
|
|
|
DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject, DrawableHitObject parent)
|
|
{
|
|
var pool = prepareDrawableHitObjectPool(hitObject);
|
|
|
|
return (DrawableHitObject)pool?.Get(d =>
|
|
{
|
|
var dho = (DrawableHitObject)d;
|
|
|
|
if (!dho.IsInitialized)
|
|
{
|
|
onNewDrawableHitObject(dho);
|
|
|
|
// If this is the first time this DHO is being used, then apply the DHO mods.
|
|
// This is done before Apply() so that the state is updated once when the hitobject is applied.
|
|
if (mods != null)
|
|
{
|
|
foreach (Mod mod in mods)
|
|
{
|
|
if (mod is IApplicableToDrawableHitObject applicable)
|
|
applicable.ApplyToDrawableHitObject(dho);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!entryManager.TryGet(hitObject, out var entry))
|
|
{
|
|
entry = CreateLifetimeEntry(hitObject);
|
|
entryManager.Add(entry, parent?.HitObject);
|
|
}
|
|
|
|
dho.ParentHitObject = parent;
|
|
dho.Apply(entry);
|
|
});
|
|
}
|
|
|
|
private IDrawablePool prepareDrawableHitObjectPool(HitObject hitObject)
|
|
{
|
|
var lookupType = hitObject.GetType();
|
|
|
|
IDrawablePool pool;
|
|
|
|
// Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists.
|
|
if (!pools.TryGetValue(lookupType, out pool))
|
|
{
|
|
foreach (var (t, p) in pools)
|
|
{
|
|
if (!t.IsInstanceOfType(hitObject))
|
|
continue;
|
|
|
|
pools[lookupType] = pool = p;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return pool;
|
|
}
|
|
|
|
private readonly Dictionary<ISampleInfo, DrawablePool<PoolableSkinnableSample>> samplePools = new Dictionary<ISampleInfo, DrawablePool<PoolableSkinnableSample>>();
|
|
|
|
public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) => prepareSamplePool(sampleInfo).Get();
|
|
|
|
private DrawablePool<PoolableSkinnableSample> prepareSamplePool(ISampleInfo sampleInfo)
|
|
{
|
|
if (samplePools.TryGetValue(sampleInfo, out var pool)) return pool;
|
|
|
|
AddInternal(samplePools[sampleInfo] = pool = new DrawableSamplePool(sampleInfo, 1));
|
|
|
|
return pool;
|
|
}
|
|
|
|
private partial class DrawableSamplePool : DrawablePool<PoolableSkinnableSample>
|
|
{
|
|
private readonly ISampleInfo sampleInfo;
|
|
|
|
public DrawableSamplePool(ISampleInfo sampleInfo, int initialSize, int? maximumSize = null)
|
|
: base(initialSize, maximumSize)
|
|
{
|
|
this.sampleInfo = sampleInfo;
|
|
}
|
|
|
|
protected override PoolableSkinnableSample CreateNewDrawable() => base.CreateNewDrawable().With(d => d.Apply(sampleInfo));
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void onNewResult(DrawableHitObject drawable, JudgementResult result)
|
|
{
|
|
Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null);
|
|
judgedEntries.Push(drawable.Entry.AsNonNull());
|
|
|
|
NewResult?.Invoke(drawable, result);
|
|
}
|
|
|
|
private void revertResult(HitObjectLifetimeEntry entry)
|
|
{
|
|
var result = entry.Result;
|
|
Debug.Assert(result != null);
|
|
|
|
RevertResult?.Invoke(result);
|
|
entry.OnRevertResult();
|
|
|
|
result.Reset();
|
|
}
|
|
|
|
#region Editor logic
|
|
|
|
/// <summary>
|
|
/// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this represents the time when the <see cref="HitObject"/>s become alive.
|
|
/// </remarks>
|
|
internal event Action<HitObject> HitObjectUsageBegan;
|
|
|
|
/// <summary>
|
|
/// Invoked when a <see cref="HitObject"/> becomes unused by a <see cref="DrawableHitObject"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If this <see cref="HitObjectContainer"/> uses pooled objects, this represents the time when the <see cref="HitObject"/>s become dead.
|
|
/// </remarks>
|
|
internal event Action<HitObject> HitObjectUsageFinished;
|
|
|
|
/// <summary>
|
|
/// Sets whether to keep a given <see cref="HitObject"/> always alive within this or any nested <see cref="Playfield"/>.
|
|
/// </summary>
|
|
/// <param name="hitObject">The <see cref="HitObject"/> to set.</param>
|
|
/// <param name="keepAlive">Whether to keep <paramref name="hitObject"/> always alive.</param>
|
|
internal void SetKeepAlive(HitObject hitObject, bool keepAlive)
|
|
{
|
|
if (entryManager.TryGet(hitObject, out var entry))
|
|
{
|
|
entry.KeepAlive = keepAlive;
|
|
return;
|
|
}
|
|
|
|
foreach (var p in nestedPlayfields)
|
|
p.SetKeepAlive(hitObject, keepAlive);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keeps all <see cref="HitObject"/>s alive within this and all nested <see cref="Playfield"/>s.
|
|
/// </summary>
|
|
internal void KeepAllAlive()
|
|
{
|
|
foreach (var entry in entryManager.AllEntries)
|
|
entry.KeepAlive = true;
|
|
|
|
foreach (var p in nestedPlayfields)
|
|
p.KeepAllAlive();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The amount of time prior to the current time within which <see cref="HitObject"/>s should be considered alive.
|
|
/// </summary>
|
|
internal double PastLifetimeExtension
|
|
{
|
|
get => HitObjectContainer.PastLifetimeExtension;
|
|
set
|
|
{
|
|
HitObjectContainer.PastLifetimeExtension = value;
|
|
|
|
foreach (var nested in nestedPlayfields)
|
|
nested.PastLifetimeExtension = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The amount of time after the current time within which <see cref="HitObject"/>s should be considered alive.
|
|
/// </summary>
|
|
internal double FutureLifetimeExtension
|
|
{
|
|
get => HitObjectContainer.FutureLifetimeExtension;
|
|
set
|
|
{
|
|
HitObjectContainer.FutureLifetimeExtension = value;
|
|
|
|
foreach (var nested in nestedPlayfields)
|
|
nested.FutureLifetimeExtension = value;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|