diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 91bb7f95f6..d83f5df7a3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -319,13 +319,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables const float fade_out_time = 450; - // intentionally pile on an extra FadeOut to make it happen much faster. - Ball.FadeOut(fade_out_time / 4, Easing.Out); - switch (state) { case ArmedState.Hit: - Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out); if (SliderBody?.SnakingOut.Value == true) Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. break; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 7bde60b39d..6bfb4e8aae 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour { + public const float FOLLOW_AREA = 2.4f; + public Func GetInitialHitAction; public Color4 AccentColour @@ -31,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables set => ball.Colour = value; } - private Drawable followCircle; private Drawable followCircleReceptor; private DrawableSlider drawableSlider; private Drawable ball; @@ -47,12 +48,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Children = new[] { - followCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()) + new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()) { Origin = Anchor.Centre, Anchor = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Alpha = 0, }, followCircleReceptor = new CircularContainer { @@ -103,10 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables tracking = value; - followCircleReceptor.Scale = new Vector2(tracking ? 2.4f : 1f); - - followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); - followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint); + followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f); } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs index 8211448705..254e220996 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs @@ -1,19 +1,19 @@ // 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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default { - public class DefaultFollowCircle : CompositeDrawable + public class DefaultFollowCircle : FollowCircle { public DefaultFollowCircle() { - RelativeSizeAxes = Axes.Both; - InternalChild = new CircularContainer { RelativeSizeAxes = Axes.Both, @@ -29,5 +29,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default } }; } + + protected override void OnTrackingChanged(ValueChangedEvent tracking) + { + const float scale_duration = 300f; + const float fade_duration = 300f; + + this.ScaleTo(tracking.NewValue ? DrawableSliderBall.FOLLOW_AREA : 1f, scale_duration, Easing.OutQuint) + .FadeTo(tracking.NewValue ? 1f : 0, fade_duration, Easing.OutQuint); + } + + protected override void OnSliderEnd() + { + const float fade_duration = 450f; + + // intentionally pile on an extra FadeOut to make it happen much faster + this.FadeOut(fade_duration / 4, Easing.Out); + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs index 47308375e6..97bb4a3697 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -19,13 +17,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { public class DefaultSliderBall : CompositeDrawable { - private Box box; + private Box box = null!; + + [Resolved(canBeNull: true)] + private DrawableHitObject? parentObject { get; set; } [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject, ISkinSource skin) + private void load(ISkinSource skin) { - var slider = (DrawableSlider)drawableObject; - RelativeSizeAxes = Axes.Both; float radius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; @@ -51,10 +50,62 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default } }; - slider.Tracking.BindValueChanged(trackingChanged, true); + if (parentObject != null) + { + var slider = (DrawableSlider)parentObject; + slider.Tracking.BindValueChanged(trackingChanged, true); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (parentObject != null) + { + parentObject.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(parentObject, parentObject.State.Value); + } } private void trackingChanged(ValueChangedEvent tracking) => box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint); + + private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state) + { + // Gets called by slider ticks, tails, etc., leading to duplicated + // animations which may negatively affect performance + if (drawableObject is not DrawableSlider) + return; + + const float fade_duration = 450f; + + using (BeginAbsoluteSequence(drawableObject.StateUpdateTime)) + { + this.FadeIn() + .ScaleTo(1f); + } + + using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + { + // intentionally pile on an extra FadeOut to make it happen much faster + this.FadeOut(fade_duration / 4, Easing.Out); + + switch (state) + { + case ArmedState.Hit: + this.ScaleTo(1.4f, fade_duration, Easing.Out); + break; + } + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (parentObject != null) + parentObject.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs new file mode 100644 index 0000000000..321705d25e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs @@ -0,0 +1,75 @@ +// 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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public abstract class FollowCircle : CompositeDrawable + { + [Resolved] + protected DrawableHitObject? ParentObject { get; private set; } + + protected FollowCircle() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + ((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(OnTrackingChanged, true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (ParentObject != null) + { + ParentObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(ParentObject); + + ParentObject.ApplyCustomUpdateState += updateStateTransforms; + updateStateTransforms(ParentObject, ParentObject.State.Value); + } + } + + private void onHitObjectApplied(DrawableHitObject drawableObject) + { + this.ScaleTo(1f) + .FadeOut(); + } + + private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state) + { + // Gets called by slider ticks, tails, etc., leading to duplicated + // animations which may negatively affect performance + if (drawableObject is not DrawableSlider) + return; + + using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) + OnSliderEnd(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (ParentObject != null) + { + ParentObject.HitObjectApplied -= onHitObjectApplied; + ParentObject.ApplyCustomUpdateState -= updateStateTransforms; + } + } + + protected abstract void OnTrackingChanged(ValueChangedEvent tracking); + + protected abstract void OnSliderEnd(); + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs index b8a559ce07..324f2525bc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.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 System; +using System.Diagnostics; +using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public class LegacyFollowCircle : CompositeDrawable + public class LegacyFollowCircle : FollowCircle { public LegacyFollowCircle(Drawable animationContent) { @@ -18,5 +20,36 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy RelativeSizeAxes = Axes.Both; InternalChild = animationContent; } + + protected override void OnTrackingChanged(ValueChangedEvent tracking) + { + Debug.Assert(ParentObject != null); + + if (ParentObject.Judged) + return; + + double remainingTime = ParentObject.HitStateUpdateTime - Time.Current; + + // Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour. + // This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this). + if (tracking.NewValue) + { + // TODO: Follow circle should bounce on each slider tick. + this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out) + .FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime)); + } + else + { + // TODO: Should animate only at the next slider tick if we want to match stable perfectly. + this.ScaleTo(4f, 100) + .FadeTo(0f, 100); + } + } + + protected override void OnSliderEnd() + { + this.ScaleTo(1.6f, 200, Easing.Out) + .FadeOut(200, Easing.In); + } } }