mirror of
https://github.com/ppy/osu.git
synced 2025-02-12 23:12:59 +08:00
Merge pull request #18239 from peppy/editor-readability
Add lingering hitobject display to editor, replacing "Hit Animations" toggle
This commit is contained in:
commit
6e74244022
@ -1,114 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<SliderRepeat>().First());
|
||||
});
|
||||
}
|
||||
|
||||
private HitCircle getHitCircle(int index)
|
||||
=> EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(index);
|
||||
|
||||
private Slider getSliderWithRepeats(int index)
|
||||
=> EditorBeatmap.HitObjects.OfType<Slider>().Where(s => s.RepeatCount >= 1).ElementAt(index);
|
||||
|
||||
private DrawableHitObject getDrawableObjectFor(HitObject hitObject)
|
||||
=> this.ChildrenOfType<DrawableHitObject>().Single(ho => ho.HitObject == hitObject);
|
||||
|
||||
private IEnumerable<Transform> getTransformsRecursively(Drawable drawable)
|
||||
=> drawable.ChildrenOfType<Drawable>().SelectMany(d => d.Transforms);
|
||||
|
||||
private void toggleAnimations(bool enabled)
|
||||
=> AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled));
|
||||
|
||||
private void seekSmoothlyTo(Func<double> targetTime)
|
||||
{
|
||||
AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke()));
|
||||
AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime));
|
||||
}
|
||||
|
||||
private void assertFutureTransforms(Func<Drawable> getDrawable, bool hasFutureTransforms)
|
||||
=> AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms",
|
||||
() => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms);
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<HitCircle>
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
|
||||
|
@ -1,19 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<T> : HitObjectSelectionBlueprint<T>
|
||||
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)
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700;
|
||||
|
||||
public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
@ -37,85 +23,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private class OsuEditorPlayfield : OsuPlayfield
|
||||
{
|
||||
private Bindable<bool> hitAnimations;
|
||||
|
||||
protected override GameplayCursorContainer CreateCursor() => null;
|
||||
|
||||
public OsuEditorPlayfield()
|
||||
{
|
||||
HitPolicy = new AnyOrderHitPolicy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
hitAnimations = config.GetBindable<bool>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +163,6 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
|
||||
|
||||
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
|
||||
SetDefault(OsuSetting.EditorHitAnimations, false);
|
||||
}
|
||||
|
||||
public IDictionary<OsuSetting, string> GetLoggableState() =>
|
||||
@ -356,7 +355,6 @@ namespace osu.Game.Configuration
|
||||
GameplayDisableWinKey,
|
||||
SeasonalBackgroundMode,
|
||||
EditorWaveformOpacity,
|
||||
EditorHitAnimations,
|
||||
DiscordRichPresence,
|
||||
AutomaticallyDownloadWhenSpectating,
|
||||
ShowOnlineExplicitContent,
|
||||
|
@ -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
|
||||
|
@ -271,7 +271,6 @@ namespace osu.Game.Screens.Edit
|
||||
Items = new MenuItem[]
|
||||
{
|
||||
new WaveformOpacityMenuItem(config.GetBindable<float>(OsuSetting.EditorWaveformOpacity)),
|
||||
new HitAnimationsMenuItem(config.GetBindable<bool>(OsuSetting.EditorHitAnimations))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Drawable> Content => content ?? base.Content;
|
||||
private readonly Container content;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user