Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add beat-synced animation to break overlay #29616

Merged
merged 7 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public TestSceneBreakTracker()
breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset()))
{
ProcessCustomClock = false,
BreakTracker = breakTracker,
}
};
}
Expand All @@ -57,9 +58,6 @@ protected override void Update()
[Test]
public void TestShowBreaks()
{
setClock(false);

addShowBreakStep(2);
addShowBreakStep(5);
addShowBreakStep(15);
}
Expand Down Expand Up @@ -124,7 +122,7 @@ private void addShowBreakStep(double seconds)
{
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)
};
Expand All @@ -138,7 +136,7 @@ private void setClock(bool useManual)

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);
}

Expand Down Expand Up @@ -184,6 +182,7 @@ public double ManualClockTime
}

public TestBreakTracker()
: base(0, new ScoreProcessor(new OsuRuleset()))
{
FramedManualClock = new FramedClock(manualClock = new ManualClock());
ProcessCustomClock = false;
Expand Down
108 changes: 66 additions & 42 deletions osu.Game/Screens/Play/BreakOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,61 @@
// 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;
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
{
/// <summary>
/// The duration of the break overlay fading.
/// </summary>
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<BreakPeriod> breaks = Array.Empty<BreakPeriod>();

public IReadOnlyList<BreakPeriod> 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;
private readonly BreakArrows breakArrows;
private readonly ScoreProcessor scoreProcessor;
private readonly BreakInfo info;

private readonly IBindable<Period?> currentPeriod = new Bindable<Period?>();

public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor)
{
this.scoreProcessor = scoreProcessor;
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
{
Alpha = 0,
Expand Down Expand Up @@ -114,13 +113,13 @@ public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor)
{
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
{
Expand All @@ -134,51 +133,76 @@ public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor)
protected override void LoadComplete()
{
base.LoadComplete();
initializeBreaks();

info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy);
((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()
{
base.Update();

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;
}
}

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't get why this transform has duration of two beat lengths? Won't that cause overlapping transforms if this fires on every beat?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's just to make it really smooth (looks weird if it comes to a full stop).

}

private void initializeBreaks()
private void updateDisplay(ValueChangedEvent<Period?> 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))
{
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);

remainingTimeAdjustmentBox
.ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint)
.Delay(b.Duration - BREAK_FADE_DURATION)
.ResizeWidthTo(0);
remainingTimeBox.ResizeWidthTo(remainingTimeForCurrentPeriod);

remainingTimeBox
.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);

using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION))
{
fadeContainer.FadeOut(BREAK_FADE_DURATION);
breakArrows.Hide(BREAK_FADE_DURATION);
}
info.MoveToX(50)
.MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint);

using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION))
{
fadeContainer.FadeOut(BREAK_FADE_DURATION);
breakArrows.Hide(BREAK_FADE_DURATION);
}
}
}
Expand Down
21 changes: 14 additions & 7 deletions osu.Game/Screens/Play/BreakTracker.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// 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.

#nullable disable

using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
Expand All @@ -18,7 +16,7 @@ public partial class BreakTracker : Component
private readonly ScoreProcessor scoreProcessor;
private readonly double gameplayStartTime;

private PeriodTracker breaks;
private PeriodTracker breaks = new PeriodTracker(Enumerable.Empty<Period>());

/// <summary>
/// Whether the gameplay is currently in a break.
Expand All @@ -27,6 +25,8 @@ public partial class BreakTracker : Component

private readonly BindableBool isBreakTime = new BindableBool(true);

public readonly Bindable<Period?> CurrentPeriod = new Bindable<Period?>();

public IReadOnlyList<BreakPeriod> Breaks
{
set
Expand All @@ -39,7 +39,7 @@ public IReadOnlyList<BreakPeriod> Breaks
}
}

public BreakTracker(double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null)
public BreakTracker(double gameplayStartTime, ScoreProcessor scoreProcessor)
{
this.gameplayStartTime = gameplayStartTime;
this.scoreProcessor = scoreProcessor;
Expand All @@ -55,9 +55,16 @@ private void updateBreakTime()
{
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;
}
}
}
}
2 changes: 1 addition & 1 deletion osu.Game/Screens/Play/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ private Drawable createOverlayComponents(IWorkingBeatmap working)
{
Clock = DrawableRuleset.FrameStableClock,
ProcessCustomClock = false,
Breaks = working.Beatmap.Breaks
BreakTracker = breakTracker,
},
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
Expand Down
24 changes: 22 additions & 2 deletions osu.Game/Utils/PeriodTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

namespace osu.Game.Utils
Expand All @@ -24,8 +25,17 @@ public PeriodTracker(IEnumerable<Period> periods)
/// Whether the provided time is in any of the added periods.
/// </summary>
/// <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)
return false;

Expand All @@ -41,7 +51,15 @@ public bool IsInAny(double time)
}

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;
}
}

Expand All @@ -57,6 +75,8 @@ public readonly struct Period
/// </summary>
public readonly double End;

public double Duration => End - Start;

public Period(double start, double end)
{
if (start >= end)
Expand Down
Loading