diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs deleted file mode 100644 index 7ffa2c1f94..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorHitAnimations.cs +++ /dev/null @@ -1,114 +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 System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Configuration; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Edit; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Tests.Editor -{ - [TestFixture] - public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor - { - [Resolved] - private OsuConfigManager config { get; set; } - - [Test] - public void TestHitCircleAnimationDisable() - { - HitCircle hitCircle = null; - DrawableHitCircle drawableHitCircle = null; - - AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0)); - toggleAnimations(true); - seekSmoothlyTo(() => hitCircle.StartTime + 10); - - AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); - assertFutureTransforms(() => drawableHitCircle.CirclePiece, true); - - AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1)); - toggleAnimations(false); - seekSmoothlyTo(() => hitCircle.StartTime + 10); - - AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle)); - assertFutureTransforms(() => drawableHitCircle.CirclePiece, false); - AddAssert("hit circle has longer fade-out applied", () => - { - var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha)); - return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION; - }); - } - - [Test] - public void TestSliderAnimationDisable() - { - Slider slider = null; - DrawableSlider drawableSlider = null; - DrawableSliderRepeat sliderRepeat = null; - - AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0)); - toggleAnimations(true); - seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10); - - retrieveDrawables(); - assertFutureTransforms(() => sliderRepeat, true); - - AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1)); - toggleAnimations(false); - seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10); - - retrieveDrawables(); - assertFutureTransforms(() => sliderRepeat.Arrow, false); - seekSmoothlyTo(() => slider.GetEndTime()); - AddAssert("slider has longer fade-out applied", () => - { - var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha)); - return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION; - }); - - void retrieveDrawables() => - AddStep("retrieve drawables", () => - { - drawableSlider = (DrawableSlider)getDrawableObjectFor(slider); - sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType().First()); - }); - } - - private HitCircle getHitCircle(int index) - => EditorBeatmap.HitObjects.OfType().ElementAt(index); - - private Slider getSliderWithRepeats(int index) - => EditorBeatmap.HitObjects.OfType().Where(s => s.RepeatCount >= 1).ElementAt(index); - - private DrawableHitObject getDrawableObjectFor(HitObject hitObject) - => this.ChildrenOfType().Single(ho => ho.HitObject == hitObject); - - private IEnumerable getTransformsRecursively(Drawable drawable) - => drawable.ChildrenOfType().SelectMany(d => d.Transforms); - - private void toggleAnimations(bool enabled) - => AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled)); - - private void seekSmoothlyTo(Func targetTime) - { - AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke())); - AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime)); - } - - private void assertFutureTransforms(Func getDrawable, bool hasFutureTransforms) - => AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms", - () => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms); - } -} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs new file mode 100644 index 0000000000..ad4be2017e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCircleOverlapMarker.cs @@ -0,0 +1,86 @@ +// 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.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Screens.Edit; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components +{ + public class HitCircleOverlapMarker : BlueprintPiece + { + /// + /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. + /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. + /// + public const double FADE_OUT_EXTENSION = 700; + + private readonly RingPiece ring; + + [Resolved] + private EditorClock editorClock { get; set; } + + public HitCircleOverlapMarker() + { + Origin = Anchor.Centre; + + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); + + InternalChildren = new Drawable[] + { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + ring = new RingPiece + { + BorderThickness = 4, + } + }; + } + + [Resolved] + private ISkinSource skin { get; set; } + + public override void UpdateFrom(HitCircle hitObject) + { + base.UpdateFrom(hitObject); + + Scale = new Vector2(hitObject.Scale); + + if (hitObject is IHasComboInformation combo) + ring.BorderColour = combo.GetComboColour(skin); + + double editorTime = editorClock.CurrentTime; + double hitObjectTime = hitObject.StartTime; + bool hasReachedObject = editorTime >= hitObjectTime; + + if (hasReachedObject) + { + float alpha = Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION, Easing.In); + float ringScale = MathHelper.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1); + + ring.Scale = new Vector2(1 + 0.1f * ringScale); + Alpha = 0.9f * (1 - alpha); + } + else + Alpha = 0; + } + + public override void Hide() + { + // intentional no op so we are not hidden when not selected. + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs index b21a3e038e..3f9cfe21d4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs @@ -1,6 +1,7 @@ // 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.Primitives; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; @@ -14,11 +15,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles protected new DrawableHitCircle DrawableObject => (DrawableHitCircle)base.DrawableObject; protected readonly HitCirclePiece CirclePiece; + private readonly HitCircleOverlapMarker marker; public HitCircleSelectionBlueprint(HitCircle circle) : base(circle) { - InternalChild = CirclePiece = new HitCirclePiece(); + InternalChildren = new Drawable[] + { + marker = new HitCircleOverlapMarker(), + CirclePiece = new HitCirclePiece(), + }; } protected override void Update() @@ -26,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles base.Update(); CirclePiece.UpdateFrom(HitObject); + marker.UpdateFrom(HitObject); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs index 994c5cebeb..2d0c6fe81d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs @@ -1,19 +1,29 @@ // 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.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Screens.Edit; namespace osu.Game.Rulesets.Osu.Edit.Blueprints { public abstract class OsuSelectionBlueprint : HitObjectSelectionBlueprint where T : OsuHitObject { + [Resolved] + private EditorClock editorClock { get; set; } + protected new DrawableOsuHitObject DrawableObject => (DrawableOsuHitObject)base.DrawableObject; protected override bool AlwaysShowWhenSelected => true; + protected override bool ShouldBeAlive => base.ShouldBeAlive + || (editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION); + protected OsuSelectionBlueprint(T hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index 241ff70a18..d31d2aed97 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -1,6 +1,7 @@ // 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.Containers; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; @@ -13,20 +14,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private readonly Slider slider; private readonly SliderPosition position; + private readonly HitCircleOverlapMarker marker; public SliderCircleOverlay(Slider slider, SliderPosition position) { this.slider = slider; this.position = position; - InternalChild = CirclePiece = new HitCirclePiece(); + InternalChildren = new Drawable[] + { + marker = new HitCircleOverlapMarker(), + CirclePiece = new HitCirclePiece(), + }; } protected override void Update() { base.Update(); - CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle); + var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle; + + CirclePiece.UpdateFrom(circle); + marker.UpdateFrom(circle); + } + + public override void Hide() + { + CirclePiece.Hide(); + } + + public override void Show() + { + CirclePiece.Show(); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs index 72d04d4211..516b34d807 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditorRuleset.cs @@ -2,16 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -20,12 +12,6 @@ namespace osu.Game.Rulesets.Osu.Edit { public class DrawableOsuEditorRuleset : DrawableOsuRuleset { - /// - /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. - /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. - /// - public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700; - public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) { @@ -37,85 +23,12 @@ namespace osu.Game.Rulesets.Osu.Edit private class OsuEditorPlayfield : OsuPlayfield { - private Bindable hitAnimations; - protected override GameplayCursorContainer CreateCursor() => null; public OsuEditorPlayfield() { HitPolicy = new AnyOrderHitPolicy(); } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - hitAnimations = config.GetBindable(OsuSetting.EditorHitAnimations); - hitAnimations.BindValueChanged(_ => - { - foreach (var d in HitObjectContainer.AliveObjects) - d.RefreshStateTransforms(); - }); - } - - protected override void OnNewDrawableHitObject(DrawableHitObject d) - { - d.ApplyCustomUpdateState += updateState; - } - - private void updateState(DrawableHitObject hitObject, ArmedState state) - { - if (state == ArmedState.Idle || hitAnimations.Value) - return; - - if (hitObject is DrawableHitCircle circle) - { - using (circle.BeginAbsoluteSequence(circle.HitStateUpdateTime)) - { - circle.ApproachCircle - .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4) - .Expire(); - - circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint); - } - } - - if (hitObject is IHasMainCirclePiece mainPieceContainer) - { - // clear any explode animation logic. - // this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables. - ScheduleAfterChildren(() => - { - if (hitObject.HitObject == null) return; - - mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true); - mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true); - }); - } - - if (hitObject is DrawableSliderRepeat repeat) - { - repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true); - repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true); - } - - // adjust the visuals of top-level object types to make them stay on screen for longer than usual. - switch (hitObject) - { - case DrawableSlider _: - case DrawableHitCircle _: - // Get the existing fade out transform - var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); - - if (existing == null) - return; - - hitObject.RemoveTransform(existing); - - using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime)) - hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire(); - break; - } - } } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 026a83cceb..20d555c16c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -163,7 +163,6 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full); SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f); - SetDefault(OsuSetting.EditorHitAnimations, false); } public IDictionary GetLoggableState() => @@ -356,7 +355,6 @@ namespace osu.Game.Configuration GameplayDisableWinKey, SeasonalBackgroundMode, EditorWaveformOpacity, - EditorHitAnimations, DiscordRichPresence, AutomaticallyDownloadWhenSpectating, ShowOnlineExplicitContent, diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index 12ab89f79e..5e5bc9036d 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -89,16 +89,12 @@ namespace osu.Game.Rulesets.Edit // set the body piece's alpha directly to avoid arbitrarily rendering frame buffers etc. of children. foreach (var d in InternalChildren) d.Hide(); - - Hide(); } protected virtual void OnSelected() { foreach (var d in InternalChildren) d.Show(); - - Show(); } // When not selected, input is only required for the blueprint itself to receive IsHovering diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 0fa9d4ef01..3a2b195eed 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -271,7 +271,6 @@ namespace osu.Game.Screens.Edit Items = new MenuItem[] { new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), - new HitAnimationsMenuItem(config.GetBindable(OsuSetting.EditorHitAnimations)) } } } diff --git a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs index c3fb3bfc17..5448783f6d 100644 --- a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs @@ -2,16 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using JetBrains.Annotations; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Screens.Edit; namespace osu.Game.Tests.Visual { public abstract class SelectionBlueprintTestScene : OsuManualInputManagerTestScene { + [Cached] + private readonly EditorClock editorClock = new EditorClock(); + protected override Container Content => content ?? base.Content; private readonly Container content;