diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index e698766aac95..9fe6af1cb743 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osu.Game.Tests.Visual; using osuTK; @@ -89,10 +88,10 @@ public void TestBallTintChangedOnAccentChange() }); AddStep("set accent white", () => dho.AccentColour.Value = Color4.White); - AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White); + AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White); AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red); - AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red); + AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red); } private Slider prepareObject(Slider slider) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 3acec4498d58..1233ce626d72 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -29,7 +29,9 @@ public class DrawableSlider : DrawableOsuHitObject public DrawableSliderHead HeadCircle => headContainer.Child; public DrawableSliderTail TailCircle => tailContainer.Child; - public SliderBall Ball { get; private set; } + [Cached] + public DrawableSliderBall Ball { get; private set; } + public SkinnableDrawable Body { get; private set; } /// @@ -60,6 +62,13 @@ public DrawableSlider() public DrawableSlider([CanBeNull] Slider s = null) : base(s) { + Ball = new DrawableSliderBall + { + GetInitialHitAction = () => HeadCircle.HitAction, + BypassAutoSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0 + }; } [BackgroundDependencyLoader] @@ -73,13 +82,7 @@ private void load() repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, }, - Ball = new SliderBall(this) - { - GetInitialHitAction = () => HeadCircle.HitAction, - BypassAutoSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0 - }, + Ball, slidingSample = new PausableSkinnableSound { Looping = true } }; @@ -327,13 +330,9 @@ protected override void UpdateHitStateTransforms(ArmedState state) 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/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs similarity index 69% rename from osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs rename to osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 8943a9107675..87e3ab9bacb6 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -8,24 +8,24 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Skinning.Default +namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour + public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour { public Func GetInitialHitAction; + public const float FOLLOW_AREA = 2.4f; + public Color4 AccentColour { get => ball.Colour; @@ -33,32 +33,34 @@ public Color4 AccentColour } /// - /// Whether to track accurately to the visual size of this . + /// Whether to track accurately to the visual size of this . /// If false, tracking will be performed at the final scale at all times. /// public bool InputTracksVisualSize = true; - private readonly Drawable followCircle; - private readonly DrawableSlider drawableSlider; - private readonly Drawable ball; + private DrawableSlider drawableSlider; + private SkinnableDrawable ball; + private FollowReceptor followArea; - public SliderBall(DrawableSlider drawableSlider) + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject) { - this.drawableSlider = drawableSlider; + drawableSlider = (DrawableSlider)drawableObject; Origin = Anchor.Centre; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); - Children = new[] + Children = new Drawable[] { - followCircle = new FollowCircleContainer + followArea = new FollowReceptor { + Anchor = Anchor.Centre, Origin = Anchor.Centre, + }, + new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()) + { Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), + Origin = Anchor.Centre, }, ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()) { @@ -90,29 +92,7 @@ public override void ApplyTransformsAt(double time, bool propagateChildren = fal base.ApplyTransformsAt(time, false); } - private bool tracking; - - public bool Tracking - { - get => tracking; - private set - { - if (value == tracking) - return; - - tracking = value; - - if (InputTracksVisualSize) - followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); - else - { - // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration. - followCircle.ScaleTo(tracking ? 2.4f : 1f); - } - - followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint); - } - } + public bool Tracking { get; private set; } /// /// If the cursor moves out of the ball's radius we still need to be able to receive positional updates to stop tracking. @@ -168,7 +148,7 @@ protected override void Update() // in valid time range Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime && // in valid position range - lastScreenSpaceMousePosition.HasValue && followCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && + lastScreenSpaceMousePosition.HasValue && followArea.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && // valid action (actions?.Any(isValidTrackingAction) ?? false); @@ -207,73 +187,34 @@ public void UpdateProgress(double completionProgress) lastPosition = newPos; } - private class FollowCircleContainer : CircularContainer + private class FollowReceptor : CircularContainer { public override bool HandlePositionalInput => true; - } - public class DefaultFollowCircle : CompositeDrawable - { - public DefaultFollowCircle() + private DrawableSliderBall sliderBall; + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject, DrawableSliderBall sliderBall) { + this.sliderBall = sliderBall; RelativeSizeAxes = Axes.Both; - InternalChild = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = 5, - BorderColour = Color4.Orange, - Blending = BlendingParameters.Additive, - Child = new Box - { - Colour = Color4.Orange, - RelativeSizeAxes = Axes.Both, - Alpha = 0.2f, - } - }; + var slider = (DrawableSlider)drawableObject; + slider.Tracking.BindValueChanged(trackingChanged, true); } - } - public class DefaultSliderBall : CompositeDrawable - { - private Box box; - - [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject, ISkinSource skin) + private void trackingChanged(ValueChangedEvent e) { - var slider = (DrawableSlider)drawableObject; - - RelativeSizeAxes = Axes.Both; - - float radius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; + bool tracking = e.NewValue; - InternalChild = new CircularContainer + if (sliderBall.InputTracksVisualSize) + this.ScaleTo(tracking ? FOLLOW_AREA : 1f, 300, Easing.OutQuint); + else { - Masking = true, - RelativeSizeAxes = Axes.Both, - Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Blending = BlendingParameters.Additive, - BorderThickness = 10, - BorderColour = Color4.White, - Alpha = 1, - Child = box = new Box - { - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - AlwaysPresent = true, - Alpha = 0 - } - }; - - slider.Tracking.BindValueChanged(trackingChanged, true); + // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration. + this.ScaleTo(tracking ? FOLLOW_AREA : 1f); + } } - - private void trackingChanged(ValueChangedEvent tracking) => - box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs new file mode 100644 index 000000000000..a6bd4d9fe8fb --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs @@ -0,0 +1,84 @@ +// 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.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public class DefaultFollowCircle : CompositeDrawable + { + private DrawableSlider slider; + private readonly Bindable trackingBindable = new Bindable(); + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject) + { + slider = (DrawableSlider)drawableObject; + + RelativeSizeAxes = Axes.Both; + + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 5, + BorderColour = Color4.Orange, + Blending = BlendingParameters.Additive, + Child = new Box + { + Colour = Color4.Orange, + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f, + } + }; + + trackingBindable.BindTo(slider.Tracking); + trackingBindable.BindValueChanged(trackingChanged, true); + slider.ApplyCustomUpdateState += updateStateTransforms; + } + + private void trackingChanged(ValueChangedEvent e) + { + bool tracking = e.NewValue; + + if (slider.Ball.InputTracksVisualSize) + this.ScaleTo(tracking ? DrawableSliderBall.FOLLOW_AREA : 1f, 300, Easing.OutQuint); + else + { + // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration. + this.ScaleTo(tracking ? DrawableSliderBall.FOLLOW_AREA : 1f); + } + + this.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint); + } + + private void updateStateTransforms(DrawableHitObject obj, ArmedState state) + { + if (!(obj is DrawableSlider)) + return; + + const float fade_out_time = 450f; + + using (BeginAbsoluteSequence(slider.StateUpdateTime)) + this.FadeIn(); + + using (BeginAbsoluteSequence(slider.HitStateUpdateTime)) + this.FadeOut(fade_out_time / 4, Easing.Out); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (slider != null) + slider.ApplyCustomUpdateState -= updateStateTransforms; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs new file mode 100644 index 000000000000..9a1f648b1870 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs @@ -0,0 +1,93 @@ +// 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.Framework.Graphics.Shapes; +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.Default +{ + public class DefaultSliderBall : CompositeDrawable + { + private Box box; + private DrawableSlider slider; + private readonly Bindable trackingBindable = new Bindable(); + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject, ISkinSource skin) + { + slider = (DrawableSlider)drawableObject; + + RelativeSizeAxes = Axes.Both; + + float radius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; + + InternalChild = new CircularContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + BorderThickness = 10, + BorderColour = Color4.White, + Alpha = 1, + Child = box = new Box + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + AlwaysPresent = true, + Alpha = 0 + } + }; + + trackingBindable.BindTo(slider.Tracking); + trackingBindable.BindValueChanged(trackingChanged, true); + slider.ApplyCustomUpdateState += updateStateTransforms; + } + + private void trackingChanged(ValueChangedEvent tracking) => + box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint); + + private void updateStateTransforms(DrawableHitObject obj, ArmedState state) + { + if (!(obj is DrawableSlider)) + return; + + const float fade_out_time = 450f; + + using (BeginAbsoluteSequence(slider.StateUpdateTime)) + this.FadeIn().ScaleTo(1f); + + using (BeginAbsoluteSequence(slider.HitStateUpdateTime)) + { + this.FadeOut(fade_out_time / 4, Easing.Out); + + switch (state) + { + case ArmedState.Hit: + this.ScaleTo(1.4f, fade_out_time, Easing.Out); + break; + } + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (slider != null) + slider.ApplyCustomUpdateState -= updateStateTransforms; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs new file mode 100644 index 000000000000..44e3a1a830f3 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs @@ -0,0 +1,85 @@ +// 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.Legacy +{ + public class LegacyFollowCircle : CompositeDrawable + { + private readonly Drawable animationContent; + private DrawableSlider slider; + private readonly Bindable trackingBindable = new Bindable(); + + public LegacyFollowCircle(Drawable animationContent) + { + this.animationContent = animationContent; + } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject) + { + slider = (DrawableSlider)drawableObject; + + RelativeSizeAxes = Axes.Both; + + InternalChild = animationContent; + animationContent.Anchor = Anchor.Centre; + animationContent.Origin = Anchor.Centre; + + trackingBindable.BindTo(slider.Tracking); + trackingBindable.BindValueChanged(trackingChanged, true); + slider.ApplyCustomUpdateState += updateStateTransforms; + } + + private void trackingChanged(ValueChangedEvent e) + { + if (slider.Judged) + return; + + bool tracking = e.NewValue; + + if (slider.Ball.InputTracksVisualSize) + { + if (tracking) + this.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200, Easing.OutQuint); + else + this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 2, 100, Easing.OutQuint).Then().ScaleTo(1f); + } + else + { + // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration. + this.ScaleTo(tracking ? DrawableSliderBall.FOLLOW_AREA : 1f); + } + + this.FadeTo(tracking ? 1f : 0f, 100, Easing.OutQuint); + } + + private void updateStateTransforms(DrawableHitObject obj, ArmedState state) + { + if (!(obj is DrawableSlider)) + return; + + const float fade_out_time = 200f; + + using (BeginAbsoluteSequence(slider.HitStateUpdateTime)) + { + this.FadeOut(fade_out_time, Easing.InQuint); + this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 0.8f, fade_out_time); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (slider != null) + slider.ApplyCustomUpdateState -= updateStateTransforms; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs index e4e148366563..808a5f642d43 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs @@ -5,6 +5,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK.Graphics; @@ -13,9 +15,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy public class LegacySliderBall : CompositeDrawable { private readonly Drawable animationContent; - private readonly ISkin skin; + private DrawableSlider slider; + private Sprite layerNd; private Sprite layerSpec; @@ -28,8 +31,10 @@ public LegacySliderBall(Drawable animationContent, ISkin skin) } [BackgroundDependencyLoader] - private void load() + private void load(DrawableHitObject drawableObject) { + slider = (DrawableSlider)drawableObject; + var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; InternalChildren = new[] @@ -54,6 +59,8 @@ private void load() Blending = BlendingParameters.Additive, }, }; + + slider.ApplyCustomUpdateState += updateStateTransforms; } protected override void UpdateAfterChildren() @@ -66,5 +73,25 @@ protected override void UpdateAfterChildren() layerNd.Rotation = -appliedRotation; layerSpec.Rotation = -appliedRotation; } + + private void updateStateTransforms(DrawableHitObject obj, ArmedState state) + { + if (!(obj is DrawableSlider)) + return; + + using (BeginAbsoluteSequence(slider.StateUpdateTime)) + this.FadeIn(); + + using (BeginAbsoluteSequence(slider.HitStateUpdateTime)) + this.FadeOut(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (slider != null) + slider.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index ff9f6f0e078d..316b0bcc8895 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -36,11 +36,16 @@ public override Drawable GetDrawableComponent(ISkinComponent component) return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false); case OsuSkinComponents.SliderFollowCircle: - var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true); - if (followCircle != null) + var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true); + + if (followCircleContent != null) + { // follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x - followCircle.Scale *= 0.5f; - return followCircle; + followCircleContent.Scale *= 0.5f; + return new LegacyFollowCircle(followCircleContent); + } + + return null; case OsuSkinComponents.SliderBall: var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");