// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Judgements { /// /// A drawable object which visualises the hit result of a . /// public class DrawableJudgement : PoolableDrawable { private const float judgement_size = 128; public JudgementResult Result { get; private set; } public DrawableHitObject JudgedObject { get; private set; } public override bool RemoveCompletedTransforms => false; protected SkinnableDrawable JudgementBody { get; private set; } private readonly Container aboveHitObjectsContent; [Resolved] private ISkinSource skinSource { get; set; } /// /// Duration of initial fade in. /// [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")] protected virtual double FadeInDuration => 100; /// /// Duration to wait until fade out begins. Defaults to . /// [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")] protected virtual double FadeOutDelay => FadeInDuration; /// /// Creates a drawable which visualises a . /// /// The judgement to visualise. /// The object which was judged. public DrawableJudgement(JudgementResult result, DrawableHitObject judgedObject) : this() { Apply(result, judgedObject); } public DrawableJudgement() { Size = new Vector2(judgement_size); Origin = Anchor.Centre; AddInternal(aboveHitObjectsContent = new Container { Depth = float.MinValue, RelativeSizeAxes = Axes.Both }); } [BackgroundDependencyLoader] private void load() { prepareDrawables(); } public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy(); protected override void LoadComplete() { base.LoadComplete(); skinSource.SourceChanged += onSkinChanged; } private void onSkinChanged() { // on a skin change, the child component will update but not get correctly triggered to play its animation. // we need to trigger a reinitialisation to make things right. currentDrawableType = null; PrepareForUse(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (skinSource != null) skinSource.SourceChanged -= onSkinChanged; } /// /// Apply top-level animations to the current judgement when successfully hit. /// If displaying components which require lifetime extensions, manually adjusting is required. /// /// /// For animating the actual "default skin" judgement itself, it is recommended to use . /// This allows applying animations which don't affect custom skins. /// protected virtual void ApplyHitAnimations() { } /// /// Apply top-level animations to the current judgement when missed. /// If displaying components which require lifetime extensions, manually adjusting is required. /// /// /// For animating the actual "default skin" judgement itself, it is recommended to use . /// This allows applying animations which don't affect custom skins. /// protected virtual void ApplyMissAnimations() { } /// /// Associate a new result / object with this judgement. Should be called when retrieving a judgement from a pool. /// /// The applicable judgement. /// The drawable object. public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) { Result = result; JudgedObject = judgedObject; } protected override void PrepareForUse() { base.PrepareForUse(); Debug.Assert(Result != null); prepareDrawables(); runAnimation(); } private void runAnimation() { ApplyTransformsAt(double.MinValue, true); ClearTransforms(true); LifetimeStart = Result.TimeAbsolute; using (BeginAbsoluteSequence(Result.TimeAbsolute, true)) { // not sure if this should remain going forward. JudgementBody.ResetAnimation(); switch (Result.Type) { case HitResult.None: break; case HitResult.Miss: ApplyMissAnimations(); break; default: ApplyHitAnimations(); break; } if (JudgementBody.Drawable is IAnimatableJudgement animatable) { var drawableAnimation = (Drawable)animatable; animatable.PlayAnimation(); // a derived version of DrawableJudgement may be proposing a lifetime. // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime. double lastTransformTime = drawableAnimation.LatestTransformEndTime; if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd) LifetimeEnd = lastTransformTime; } } } private HitResult? currentDrawableType; private void prepareDrawables() { var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset // todo: this should be removed once judgements are always pooled. if (type == currentDrawableType) return; // sub-classes might have added their own children that would be removed here if .InternalChild was used. if (JudgementBody != null) RemoveInternal(JudgementBody); aboveHitObjectsContent.Clear(); AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent(type), _ => CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }); if (JudgementBody.Drawable is IAnimatableJudgement animatable) { var proxiedContent = animatable.GetAboveHitObjectsProxiedContent(); if (proxiedContent != null) aboveHitObjectsContent.Add(proxiedContent); } currentDrawableType = type; } protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result); } }