1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-09 09:37:42 +08:00
osu-lazer/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs

550 lines
22 KiB
C#
Raw Normal View History

// 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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Threading;
2018-04-13 17:19:50 +08:00
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
2018-11-20 15:51:59 +08:00
using osuTK.Graphics;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Objects.Drawables
{
[Cached(typeof(DrawableHitObject))]
2019-07-22 13:45:25 +08:00
public abstract class DrawableHitObject : SkinReloadableDrawable
2018-04-13 17:19:50 +08:00
{
public readonly HitObject HitObject;
/// <summary>
/// The colour used for various elements of this DrawableHitObject.
/// </summary>
2019-07-22 13:45:25 +08:00
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(Color4.Gray);
2018-04-13 17:19:50 +08:00
2019-11-08 15:19:55 +08:00
protected SkinnableSound Samples { get; private set; }
2018-04-13 17:19:50 +08:00
2019-06-30 20:58:30 +08:00
protected virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;
2018-04-13 17:19:50 +08:00
private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<DrawableHitObject>();
2018-04-13 17:19:50 +08:00
/// <summary>
/// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> OnNewResult;
/// <summary>
2018-08-06 11:29:22 +08:00
/// Invoked when a <see cref="JudgementResult"/> is being reverted by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
/// </summary>
2018-08-06 11:29:22 +08:00
public event Action<DrawableHitObject, JudgementResult> OnRevertResult;
2018-04-13 17:19:50 +08:00
/// <summary>
2018-08-06 10:31:54 +08:00
/// Whether a visual indicator should be displayed when a scoring result occurs.
2018-04-13 17:19:50 +08:00
/// </summary>
public virtual bool DisplayResult => true;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
2018-04-13 17:19:50 +08:00
/// </summary>
public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged);
2018-04-13 17:19:50 +08:00
/// <summary>
2019-04-25 16:36:17 +08:00
/// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit.
/// Note: This does NOT include nested hitobjects.
2018-04-13 17:19:50 +08:00
/// </summary>
public bool IsHit => Result?.IsHit ?? false;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Whether this <see cref="DrawableHitObject"/> has been judged.
/// Note: This does NOT include nested hitobjects.
2018-04-13 17:19:50 +08:00
/// </summary>
public bool Judged => Result?.HasResult ?? true;
/// <summary>
/// The scoring result of this <see cref="DrawableHitObject"/>.
/// </summary>
public JudgementResult Result { get; private set; }
private BindableList<HitSampleInfo> samplesBindable;
2019-10-18 12:18:41 +08:00
private Bindable<double> startTimeBindable;
private Bindable<int> comboIndexBindable;
2018-04-13 17:19:50 +08:00
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);
2018-04-13 17:19:50 +08:00
private readonly Bindable<ArmedState> state = new Bindable<ArmedState>();
public IBindable<ArmedState> State => state;
protected DrawableHitObject([NotNull] HitObject hitObject)
2018-04-13 17:19:50 +08:00
{
HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject));
2018-04-13 17:19:50 +08:00
}
[BackgroundDependencyLoader]
private void load()
{
2018-08-06 10:50:18 +08:00
var judgement = HitObject.CreateJudgement();
2019-04-01 11:16:05 +08:00
2018-08-06 10:50:18 +08:00
if (judgement != null)
{
Result = CreateResult(judgement);
if (Result == null)
2018-08-06 10:07:41 +08:00
throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
}
loadSamples();
2018-04-13 17:19:50 +08:00
}
protected override void LoadComplete()
{
base.LoadComplete();
2019-10-18 12:18:41 +08:00
HitObject.DefaultsApplied += onDefaultsApplied;
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
2020-02-12 18:02:25 +08:00
startTimeBindable.BindValueChanged(_ => updateState(State.Value, true));
if (HitObject is IHasComboInformation combo)
{
comboIndexBindable = combo.ComboIndexBindable.GetBoundCopy();
comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
}
samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
samplesBindable.CollectionChanged += (_, __) => loadSamples();
updateState(ArmedState.Idle, true);
2019-10-18 12:18:41 +08:00
onDefaultsApplied();
}
private void loadSamples()
{
2019-11-08 15:19:55 +08:00
if (Samples != null)
{
RemoveInternal(Samples);
Samples = null;
}
var samples = GetSamples().ToArray();
if (samples.Length <= 0)
return;
if (HitObject.SampleControlPoint == null)
2019-11-12 12:41:54 +08:00
{
2019-11-28 22:21:21 +08:00
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
2019-11-12 12:41:54 +08:00
}
AddInternal(Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))));
}
2019-10-18 12:18:41 +08:00
private void onDefaultsApplied() => apply(HitObject);
private void apply(HitObject hitObject)
{
#pragma warning disable 618 // can be removed 20200417
if (GetType().GetMethod(nameof(AddNested), BindingFlags.NonPublic | BindingFlags.Instance)?.DeclaringType != typeof(DrawableHitObject))
return;
#pragma warning restore 618
if (nestedHitObjects.IsValueCreated)
{
nestedHitObjects.Value.Clear();
ClearNestedHitObjects();
}
foreach (var h in hitObject.NestedHitObjects)
{
var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");
2019-10-21 12:52:02 +08:00
addNested(drawableNested);
AddNestedHitObject(drawableNested);
}
}
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to add nested <see cref="DrawableHitObject"/>s to the hierarchy.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to be added.</param>
protected virtual void AddNestedHitObject(DrawableHitObject hitObject)
{
}
/// <summary>
/// Adds a nested <see cref="DrawableHitObject"/>. This should not be used except for legacy nested <see cref="DrawableHitObject"/> usages.
/// </summary>
/// <param name="h"></param>
[Obsolete("Use AddNestedHitObject() / ClearNestedHitObjects() / CreateNestedHitObject() instead.")] // can be removed 20200417
2019-10-21 12:52:02 +08:00
protected virtual void AddNested(DrawableHitObject h) => addNested(h);
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to remove all previously-added nested <see cref="DrawableHitObject"/>s.
/// </summary>
protected virtual void ClearNestedHitObjects()
{
}
/// <summary>
/// Creates the drawable representation for a nested <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/>.</param>
/// <returns>The drawable representation for <paramref name="hitObject"/>.</returns>
protected virtual DrawableHitObject CreateNestedHitObject(HitObject hitObject) => null;
2019-10-21 12:52:02 +08:00
private void addNested(DrawableHitObject hitObject)
{
// Todo: Exists for legacy purposes, can be removed 20200417
hitObject.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r);
hitObject.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r);
hitObject.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);
nestedHitObjects.Value.Add(hitObject);
}
2019-07-22 14:33:12 +08:00
#region State / Transform Management
2019-07-22 15:08:38 +08:00
/// <summary>
/// Bind to apply a custom state which can override the default implementation.
/// </summary>
public event Action<DrawableHitObject, ArmedState> ApplyCustomUpdateState;
2019-08-27 15:09:47 +08:00
#pragma warning disable 618 // (legacy state management) - can be removed 20200227
2019-07-22 14:33:12 +08:00
/// <summary>
/// Enables automatic transform management of this hitobject. Implementation of transforms should be done in <see cref="UpdateInitialTransforms"/> and <see cref="UpdateStateTransforms"/> only. Rewinding and removing previous states is done automatically.
/// </summary>
/// <remarks>
/// Going forward, this is the preferred way of implementing <see cref="DrawableHitObject"/>s. Previous functionality
/// is offered as a compatibility layer until all rulesets have been migrated across.
/// </remarks>
2019-08-27 15:09:47 +08:00
[Obsolete("Use UpdateInitialTransforms()/UpdateStateTransforms() instead")] // can be removed 20200227
protected virtual bool UseTransformStateManagement => true;
2019-07-22 14:33:12 +08:00
protected override void ClearInternal(bool disposeChildren = true) => throw new InvalidOperationException($"Should never clear a {nameof(DrawableHitObject)}");
private void updateState(ArmedState newState, bool force = false)
{
if (State.Value == newState && !force)
return;
if (UseTransformStateManagement)
{
LifetimeEnd = double.MaxValue;
double transformTime = HitObject.StartTime - InitialLifetimeOffset;
base.ApplyTransformsAt(double.MinValue, true);
base.ClearTransformsAfter(double.MinValue, true);
using (BeginAbsoluteSequence(transformTime, true))
{
2019-07-22 14:33:12 +08:00
UpdateInitialTransforms();
2018-04-13 17:19:50 +08:00
var judgementOffset = Result?.TimeOffset ?? 0;
using (BeginDelayedSequence(InitialLifetimeOffset + judgementOffset, true))
{
UpdateStateTransforms(newState);
state.Value = newState;
}
}
2019-09-12 18:29:08 +08:00
if (state.Value != ArmedState.Idle && LifetimeEnd == double.MaxValue)
Expire();
}
else
state.Value = newState;
UpdateState(newState);
// apply any custom state overrides
ApplyCustomUpdateState?.Invoke(this, newState);
if (newState == ArmedState.Hit)
PlaySamples();
2018-04-13 17:19:50 +08:00
}
2019-07-22 15:08:38 +08:00
/// <summary>
2019-07-23 20:15:55 +08:00
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
2019-07-22 15:08:38 +08:00
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> for convenience.
///
/// By default this will fade in the object from zero with no duration.
2019-07-22 15:08:38 +08:00
/// </summary>
/// <remarks>
/// This is called once before every <see cref="UpdateStateTransforms"/>. This is to ensure a good state in the case
/// the <see cref="JudgementResult.TimeOffset"/> was negative and potentially altered the pre-hit transforms.
/// </remarks>
2019-07-22 14:33:12 +08:00
protected virtual void UpdateInitialTransforms()
{
this.FadeInFromZero();
}
2019-07-22 15:08:38 +08:00
/// <summary>
/// Apply transforms based on the current <see cref="ArmedState"/>. Previous states are automatically cleared.
2019-09-12 18:29:08 +08:00
/// In the case of a non-idle <see cref="ArmedState"/>, and if <see cref="Drawable.LifetimeEnd"/> was not set during this call, <see cref="Drawable.Expire"/> will be invoked.
2019-07-22 15:08:38 +08:00
/// </summary>
/// <param name="state">The new armed state.</param>
2019-07-22 14:33:12 +08:00
protected virtual void UpdateStateTransforms(ArmedState state)
{
}
public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
{
2019-07-23 20:15:55 +08:00
// When we are using automatic state management, parent calls to this should be blocked for safety.
if (!UseTransformStateManagement)
base.ClearTransformsAfter(time, propagateChildren, targetMember);
}
public override void ApplyTransformsAt(double time, bool propagateChildren = false)
{
2019-07-23 20:15:55 +08:00
// When we are using automatic state management, parent calls to this should be blocked for safety.
if (!UseTransformStateManagement)
base.ApplyTransformsAt(time, propagateChildren);
}
2019-07-22 15:08:38 +08:00
/// <summary>
/// Legacy method to handle state changes.
/// Should generally not be used when <see cref="UseTransformStateManagement"/> is true; use <see cref="UpdateStateTransforms"/> instead.
/// </summary>
/// <param name="state">The new armed state.</param>
2019-08-27 15:09:47 +08:00
[Obsolete("Use UpdateInitialTransforms()/UpdateStateTransforms() instead")] // can be removed 20200227
protected virtual void UpdateState(ArmedState state)
{
}
2018-04-13 17:19:50 +08:00
2019-08-27 15:09:47 +08:00
#pragma warning restore 618
2019-07-22 14:33:12 +08:00
#endregion
protected sealed override void SkinChanged(ISkinSource skin, bool allowFallback)
2019-07-22 14:33:12 +08:00
{
base.SkinChanged(skin, allowFallback);
updateComboColour();
ApplySkin(skin, allowFallback);
if (IsLoaded)
updateState(State.Value, true);
}
private void updateComboColour()
{
2020-02-20 14:14:40 +08:00
if (!(HitObject is IHasComboInformation)) return;
var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value;
AccentColour.Value = GetComboColour(comboColours);
}
/// <summary>
2020-02-20 14:14:40 +08:00
/// Called to retrieve the combo colour. Automatically assigned to <see cref="AccentColour"/>.
/// Defaults to using <see cref="IHasComboInformation.ComboIndex"/> to decide on a colour.
/// </summary>
2020-02-20 14:14:40 +08:00
/// <remarks>
/// This will only be called if the <see cref="HitObject"/> implements <see cref="IHasComboInformation"/>.
/// </remarks>
/// <param name="comboColours">A list of combo colours provided by the beatmap or skin. Can be null if not available.</param>
2020-02-20 14:14:40 +08:00
protected virtual Color4 GetComboColour(IReadOnlyList<Color4> comboColours)
{
2020-02-20 14:14:40 +08:00
if (!(HitObject is IHasComboInformation combo))
throw new InvalidOperationException($"{nameof(HitObject)} must implement {nameof(IHasComboInformation)}");
return comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White;
}
/// <summary>
/// Called when a change is made to the skin.
/// </summary>
/// <param name="skin">The new skin.</param>
/// <param name="allowFallback">Whether fallback to default skin should be allowed if the custom skin is missing this resource.</param>
protected virtual void ApplySkin(ISkinSource skin, bool allowFallback)
{
2019-07-22 14:33:12 +08:00
}
2018-04-13 17:19:50 +08:00
/// <summary>
2018-09-15 22:30:11 +08:00
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
2018-04-13 17:19:50 +08:00
/// </summary>
public void PlaySamples() => Samples?.Play();
protected override void Update()
{
base.Update();
2018-08-14 17:31:32 +08:00
if (Result != null && Result.HasResult)
2018-04-13 17:19:50 +08:00
{
var endTime = HitObject.GetEndTime();
2018-04-13 17:19:50 +08:00
2018-08-14 17:31:32 +08:00
if (Result.TimeOffset + endTime > Time.Current)
{
2018-08-06 11:29:22 +08:00
OnRevertResult?.Invoke(this, Result);
Result.TimeOffset = 0;
Result.Type = HitResult.None;
updateState(ArmedState.Idle);
}
2018-04-13 17:19:50 +08:00
}
}
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => AllJudged && base.ComputeIsMaskedAway(maskingBounds);
2018-04-13 17:19:50 +08:00
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
UpdateResult(false);
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Schedules an <see cref="Action"/> to this <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// Only provided temporarily until hitobject pooling is implemented.
/// </remarks>
protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
2019-07-16 12:45:59 +08:00
private double? lifetimeStart;
public override double LifetimeStart
{
get => lifetimeStart ?? (HitObject.StartTime - InitialLifetimeOffset);
set
{
lifetimeStart = value;
base.LifetimeStart = value;
2019-07-16 12:45:59 +08:00
}
}
/// <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"/>.
/// </summary>
/// <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.
2019-07-22 15:08:38 +08:00
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
2019-09-12 18:30:27 +08:00
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
2019-07-16 12:45:59 +08:00
/// </remarks>
protected virtual double InitialLifetimeOffset => 10000;
/// <summary>
/// Will be called at least once after this <see cref="DrawableHitObject"/> has become not alive.
2019-07-16 12:45:59 +08:00
/// </summary>
public virtual void OnKilled()
2019-07-16 12:45:59 +08:00
{
foreach (var nested in NestedHitObjects)
nested.OnKilled();
2019-07-16 12:45:59 +08:00
UpdateResult(false);
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
/// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>.</param>
protected void ApplyResult(Action<JudgementResult> application)
2018-04-13 17:19:50 +08:00
{
application?.Invoke(Result);
if (!Result.HasResult)
throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}.");
2018-04-13 17:19:50 +08:00
// Ensure that the judgement is given a valid time offset, because this may not get set by the caller
var endTime = HitObject.GetEndTime();
Result.TimeOffset = Math.Min(HitObject.HitWindows.WindowFor(HitResult.Miss), Time.Current - endTime);
2018-04-13 17:19:50 +08:00
switch (Result.Type)
2018-04-13 17:19:50 +08:00
{
case HitResult.None:
break;
2019-04-01 11:16:05 +08:00
case HitResult.Miss:
updateState(ArmedState.Miss);
break;
2019-04-01 11:16:05 +08:00
default:
updateState(ArmedState.Hit);
break;
2018-04-13 17:19:50 +08:00
}
OnNewResult?.Invoke(this, Result);
2018-04-13 17:19:50 +08:00
}
/// <summary>
2018-08-06 10:31:54 +08:00
/// Processes this <see cref="DrawableHitObject"/>, checking if a scoring result has occurred.
2018-04-13 17:19:50 +08:00
/// </summary>
/// <param name="userTriggered">Whether the user triggered this process.</param>
2018-08-06 10:31:54 +08:00
/// <returns>Whether a scoring result has occurred from this <see cref="DrawableHitObject"/> or any nested <see cref="DrawableHitObject"/>.</returns>
protected bool UpdateResult(bool userTriggered)
2018-04-13 17:19:50 +08:00
{
// It's possible for input to get into a bad state when rewinding gameplay, so results should not be processed
if (Time.Elapsed < 0)
return false;
2019-09-04 17:14:55 +08:00
if (Judged)
2018-04-13 17:19:50 +08:00
return false;
var endTime = HitObject.GetEndTime();
CheckForResult(userTriggered, Time.Current - endTime);
2018-04-13 17:19:50 +08:00
2019-09-04 17:14:55 +08:00
return Judged;
2018-04-13 17:19:50 +08:00
}
/// <summary>
2018-08-06 10:31:54 +08:00
/// Checks if a scoring result has occurred for this <see cref="DrawableHitObject"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-08-06 10:31:54 +08:00
/// <remarks>
/// If a scoring result has occurred, this method must invoke <see cref="ApplyResult"/> to update the result and notify responders.
/// </remarks>
2018-04-13 17:19:50 +08:00
/// <param name="userTriggered">Whether the user triggered this check.</param>
2018-08-06 10:31:54 +08:00
/// <param name="timeOffset">The offset from the end time of the <see cref="HitObject"/> at which this check occurred.
/// A <paramref name="timeOffset"/> &gt; 0 implies that this check occurred after the end time of the <see cref="HitObject"/>. </param>
protected virtual void CheckForResult(bool userTriggered, double timeOffset)
2018-04-13 17:19:50 +08:00
{
}
/// <summary>
/// Creates the <see cref="JudgementResult"/> that represents the scoring result for this <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="judgement">The <see cref="Judgement"/> that provides the scoring information.</param>
protected virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(HitObject, judgement);
2019-10-18 12:18:41 +08:00
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
HitObject.DefaultsApplied -= onDefaultsApplied;
}
2018-04-13 17:19:50 +08:00
}
public abstract class DrawableHitObject<TObject> : DrawableHitObject
where TObject : HitObject
{
public new readonly TObject HitObject;
protected DrawableHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
}
}
}