1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 17:43:05 +08:00

Merge pull request #29616 from peppy/break-overlay-animation

Add beat-synced animation to break overlay
This commit is contained in:
Dean Herbert 2024-09-04 19:39:35 +09:00 committed by GitHub
commit ca09028b06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 110 additions and 60 deletions

View File

@ -43,6 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset())) breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset()))
{ {
ProcessCustomClock = false, ProcessCustomClock = false,
BreakTracker = breakTracker,
} }
}; };
} }
@ -57,9 +58,6 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestShowBreaks() public void TestShowBreaks()
{ {
setClock(false);
addShowBreakStep(2);
addShowBreakStep(5); addShowBreakStep(5);
addShowBreakStep(15); addShowBreakStep(15);
} }
@ -124,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep($"show '{seconds}s' break", () => AddStep($"show '{seconds}s' break", () =>
{ {
breakOverlay.Breaks = breakTracker.Breaks = new List<BreakPeriod> breakTracker.Breaks = new List<BreakPeriod>
{ {
new BreakPeriod(Clock.CurrentTime, Clock.CurrentTime + seconds * 1000) new BreakPeriod(Clock.CurrentTime, Clock.CurrentTime + seconds * 1000)
}; };
@ -138,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void loadBreaksStep(string breakDescription, IReadOnlyList<BreakPeriod> breaks) private void loadBreaksStep(string breakDescription, IReadOnlyList<BreakPeriod> breaks)
{ {
AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breakTracker.Breaks = breaks); AddStep($"load {breakDescription}", () => breakTracker.Breaks = breaks);
seekAndAssertBreak("seek back to 0", 0, false); seekAndAssertBreak("seek back to 0", 0, false);
} }
@ -184,6 +182,7 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
public TestBreakTracker() public TestBreakTracker()
: base(0, new ScoreProcessor(new OsuRuleset()))
{ {
FramedManualClock = new FramedClock(manualClock = new ManualClock()); FramedManualClock = new FramedClock(manualClock = new ManualClock());
ProcessCustomClock = false; ProcessCustomClock = false;

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,15 +10,18 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play.Break; using osu.Game.Screens.Play.Break;
using osu.Game.Utils;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
public partial class BreakOverlay : Container public partial class BreakOverlay : BeatSyncedContainer
{ {
/// <summary> /// <summary>
/// The duration of the break overlay fading. /// 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; public const double BREAK_FADE_DURATION = BreakPeriod.MIN_BREAK_DURATION / 2;
private const float remaining_time_container_max_size = 0.3f; 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 readonly Container fadeContainer;
private IReadOnlyList<BreakPeriod> breaks = Array.Empty<BreakPeriod>();
public IReadOnlyList<BreakPeriod> Breaks
{
get => breaks;
set
{
breaks = value;
if (IsLoaded)
initializeBreaks();
}
}
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
public BreakTracker BreakTracker { get; init; } = null!;
private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeAdjustmentBox;
private readonly Container remainingTimeBox; private readonly Container remainingTimeBox;
private readonly RemainingTimeCounter remainingTimeCounter; private readonly RemainingTimeCounter remainingTimeCounter;
@ -53,11 +44,19 @@ namespace osu.Game.Screens.Play
private readonly ScoreProcessor scoreProcessor; private readonly ScoreProcessor scoreProcessor;
private readonly BreakInfo info; private readonly BreakInfo info;
private readonly IBindable<Period?> currentPeriod = new Bindable<Period?>();
public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor)
{ {
this.scoreProcessor = scoreProcessor; this.scoreProcessor = scoreProcessor;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
MinimumBeatLength = 200;
// Doesn't play well with pause/unpause.
// This might mean that some beats don't animate if the user is running <60fps, but we'll deal with that if anyone notices.
AllowMistimedEventFiring = false;
Child = fadeContainer = new Container Child = fadeContainer = new Container
{ {
Alpha = 0, Alpha = 0,
@ -114,13 +113,13 @@ namespace osu.Game.Screens.Play
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Margin = new MarginPadding { Bottom = vertical_margin }, Y = -vertical_margin,
}, },
info = new BreakInfo info = new BreakInfo
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = vertical_margin }, Y = vertical_margin,
}, },
breakArrows = new BreakArrows breakArrows = new BreakArrows
{ {
@ -134,30 +133,53 @@ namespace osu.Game.Screens.Play
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
initializeBreaks();
info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy);
((IBindable<ScoreRank>)info.GradeDisplay.Current).BindTo(scoreProcessor.Rank); ((IBindable<ScoreRank>)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() protected override void Update()
{ {
base.Update(); base.Update();
remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth);
// Keep things simple by resetting beat synced transforms on a rewind.
if (Clock.ElapsedFrameTime < 0)
{
remainingTimeBox.ClearTransforms(targetMember: nameof(Width));
remainingTimeBox.Width = remainingTimeForCurrentPeriod;
}
} }
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?> period)
{ {
FinishTransforms(true); FinishTransforms(true);
Scheduler.CancelDelayedTasks(); Scheduler.CancelDelayedTasks();
foreach (var b in breaks) if (period.NewValue == null)
{ return;
if (!b.HasEffect)
continue;
using (BeginAbsoluteSequence(b.StartTime)) var b = period.NewValue.Value;
using (BeginAbsoluteSequence(b.Start))
{ {
fadeContainer.FadeIn(BREAK_FADE_DURATION); fadeContainer.FadeIn(BREAK_FADE_DURATION);
breakArrows.Show(BREAK_FADE_DURATION); breakArrows.Show(BREAK_FADE_DURATION);
@ -167,13 +189,16 @@ namespace osu.Game.Screens.Play
.Delay(b.Duration - BREAK_FADE_DURATION) .Delay(b.Duration - BREAK_FADE_DURATION)
.ResizeWidthTo(0); .ResizeWidthTo(0);
remainingTimeBox remainingTimeBox.ResizeWidthTo(remainingTimeForCurrentPeriod);
.ResizeWidthTo(0, b.Duration - BREAK_FADE_DURATION)
.Then()
.ResizeWidthTo(1);
remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration);
remainingTimeCounter.MoveToX(-50)
.MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint);
info.MoveToX(50)
.MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint);
using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION))
{ {
fadeContainer.FadeOut(BREAK_FADE_DURATION); fadeContainer.FadeOut(BREAK_FADE_DURATION);
@ -183,4 +208,3 @@ namespace osu.Game.Screens.Play
} }
} }
} }
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -18,7 +16,7 @@ namespace osu.Game.Screens.Play
private readonly ScoreProcessor scoreProcessor; private readonly ScoreProcessor scoreProcessor;
private readonly double gameplayStartTime; private readonly double gameplayStartTime;
private PeriodTracker breaks; private PeriodTracker breaks = new PeriodTracker(Enumerable.Empty<Period>());
/// <summary> /// <summary>
/// Whether the gameplay is currently in a break. /// Whether the gameplay is currently in a break.
@ -27,6 +25,8 @@ namespace osu.Game.Screens.Play
private readonly BindableBool isBreakTime = new BindableBool(true); private readonly BindableBool isBreakTime = new BindableBool(true);
public readonly Bindable<Period?> CurrentPeriod = new Bindable<Period?>();
public IReadOnlyList<BreakPeriod> Breaks public IReadOnlyList<BreakPeriod> Breaks
{ {
set 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.gameplayStartTime = gameplayStartTime;
this.scoreProcessor = scoreProcessor; this.scoreProcessor = scoreProcessor;
@ -55,9 +55,16 @@ namespace osu.Game.Screens.Play
{ {
double time = Clock.CurrentTime; double time = Clock.CurrentTime;
isBreakTime.Value = breaks?.IsInAny(time) == true if (breaks.IsInAny(time, out var currentBreak))
|| time < gameplayStartTime {
|| scoreProcessor?.HasCompleted.Value == true; CurrentPeriod.Value = currentBreak;
isBreakTime.Value = true;
}
else
{
CurrentPeriod.Value = null;
isBreakTime.Value = time < gameplayStartTime || scoreProcessor.HasCompleted.Value;
}
} }
} }
} }

View File

@ -468,7 +468,7 @@ namespace osu.Game.Screens.Play
{ {
Clock = DrawableRuleset.FrameStableClock, Clock = DrawableRuleset.FrameStableClock,
ProcessCustomClock = false, ProcessCustomClock = false,
Breaks = working.Beatmap.Breaks BreakTracker = breakTracker,
}, },
// display the cursor above some HUD elements. // display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
namespace osu.Game.Utils namespace osu.Game.Utils
@ -24,8 +25,17 @@ namespace osu.Game.Utils
/// Whether the provided time is in any of the added periods. /// Whether the provided time is in any of the added periods.
/// </summary> /// </summary>
/// <param name="time">The time value to check.</param> /// <param name="time">The time value to check.</param>
public bool IsInAny(double time) public bool IsInAny(double time) => IsInAny(time, out _);
/// <summary>
/// Whether the provided time is in any of the added periods.
/// </summary>
/// <param name="time">The time value to check.</param>
/// <param name="period">The period which matched.</param>
public bool IsInAny(double time, [NotNullWhen(true)] out Period? period)
{ {
period = null;
if (periods.Count == 0) if (periods.Count == 0)
return false; return false;
@ -41,7 +51,15 @@ namespace osu.Game.Utils
} }
var nearest = periods[nearestIndex]; 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
/// </summary> /// </summary>
public readonly double End; public readonly double End;
public double Duration => End - Start;
public Period(double start, double end) public Period(double start, double end)
{ {
if (start >= end) if (start >= end)