From 794765ba853dda7b08f5e970516619a21318d115 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Jan 2025 18:36:58 +0900 Subject: [PATCH 1/2] Remove use of `Loop` (and transforms) for slider repeat arrow animations Less transforms in gameplay is always better. This fixes repeat arrows animating completely incorrectly in the editor (and probably gameplay when rewinding). --- .../Skinning/Argon/ArgonReverseArrow.cs | 52 ++++++++----------- .../Skinning/Default/DefaultReverseArrow.cs | 42 +++++++-------- .../Skinning/Legacy/LegacyReverseArrow.cs | 46 ++++++---------- 3 files changed, 58 insertions(+), 82 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs index 87b89a07cf..9f15e8e177 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs @@ -5,12 +5,12 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -75,44 +75,38 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon accentColour = drawableRepeat.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true); - - drawableRepeat.ApplyCustomUpdateState += updateStateTransforms; } - private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) + protected override void Update() { + base.Update(); + + if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit) + { + double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); + Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out)); + } + else + Scale = Vector2.One; + const float move_distance = -12; + const float scale_amount = 1.3f; + const double move_out_duration = 35; const double move_in_duration = 250; const double total = 300; - switch (state) - { - case ArmedState.Idle: - main.ScaleTo(1.3f, move_out_duration, Easing.Out) - .Then() - .ScaleTo(1f, move_in_duration, Easing.Out) - .Loop(total - (move_in_duration + move_out_duration)); - side - .MoveToX(move_distance, move_out_duration, Easing.Out) - .Then() - .MoveToX(0, move_in_duration, Easing.Out) - .Loop(total - (move_in_duration + move_out_duration)); - break; + double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total; - case ArmedState.Hit: - double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); - this.ScaleTo(1.5f, animDuration, Easing.Out); - break; - } - } + if (loopCurrentTime < move_out_duration) + main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out)); + else + main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out)); - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (drawableRepeat.IsNotNull()) - drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; + if (loopCurrentTime < move_out_duration) + side.X = Interpolation.ValueAt(loopCurrentTime, 1, move_distance, 0, move_out_duration, Easing.Out); + else + side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs index ad49150d81..5e2d04700d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs @@ -3,10 +3,10 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -40,37 +40,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private void load(DrawableHitObject drawableObject) { drawableRepeat = (DrawableSliderRepeat)drawableObject; - drawableRepeat.ApplyCustomUpdateState += updateStateTransforms; } - private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) + protected override void Update() { - const double move_out_duration = 35; - const double move_in_duration = 250; - const double total = 300; + base.Update(); - switch (state) + if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit) { - case ArmedState.Idle: - InternalChild.ScaleTo(1.3f, move_out_duration, Easing.Out) - .Then() - .ScaleTo(1f, move_in_duration, Easing.Out) - .Loop(total - (move_in_duration + move_out_duration)); - break; - - case ArmedState.Hit: - double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); - InternalChild.ScaleTo(1.5f, animDuration, Easing.Out); - break; + double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); + Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out)); } - } + else + { + const float scale_amount = 1.3f; - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); + const double move_out_duration = 35; + const double move_in_duration = 250; + const double total = 300; - if (drawableRepeat.IsNotNull()) - drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; + double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total; + if (loopCurrentTime < move_out_duration) + Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out)); + else + Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out)); + } } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index ad1fb98aef..940e068da0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -9,10 +9,12 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -51,8 +53,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin; - drawableObject.ApplyCustomUpdateState += updateStateTransforms; - shouldRotate = skinSource.GetConfig(SkinConfiguration.LegacySetting.Version)?.Value <= 1; } @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy accentColour = drawableRepeat.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(c => { - arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White; + arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > 600 / 255f ? Color4.Black : Color4.White; }, true); } @@ -80,36 +80,25 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy); } - private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state) + protected override void Update() { - const double duration = 300; - const float rotation = 5.625f; + base.Update(); - switch (state) + if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit) { - case ArmedState.Idle: - if (shouldRotate) - { - InternalChild.ScaleTo(1.3f) - .RotateTo(rotation) - .Then() - .ScaleTo(1f, duration) - .RotateTo(-rotation, duration) - .Loop(); - } - else - { - InternalChild.ScaleTo(1.3f).Then() - .ScaleTo(1f, duration, Easing.Out) - .Loop(); - } + double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); + arrow.Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.4f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out)); + } + else + { + const double duration = 300; + const float rotation = 5.625f; - break; + double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration; - case ArmedState.Hit: - double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); - InternalChild.ScaleTo(1.4f, animDuration, Easing.Out); - break; + if (shouldRotate) + arrow.Rotation = Interpolation.ValueAt(loopCurrentTime, rotation, -rotation, 0, duration); + arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration)); } } @@ -120,7 +109,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (drawableRepeat.IsNotNull()) { drawableRepeat.HitObjectApplied -= onHitObjectApplied; - drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms; } } } From 8d913e8971ab827a0d47a434f1ded439d6251c36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jan 2025 16:54:11 +0900 Subject: [PATCH 2/2] Fix multiple animation inconsistencies pointed out in review --- .../Skinning/Argon/ArgonReverseArrow.cs | 4 ++-- .../Skinning/Legacy/LegacyReverseArrow.cs | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs index 9f15e8e177..1fbdbafec4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs @@ -104,9 +104,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out)); if (loopCurrentTime < move_out_duration) - side.X = Interpolation.ValueAt(loopCurrentTime, 1, move_distance, 0, move_out_duration, Easing.Out); + side.X = Interpolation.ValueAt(loopCurrentTime, 0, move_distance, 0, move_out_duration, Easing.Out); else - side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out); + side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 0, move_out_duration, move_out_duration + move_in_duration, Easing.Out); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index 940e068da0..85c895006b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -96,9 +96,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration; + // Reference: https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!/GameplayElements/HitObjects/Osu/HitCircleSliderEnd.cs#L79-L96 if (shouldRotate) + { arrow.Rotation = Interpolation.ValueAt(loopCurrentTime, rotation, -rotation, 0, duration); - arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration)); + arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration)); + } + else + { + arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration, Easing.Out)); + } } }