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

289 lines
11 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 osu.Framework.Allocation;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics.Primitives;
2018-04-13 17:19:50 +08:00
using osu.Game.Audio;
using osu.Game.Graphics;
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
{
public abstract class DrawableHitObject : SkinReloadableDrawable, IHasAccentColour
{
public readonly HitObject HitObject;
/// <summary>
/// The colour used for various elements of this DrawableHitObject.
/// </summary>
public virtual Color4 AccentColour { get; set; } = Color4.Gray;
// Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first
protected virtual string SampleNamespace => null;
protected SkinnableSound Samples;
protected virtual IEnumerable<SampleInfo> GetSamples() => HitObject.Samples;
private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
2018-07-02 15:10:56 +08:00
public IEnumerable<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : Enumerable.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>
/// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result.IsHit"/> is <see cref="true"/>.
/// 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; }
2018-04-13 17:19:50 +08:00
private bool judgementOccurred;
public bool Interactive = true;
public override bool HandleNonPositionalInput => Interactive;
public override bool HandlePositionalInput => Interactive;
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
public readonly Bindable<ArmedState> State = new Bindable<ArmedState>();
protected DrawableHitObject(HitObject hitObject)
{
HitObject = hitObject;
}
[BackgroundDependencyLoader]
private void load()
{
2018-08-06 10:50:18 +08:00
var judgement = HitObject.CreateJudgement();
if (judgement != null)
{
2018-08-06 10:50:18 +08:00
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)}.");
}
2018-04-13 17:19:50 +08:00
var samples = GetSamples().ToArray();
if (samples.Any())
{
if (HitObject.SampleControlPoint == null)
throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{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}.");
samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).ToArray();
foreach (var s in samples)
s.Namespace = SampleNamespace;
AddInternal(Samples = new SkinnableSound(samples));
2018-04-13 17:19:50 +08:00
}
}
protected override void LoadComplete()
{
base.LoadComplete();
State.ValueChanged += armed =>
2018-04-13 17:19:50 +08:00
{
UpdateState(armed.NewValue);
2018-04-13 17:19:50 +08:00
// apply any custom state overrides
ApplyCustomUpdateState?.Invoke(this, armed.NewValue);
2018-04-13 17:19:50 +08:00
if (armed.NewValue == ArmedState.Hit)
2018-04-13 17:19:50 +08:00
PlaySamples();
};
State.TriggerChange();
}
protected abstract void UpdateState(ArmedState state);
/// <summary>
/// Bind to apply a custom state which can override the default implementation.
/// </summary>
public event Action<DrawableHitObject, ArmedState> ApplyCustomUpdateState;
/// <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 as IHasEndTime)?.EndTime ?? HitObject.StartTime;
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.Type = HitResult.None;
State.Value = 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
}
protected virtual void AddNested(DrawableHitObject h)
{
h.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r);
2018-08-06 11:29:22 +08:00
h.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r);
2018-04-13 17:19:50 +08:00
h.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);
nestedHitObjects.Value.Add(h);
}
/// <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)}.");
judgementOccurred = true;
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 as IHasEndTime)?.EndTime ?? HitObject.StartTime;
Result.TimeOffset = 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;
case HitResult.Miss:
State.Value = ArmedState.Miss;
break;
default:
State.Value = ArmedState.Hit;
break;
2018-04-13 17:19:50 +08:00
}
OnNewResult?.Invoke(this, Result);
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Should be called at least once after lifetime of this hit object is end.
/// </summary>
public void OnLifetimeEnd()
{
foreach (var nested in NestedHitObjects)
nested.OnLifetimeEnd();
UpdateResult(false);
}
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
{
judgementOccurred = false;
if (AllJudged)
return false;
2018-07-02 15:10:56 +08:00
foreach (var d in NestedHitObjects)
judgementOccurred |= d.UpdateResult(userTriggered);
2018-04-13 17:19:50 +08:00
if (judgementOccurred || Judged)
2018-04-13 17:19:50 +08:00
return judgementOccurred;
var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;
CheckForResult(userTriggered, Time.Current - endTime);
2018-04-13 17:19:50 +08:00
return judgementOccurred;
}
/// <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>
2018-08-06 10:07:41 +08:00
protected virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(judgement);
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;
}
}
}