diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index a4d4ec50f8..dcb25f21ba 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -24,7 +24,10 @@ public TestSceneDrawableJudgement() if (hitWindows.IsHitResultAllowed(result)) { AddStep("Show " + result.GetDescription(), () => SetContents(() => - new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null) + new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement()) + { + Type = result + }, null) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index ebce40a785..a3dcd0e57f 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -5,6 +5,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -19,22 +20,42 @@ public DrawableManiaJudgement() { } - protected override double FadeInDuration => 50; + protected override void ApplyMissAnimations() + { + if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece)) + { + // this is temporary logic until mania's skin transformer returns IAnimatableJudgements + JudgementBody.ScaleTo(1.6f); + JudgementBody.ScaleTo(1, 100, Easing.In); + + JudgementBody.MoveTo(Vector2.Zero); + JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + JudgementBody.RotateTo(0); + JudgementBody.RotateTo(40, 800, Easing.InQuint); + JudgementBody.FadeOutFromOne(800); + + LifetimeEnd = JudgementBody.LatestTransformEndTime; + } + + base.ApplyMissAnimations(); + } protected override void ApplyHitAnimations() { JudgementBody.ScaleTo(0.8f); JudgementBody.ScaleTo(1, 250, Easing.OutElastic); - JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250); - this.Delay(FadeInDuration).FadeOut(200); + JudgementBody.Delay(50) + .ScaleTo(0.75f, 250) + .FadeOut(200); } - protected override Drawable CreateDefaultJudgement(HitResult result) => new ManiaJudgementPiece(result); + protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); - private class ManiaJudgementPiece : DefaultJudgementPiece + private class DefaultManiaJudgementPiece : DefaultJudgementPiece { - public ManiaJudgementPiece(HitResult result) + public DefaultManiaJudgementPiece(HitResult result) : base(result) { } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 646f12f710..e4158d8f07 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -43,10 +43,8 @@ public void TestHitLightingDisabled() showResult(HitResult.Great); AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); - AddAssert("judgement body immediately visible", - () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha == 1)); - AddAssert("hit lighting hidden", - () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0)); + AddAssert("hit lighting has no transforms", () => this.ChildrenOfType().All(judgement => !judgement.Lighting.Transforms.Any())); + AddAssert("hit lighting hidden", () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0)); } [Test] @@ -57,10 +55,7 @@ public void TestHitLightingEnabled() showResult(HitResult.Great); AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); - AddAssert("judgement body not immediately visible", - () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1)); - AddAssert("hit lighting shown", - () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha > 0)); + AddUntilStep("hit lighting shown", () => this.ChildrenOfType().Any(judgement => judgement.Lighting.Alpha > 0)); } private void showResult(HitResult result) @@ -89,7 +84,13 @@ private void showResult(HitResult result) Children = new Drawable[] { pool, - pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j => + pool.Get(j => j.Apply(new JudgementResult(new HitObject + { + StartTime = Time.Current + }, new Judgement()) + { + Type = result, + }, null)).With(j => { j.Anchor = Anchor.Centre; j.Origin = Anchor.Centre; @@ -106,7 +107,7 @@ private void showResult(HitResult result) private class TestDrawableOsuJudgement : DrawableOsuJudgement { public new SkinnableSprite Lighting => base.Lighting; - public new Container JudgementBody => base.JudgementBody; + public new SkinnableDrawable JudgementBody => base.JudgementBody; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 47fb53379f..13f5960bd4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -44,26 +44,21 @@ protected override void PrepareForUse() } } - private double fadeOutDelay; - protected override double FadeOutDelay => fadeOutDelay; - protected override void ApplyHitAnimations() { bool hitLightingEnabled = config.Get(OsuSetting.HitLighting); - if (hitLightingEnabled) - { - JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400); + Lighting.Alpha = 0; + if (hitLightingEnabled && Lighting.Drawable != null) + { + // todo: this animation changes slightly based on new/old legacy skin versions. Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); - } - else - { - JudgementBody.Alpha = 1; - } - fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay; + // extend the lifetime to cover lighting fade + LifetimeEnd = Lighting.LatestTransformEndTime; + } base.ApplyHitAnimations(); } diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index 7fe3917893..b89c1f4e4f 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -47,18 +47,18 @@ private void load() public virtual void PlayAnimation() { - this.RotateTo(0); - this.MoveTo(Vector2.Zero); - switch (Result) { case HitResult.Miss: this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); + this.MoveTo(Vector2.Zero); this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + this.RotateTo(0); this.RotateTo(40, 800, Easing.InQuint); + break; default: @@ -66,6 +66,8 @@ public virtual void PlayAnimation() this.ScaleTo(1, 500, Easing.OutElastic); break; } + + this.FadeOutFromOne(800); } } } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 468b3190b0..4aed38e689 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -1,11 +1,11 @@ // 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; @@ -25,18 +25,23 @@ public class DrawableJudgement : PoolableDrawable public DrawableHitObject JudgedObject { get; private set; } - protected Container JudgementBody { get; private set; } + public override bool RemoveCompletedTransforms => false; - private SkinnableDrawable skinnableJudgement; + protected SkinnableDrawable JudgementBody { get; private set; } + + [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; /// @@ -62,10 +67,32 @@ private void load() prepareDrawables(); } + 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. - /// Generally used for fading, defaulting to a simple fade out based on . - /// This will be used to calculate the lifetime of the judgement. + /// If displaying components which require lifetime extensions, manually adjusting is required. /// /// /// For animating the actual "default skin" judgement itself, it is recommended to use . @@ -73,13 +100,11 @@ private void load() /// protected virtual void ApplyHitAnimations() { - this.Delay(FadeOutDelay).FadeOut(400); } /// /// Apply top-level animations to the current judgement when missed. - /// Generally used for fading, defaulting to a simple fade out based on . - /// This will be used to calculate the lifetime of the judgement. + /// If displaying components which require lifetime extensions, manually adjusting is required. /// /// /// For animating the actual "default skin" judgement itself, it is recommended to use . @@ -87,7 +112,6 @@ protected virtual void ApplyHitAnimations() /// protected virtual void ApplyMissAnimations() { - this.Delay(600).FadeOut(200); } /// @@ -109,32 +133,46 @@ protected override void PrepareForUse() prepareDrawables(); - // not sure if this should remain going forward. - skinnableJudgement.ResetAnimation(); + runAnimation(); + } - this.FadeInFromZero(FadeInDuration, Easing.OutQuint); + private void runAnimation() + { + ClearTransforms(true); + LifetimeStart = Result.TimeAbsolute; - switch (Result.Type) + using (BeginAbsoluteSequence(Result.TimeAbsolute, true)) { - case HitResult.None: - break; + // not sure if this should remain going forward. + JudgementBody.ResetAnimation(); - case HitResult.Miss: - ApplyMissAnimations(); - break; + switch (Result.Type) + { + case HitResult.None: + break; - default: - ApplyHitAnimations(); - break; - } + case HitResult.Miss: + ApplyMissAnimations(); + break; + + default: + ApplyHitAnimations(); + break; + } + + if (JudgementBody.Drawable is IAnimatableJudgement animatable) + { + var drawableAnimation = (Drawable)animatable; - if (skinnableJudgement.Drawable is IAnimatableJudgement animatable) - { - using (BeginAbsoluteSequence(Result.TimeAbsolute)) animatable.PlayAnimation(); - } - Expire(true); + // 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; @@ -151,13 +189,11 @@ private void prepareDrawables() if (JudgementBody != null) RemoveInternal(JudgementBody); - AddInternal(JudgementBody = new Container + AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent(type), _ => + CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Child = skinnableJudgement = new SkinnableDrawable(new GameplaySkinComponent(type), _ => - CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) }); currentDrawableType = type; diff --git a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs index 3d5bbe6dad..32312f1115 100644 --- a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; + namespace osu.Game.Rulesets.Judgements { /// /// A skinnable judgement element which supports playing an animation from the current point in time. /// - public interface IAnimatableJudgement + public interface IAnimatableJudgement : IDrawable { void PlayAnimation(); } diff --git a/osu.Game/Skinning/LegacyJudgementPiece.cs b/osu.Game/Skinning/LegacyJudgementPiece.cs deleted file mode 100644 index 3def37e22c..0000000000 --- a/osu.Game/Skinning/LegacyJudgementPiece.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; -using osuTK; - -namespace osu.Game.Skinning -{ - public class LegacyJudgementPiece : CompositeDrawable, IAnimatableJudgement - { - private readonly HitResult result; - - public LegacyJudgementPiece(HitResult result, Drawable drawable) - { - this.result = result; - - AutoSizeAxes = Axes.Both; - Origin = Anchor.Centre; - - InternalChild = drawable; - } - - public virtual void PlayAnimation() - { - var animation = InternalChild as IFramedAnimation; - - animation?.GotoFrame(0); - - this.RotateTo(0); - this.MoveTo(Vector2.Zero); - - // legacy judgements don't play any transforms if they are an animation. - if (animation?.FrameCount > 1) - return; - - switch (result) - { - case HitResult.Miss: - this.ScaleTo(1.6f); - this.ScaleTo(1, 100, Easing.In); - - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - this.RotateTo(40, 800, Easing.InQuint); - break; - - default: - this.ScaleTo(0.9f); - this.ScaleTo(1, 500, Easing.OutElastic); - break; - } - } - } -} diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs new file mode 100644 index 0000000000..b5e1de337a --- /dev/null +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs @@ -0,0 +1,103 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osuTK; + +namespace osu.Game.Skinning +{ + public class LegacyJudgementPieceNew : CompositeDrawable, IAnimatableJudgement + { + private readonly HitResult result; + + private readonly LegacyJudgementPieceOld temporaryOldStyle; + + private readonly Drawable mainPiece; + + public LegacyJudgementPieceNew(HitResult result, Func createMainDrawable, Func createParticleDrawable) + { + this.result = result; + + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + InternalChildren = new[] + { + mainPiece = createMainDrawable().With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + }) + }; + + if (result != HitResult.Miss) + { + //new judgement shows old as a temporary effect + AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f) + { + Blending = BlendingParameters.Additive, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } + + public void PlayAnimation() + { + var animation = mainPiece as IFramedAnimation; + + animation?.GotoFrame(0); + + const double fade_in_length = 120; + const double fade_out_delay = 500; + const double fade_out_length = 600; + + this.FadeInFromZero(fade_in_length); + this.Delay(fade_out_delay).FadeOut(fade_out_length); + + // new style non-miss judgements show the original style temporarily, with additive colour. + if (temporaryOldStyle != null) + { + temporaryOldStyle.PlayAnimation(); + + temporaryOldStyle.Hide(); + temporaryOldStyle.Delay(-16) + .FadeTo(0.5f, 56, Easing.Out).Then() + .FadeOut(300); + } + + // legacy judgements don't play any transforms if they are an animation. + if (animation?.FrameCount > 1) + return; + + switch (result) + { + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + //todo: this only applies to osu! ruleset apparently. + this.MoveTo(new Vector2(0, -2)); + this.MoveToOffset(new Vector2(0, 20), fade_out_delay + fade_out_length, Easing.In); + + float rotation = RNG.NextSingle(-8.6f, 8.6f); + + this.RotateTo(0); + this.RotateTo(rotation, fade_in_length) + .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In); + break; + + default: + mainPiece.ScaleTo(0.9f); + mainPiece.ScaleTo(1.05f, fade_out_delay + fade_out_length); + break; + } + } + } +} diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs new file mode 100644 index 0000000000..3486dce081 --- /dev/null +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -0,0 +1,73 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Skinning +{ + public class LegacyJudgementPieceOld : CompositeDrawable, IAnimatableJudgement + { + private readonly HitResult result; + + private readonly float finalScale; + + public LegacyJudgementPieceOld(HitResult result, Func createMainDrawable, float finalScale = 1f) + { + this.result = result; + this.finalScale = finalScale; + + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + InternalChild = createMainDrawable(); + } + + public virtual void PlayAnimation() + { + var animation = InternalChild as IFramedAnimation; + + animation?.GotoFrame(0); + + const double fade_in_length = 120; + const double fade_out_delay = 500; + const double fade_out_length = 600; + + this.FadeInFromZero(fade_in_length); + this.Delay(fade_out_delay).FadeOut(fade_out_length); + + // legacy judgements don't play any transforms if they are an animation. + if (animation?.FrameCount > 1) + return; + + switch (result) + { + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + float rotation = RNG.NextSingle(-8.6f, 8.6f); + + this.RotateTo(0); + this.RotateTo(rotation, fade_in_length) + .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In); + break; + + default: + + this.ScaleTo(0.6f).Then() + .ScaleTo(1.1f, fade_in_length * 0.8f).Then() + // this is actually correct to match stable; there were overlapping transforms. + .ScaleTo(0.9f).Delay(fade_in_length * 0.2f) + .ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then() + .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); + break; + } + } + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index ca8bb58023..6faee8c2e7 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -371,9 +371,16 @@ public override Drawable GetDrawableComponent(ISkinComponent component) } case GameplaySkinComponent resultComponent: - var drawable = getJudgementAnimation(resultComponent.Component); - if (drawable != null) - return new LegacyJudgementPiece(resultComponent.Component, drawable); + Func createDrawable = () => getJudgementAnimation(resultComponent.Component); + + // kind of wasteful that we throw this away, but should do for now. + if (createDrawable() != null) + { + if (Configuration.LegacyVersion > 1) + return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, () => getParticleDrawable(resultComponent.Component)); + else + return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable); + } break; } @@ -381,6 +388,23 @@ public override Drawable GetDrawableComponent(ISkinComponent component) return this.GetAnimation(component.LookupName, false, false); } + private Drawable getParticleDrawable(HitResult result) + { + switch (result) + { + case HitResult.Meh: + return this.GetAnimation("particle50", false, false); + + case HitResult.Ok: + return this.GetAnimation("particle100", false, false); + + case HitResult.Great: + return this.GetAnimation("particle300", false, false); + } + + return null; + } + private Drawable getJudgementAnimation(HitResult result) { switch (result)