From 6f1664f0a60fc08995d737e40272b61742fbe580 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 16:30:49 +0900 Subject: [PATCH] Add beat-synced animation to break overlay I've been meaning to make the progress bar synchronise with the beat rather than a continuous countdown, just to give the overlay a bit more of a rhythmic feel. Not completely happy with how this feels but I think it's a start? I had to refactor how the break overlay works in the process. It no longer creates transforms for all breaks ahead-of-time, which could be argued as a better way of doing things. It's more dynamically able to handle breaks now (maybe useful for the future, who knows). --- .../Visual/Gameplay/TestSceneBreakTracker.cs | 13 ++- osu.Game/Screens/Play/BreakOverlay.cs | 104 ++++++++++-------- osu.Game/Screens/Play/BreakTracker.cs | 21 ++-- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Utils/PeriodTracker.cs | 24 +++- 5 files changed, 102 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index ea21262fc0..21b6495865 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osuTK.Graphics; @@ -38,9 +40,10 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, }, breakTracker = new TestBreakTracker(), - breakOverlay = new BreakOverlay(true, null) + breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset())) { ProcessCustomClock = false, + BreakTracker = breakTracker, } }; } @@ -55,9 +58,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestShowBreaks() { - setClock(false); - - addShowBreakStep(2); addShowBreakStep(5); addShowBreakStep(15); } @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep($"show '{seconds}s' break", () => { - breakOverlay.Breaks = breakTracker.Breaks = new List + breakTracker.Breaks = new List { new BreakPeriod(Clock.CurrentTime, Clock.CurrentTime + seconds * 1000) }; @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadBreaksStep(string breakDescription, IReadOnlyList breaks) { - AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breakTracker.Breaks = breaks); + AddStep($"load {breakDescription}", () => breakTracker.Breaks = breaks); seekAndAssertBreak("seek back to 0", 0, false); } @@ -182,6 +182,7 @@ namespace osu.Game.Tests.Visual.Gameplay } public TestBreakTracker() + : base(0, new ScoreProcessor(new OsuRuleset())) { FramedManualClock = new FramedClock(manualClock = new ManualClock()); ProcessCustomClock = false; diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 120d72a8e7..7fc3dba3eb 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -10,15 +10,18 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.Break; +using osu.Game.Utils; namespace osu.Game.Screens.Play { - public partial class BreakOverlay : Container + public partial class BreakOverlay : BeatSyncedContainer { /// /// The duration of the break overlay fading. @@ -26,26 +29,14 @@ namespace osu.Game.Screens.Play public const double BREAK_FADE_DURATION = BreakPeriod.MIN_BREAK_DURATION / 2; private const float remaining_time_container_max_size = 0.3f; - private const int vertical_margin = 25; + private const int vertical_margin = 15; private readonly Container fadeContainer; - private IReadOnlyList breaks = Array.Empty(); - - public IReadOnlyList Breaks - { - get => breaks; - set - { - breaks = value; - - if (IsLoaded) - initializeBreaks(); - } - } - public override bool RemoveCompletedTransforms => false; + public BreakTracker BreakTracker { get; init; } = null!; + private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; private readonly RemainingTimeCounter remainingTimeCounter; @@ -53,11 +44,15 @@ namespace osu.Game.Screens.Play private readonly ScoreProcessor scoreProcessor; private readonly BreakInfo info; + private readonly IBindable currentPeriod = new Bindable(); + public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) { this.scoreProcessor = scoreProcessor; RelativeSizeAxes = Axes.Both; + MinimumBeatLength = 200; + Child = fadeContainer = new Container { Alpha = 0, @@ -114,13 +109,13 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.Centre, Origin = Anchor.BottomCentre, - Margin = new MarginPadding { Bottom = vertical_margin }, + Y = -vertical_margin, }, info = new BreakInfo { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, - Margin = new MarginPadding { Top = vertical_margin }, + Y = vertical_margin, }, breakArrows = new BreakArrows { @@ -134,51 +129,68 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { base.LoadComplete(); - initializeBreaks(); info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); ((IBindable)info.GradeDisplay.Current).BindTo(scoreProcessor.Rank); + + currentPeriod.BindTo(BreakTracker.CurrentPeriod); + currentPeriod.BindValueChanged(updateDisplay, true); } + private float remainingTimeForCurrentPeriod => + currentPeriod.Value == null ? 0 : (float)Math.Max(0, (currentPeriod.Value.Value.End - Time.Current - BREAK_FADE_DURATION) / currentPeriod.Value.Value.Duration); + protected override void Update() { base.Update(); - remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); + if (currentPeriod.Value != null) + { + remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); + remainingTimeCounter.X = -(remainingTimeForCurrentPeriod - 0.5f) * 30; + info.X = (remainingTimeForCurrentPeriod - 0.5f) * 30; + } } - private void initializeBreaks() + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (currentPeriod.Value == null) + return; + + float timeBoxTargetWidth = (float)Math.Max(0, (remainingTimeForCurrentPeriod - timingPoint.BeatLength / currentPeriod.Value.Value.Duration)); + remainingTimeBox.ResizeWidthTo(timeBoxTargetWidth, timingPoint.BeatLength * 2, Easing.OutQuint); + } + + private void updateDisplay(ValueChangedEvent period) { FinishTransforms(true); Scheduler.CancelDelayedTasks(); - foreach (var b in breaks) + if (period.NewValue == null) + return; + + var b = period.NewValue.Value; + + using (BeginAbsoluteSequence(b.Start)) { - if (!b.HasEffect) - continue; + fadeContainer.FadeIn(BREAK_FADE_DURATION); + breakArrows.Show(BREAK_FADE_DURATION); - using (BeginAbsoluteSequence(b.StartTime)) + remainingTimeAdjustmentBox + .ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint) + .Delay(b.Duration - BREAK_FADE_DURATION) + .ResizeWidthTo(0); + + remainingTimeBox.ResizeWidthTo(remainingTimeForCurrentPeriod); + + remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); + + using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) { - fadeContainer.FadeIn(BREAK_FADE_DURATION); - breakArrows.Show(BREAK_FADE_DURATION); - - remainingTimeAdjustmentBox - .ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint) - .Delay(b.Duration - BREAK_FADE_DURATION) - .ResizeWidthTo(0); - - remainingTimeBox - .ResizeWidthTo(0, b.Duration - BREAK_FADE_DURATION) - .Then() - .ResizeWidthTo(1); - - remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); - - using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) - { - fadeContainer.FadeOut(BREAK_FADE_DURATION); - breakArrows.Hide(BREAK_FADE_DURATION); - } + fadeContainer.FadeOut(BREAK_FADE_DURATION); + breakArrows.Hide(BREAK_FADE_DURATION); } } } diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index 20ef1dc4bf..3c3f31053a 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.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 System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; @@ -18,7 +16,7 @@ namespace osu.Game.Screens.Play private readonly ScoreProcessor scoreProcessor; private readonly double gameplayStartTime; - private PeriodTracker breaks; + private PeriodTracker breaks = new PeriodTracker(Enumerable.Empty()); /// /// Whether the gameplay is currently in a break. @@ -27,6 +25,8 @@ namespace osu.Game.Screens.Play private readonly BindableBool isBreakTime = new BindableBool(true); + public readonly Bindable CurrentPeriod = new Bindable(); + public IReadOnlyList Breaks { set @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play } } - public BreakTracker(double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null) + public BreakTracker(double gameplayStartTime, ScoreProcessor scoreProcessor) { this.gameplayStartTime = gameplayStartTime; this.scoreProcessor = scoreProcessor; @@ -55,9 +55,16 @@ namespace osu.Game.Screens.Play { double time = Clock.CurrentTime; - isBreakTime.Value = breaks?.IsInAny(time) == true - || time < gameplayStartTime - || scoreProcessor?.HasCompleted.Value == true; + if (breaks.IsInAny(time, out var currentBreak)) + { + CurrentPeriod.Value = currentBreak; + isBreakTime.Value = true; + } + else + { + CurrentPeriod.Value = null; + isBreakTime.Value = time < gameplayStartTime || scoreProcessor.HasCompleted.Value; + } } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 91bd0a676b..2a66c3d5d3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -468,7 +468,7 @@ namespace osu.Game.Screens.Play { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, - Breaks = working.Beatmap.Breaks + BreakTracker = breakTracker, }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), diff --git a/osu.Game/Utils/PeriodTracker.cs b/osu.Game/Utils/PeriodTracker.cs index ba77702247..2c62684ac4 100644 --- a/osu.Game/Utils/PeriodTracker.cs +++ b/osu.Game/Utils/PeriodTracker.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace osu.Game.Utils @@ -24,8 +25,17 @@ namespace osu.Game.Utils /// Whether the provided time is in any of the added periods. /// /// The time value to check. - public bool IsInAny(double time) + public bool IsInAny(double time) => IsInAny(time, out _); + + /// + /// Whether the provided time is in any of the added periods. + /// + /// The time value to check. + /// The period which matched. + public bool IsInAny(double time, [NotNullWhen(true)] out Period? period) { + period = null; + if (periods.Count == 0) return false; @@ -41,7 +51,15 @@ namespace osu.Game.Utils } var nearest = periods[nearestIndex]; - return time >= nearest.Start && time <= nearest.End; + bool isInAny = time >= nearest.Start && time <= nearest.End; + + if (isInAny) + { + period = nearest; + return true; + } + + return false; } } @@ -57,6 +75,8 @@ namespace osu.Game.Utils /// public readonly double End; + public double Duration => End - Start; + public Period(double start, double end) { if (start >= end)