Merge pull request #19776 from peppy/even-less-gameplay-clock

Remove `FrameStableClock` (and redirect usages to `FrameStabilityContainer`)
This commit is contained in:
Dan Balasescu 2022-08-15 23:21:47 +09:00 committed by GitHub
commit 5a88f9c7ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 90 additions and 81 deletions

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -30,7 +31,7 @@ namespace osu.Game.Tests.NonVisual
{ {
public List<Bindable<double>> MutableNonGameplayAdjustments { get; } = new List<Bindable<double>>(); public List<Bindable<double>> MutableNonGameplayAdjustments { get; } = new List<Bindable<double>>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments; public override IEnumerable<double> NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value);
public TestGameplayClock(IFrameBasedClock underlyingClock) public TestGameplayClock(IFrameBasedClock underlyingClock)
: base(underlyingClock) : base(underlyingClock)

View File

@ -137,13 +137,13 @@ namespace osu.Game.Tests.Visual.Gameplay
private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time); private void seekManualTo(double time) => AddStep($"seek manual clock to {time}", () => manualClock.CurrentTime = time);
private void confirmSeek(double time) => AddUntilStep($"wait for seek to {time}", () => consumer.Clock.CurrentTime == time); private void confirmSeek(double time) => AddUntilStep($"wait for seek to {time}", () => consumer.Clock.CurrentTime, () => Is.EqualTo(time));
private void checkFrameCount(int frames) => private void checkFrameCount(int frames) =>
AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames); AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames, () => Is.EqualTo(frames));
private void checkRate(double rate) => private void checkRate(double rate) =>
AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate); AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate, () => Is.EqualTo(rate));
public class ClockConsumingChild : CompositeDrawable public class ClockConsumingChild : CompositeDrawable
{ {

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 NUnit.Framework; using NUnit.Framework;
@ -21,22 +19,22 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestAllSamplesStopDuringSeek() public void TestAllSamplesStopDuringSeek()
{ {
DrawableSlider slider = null; DrawableSlider? slider = null;
PoolableSkinnableSample[] samples = null; PoolableSkinnableSample[] samples = null!;
ISamplePlaybackDisabler sampleDisabler = null; ISamplePlaybackDisabler sampleDisabler = null!;
AddUntilStep("get variables", () => AddUntilStep("get variables", () =>
{ {
sampleDisabler = Player; sampleDisabler = Player;
slider = Player.ChildrenOfType<DrawableSlider>().MinBy(s => s.HitObject.StartTime); slider = Player.ChildrenOfType<DrawableSlider>().MinBy(s => s.HitObject.StartTime);
samples = slider?.ChildrenOfType<PoolableSkinnableSample>().ToArray(); samples = slider.ChildrenOfType<PoolableSkinnableSample>().ToArray();
return slider != null; return slider != null;
}); });
AddUntilStep("wait for slider sliding then seek", () => AddUntilStep("wait for slider sliding then seek", () =>
{ {
if (!slider.Tracking.Value) if (slider?.Tracking.Value != true)
return false; return false;
if (!samples.Any(s => s.Playing)) if (!samples.Any(s => s.Playing))

View File

@ -363,7 +363,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private Player player => Stack.CurrentScreen as Player; private Player player => Stack.CurrentScreen as Player;
private double currentFrameStableTime private double currentFrameStableTime
=> player.ChildrenOfType<FrameStabilityContainer>().First().FrameStableClock.CurrentTime; => player.ChildrenOfType<FrameStabilityContainer>().First().CurrentTime;
private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true);

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.UI
public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both };
public override IFrameStableClock FrameStableClock => frameStabilityContainer.FrameStableClock; public override IFrameStableClock FrameStableClock => frameStabilityContainer;
private bool frameStablePlayback = true; private bool frameStablePlayback = true;

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; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -12,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -21,7 +20,9 @@ namespace osu.Game.Rulesets.UI
/// A container which consumes a parent gameplay clock and standardises frame counts for children. /// A container which consumes a parent gameplay clock and standardises frame counts for children.
/// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks. /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks.
/// </summary> /// </summary>
public class FrameStabilityContainer : Container, IHasReplayHandler [Cached(typeof(IGameplayClock))]
[Cached(typeof(IFrameStableClock))]
public class FrameStabilityContainer : Container, IHasReplayHandler, IFrameStableClock, IGameplayClock
{ {
private readonly double gameplayStartTime; private readonly double gameplayStartTime;
@ -35,16 +36,17 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
internal bool FrameStablePlayback = true; internal bool FrameStablePlayback = true;
public IFrameStableClock FrameStableClock => frameStableClock; public readonly Bindable<bool> IsCatchingUp = new Bindable<bool>();
[Cached(typeof(IGameplayClock))] public readonly Bindable<bool> WaitingOnFrames = new Bindable<bool>();
private readonly FrameStabilityClock frameStableClock;
public IBindable<bool> IsPaused { get; } = new BindableBool();
public FrameStabilityContainer(double gameplayStartTime = double.MinValue) public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
frameStableClock = new FrameStabilityClock(framedClock = new FramedClock(manualClock = new ManualClock())); framedClock = new FramedClock(manualClock = new ManualClock());
this.gameplayStartTime = gameplayStartTime; this.gameplayStartTime = gameplayStartTime;
} }
@ -53,7 +55,9 @@ namespace osu.Game.Rulesets.UI
private readonly FramedClock framedClock; private readonly FramedClock framedClock;
private IFrameBasedClock parentGameplayClock; private IGameplayClock? parentGameplayClock;
private IClock referenceClock = null!;
/// <summary> /// <summary>
/// The current direction of playback to be exposed to frame stable children. /// The current direction of playback to be exposed to frame stable children.
@ -63,20 +67,17 @@ namespace osu.Game.Rulesets.UI
/// </remarks> /// </remarks>
private int direction = 1; private int direction = 1;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader]
private void load(IGameplayClock clock) private void load(IGameplayClock? gameplayClock)
{ {
if (clock != null) if (gameplayClock != null)
{ {
parentGameplayClock = frameStableClock.ParentGameplayClock = clock; parentGameplayClock = gameplayClock;
((IBindable<bool>)frameStableClock.IsPaused).BindTo(clock.IsPaused); IsPaused.BindTo(parentGameplayClock.IsPaused);
} }
}
protected override void LoadComplete() referenceClock = gameplayClock ?? Clock;
{ Clock = this;
base.LoadComplete();
setClock();
} }
private PlaybackState state; private PlaybackState state;
@ -111,12 +112,12 @@ namespace osu.Game.Rulesets.UI
private void updateClock() private void updateClock()
{ {
if (frameStableClock.WaitingOnFrames.Value) if (WaitingOnFrames.Value)
{ {
// if waiting on frames, run one update loop to determine if frames have arrived. // if waiting on frames, run one update loop to determine if frames have arrived.
state = PlaybackState.Valid; state = PlaybackState.Valid;
} }
else if (frameStableClock.IsPaused.Value) else if (IsPaused.Value)
{ {
// time should not advance while paused, nor should anything run. // time should not advance while paused, nor should anything run.
state = PlaybackState.NotValid; state = PlaybackState.NotValid;
@ -127,12 +128,7 @@ namespace osu.Game.Rulesets.UI
state = PlaybackState.Valid; state = PlaybackState.Valid;
} }
if (parentGameplayClock == null) double proposedTime = referenceClock.CurrentTime;
setClock(); // LoadComplete may not be run yet, but we still want the clock.
Debug.Assert(parentGameplayClock != null);
double proposedTime = parentGameplayClock.CurrentTime;
if (FrameStablePlayback) if (FrameStablePlayback)
// if we require frame stability, the proposed time will be adjusted to move at most one known // if we require frame stability, the proposed time will be adjusted to move at most one known
@ -152,14 +148,14 @@ namespace osu.Game.Rulesets.UI
if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime)
direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; direction = proposedTime >= manualClock.CurrentTime ? 1 : -1;
double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); double timeBehind = Math.Abs(proposedTime - referenceClock.CurrentTime);
frameStableClock.IsCatchingUp.Value = timeBehind > 200; IsCatchingUp.Value = timeBehind > 200;
frameStableClock.WaitingOnFrames.Value = state == PlaybackState.NotValid; WaitingOnFrames.Value = state == PlaybackState.NotValid;
manualClock.CurrentTime = proposedTime; manualClock.CurrentTime = proposedTime;
manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; manualClock.Rate = Math.Abs(referenceClock.Rate) * direction;
manualClock.IsRunning = parentGameplayClock.IsRunning; manualClock.IsRunning = referenceClock.IsRunning;
// determine whether catch-up is required. // determine whether catch-up is required.
if (state == PlaybackState.Valid && timeBehind > 0) if (state == PlaybackState.Valid && timeBehind > 0)
@ -177,6 +173,8 @@ namespace osu.Game.Rulesets.UI
/// <returns>Whether playback is still valid.</returns> /// <returns>Whether playback is still valid.</returns>
private bool updateReplay(ref double proposedTime) private bool updateReplay(ref double proposedTime)
{ {
Debug.Assert(ReplayInputHandler != null);
double? newTime; double? newTime;
if (FrameStablePlayback) if (FrameStablePlayback)
@ -236,20 +234,51 @@ namespace osu.Game.Rulesets.UI
} }
} }
private void setClock() public ReplayInputHandler? ReplayInputHandler { get; set; }
#region Delegation of IFrameStableClock
public double CurrentTime => framedClock.CurrentTime;
public double Rate => framedClock.Rate;
public bool IsRunning => framedClock.IsRunning;
public void ProcessFrame() { }
public double ElapsedFrameTime => framedClock.ElapsedFrameTime;
public double FramesPerSecond => framedClock.FramesPerSecond;
public FrameTimeInfo TimeInfo => framedClock.TimeInfo;
#endregion
#region Delegation of IGameplayClock
public double TrueGameplayRate
{ {
if (parentGameplayClock == null) get
{ {
// in case a parent gameplay clock isn't available, just use the parent clock. double baseRate = Rate;
parentGameplayClock ??= Clock;
} foreach (double adjustment in NonGameplayAdjustments)
else {
{ if (Precision.AlmostEquals(adjustment, 0))
Clock = frameStableClock; return 0;
baseRate /= adjustment;
}
return baseRate;
} }
} }
public ReplayInputHandler ReplayInputHandler { get; set; } public double? StartTime => parentGameplayClock?.StartTime;
public IEnumerable<double> NonGameplayAdjustments => parentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<double>();
#endregion
private enum PlaybackState private enum PlaybackState
{ {
@ -270,24 +299,7 @@ namespace osu.Game.Rulesets.UI
Valid Valid
} }
private class FrameStabilityClock : GameplayClock, IFrameStableClock IBindable<bool> IFrameStableClock.IsCatchingUp => IsCatchingUp;
{ IBindable<bool> IFrameStableClock.WaitingOnFrames => WaitingOnFrames;
public IGameplayClock ParentGameplayClock;
public readonly Bindable<bool> IsCatchingUp = new Bindable<bool>();
public readonly Bindable<bool> WaitingOnFrames = new Bindable<bool>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<Bindable<double>>();
public FrameStabilityClock(FramedClock underlyingClock)
: base(underlyingClock)
{
}
IBindable<bool> IFrameStableClock.IsCatchingUp => IsCatchingUp;
IBindable<bool> IFrameStableClock.WaitingOnFrames => WaitingOnFrames;
}
} }
} }

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 osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Framework.Timing;

View File

@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play
IBindable<bool> IGameplayClock.IsPaused => IsPaused; IBindable<bool> IGameplayClock.IsPaused => IsPaused;
public virtual IEnumerable<Bindable<double>> NonGameplayAdjustments => Enumerable.Empty<Bindable<double>>(); public virtual IEnumerable<double> NonGameplayAdjustments => Enumerable.Empty<double>();
public GameplayClock(IFrameBasedClock underlyingClock) public GameplayClock(IFrameBasedClock underlyingClock)
{ {
@ -46,12 +46,12 @@ namespace osu.Game.Screens.Play
{ {
double baseRate = Rate; double baseRate = Rate;
foreach (var adjustment in NonGameplayAdjustments) foreach (double adjustment in NonGameplayAdjustments)
{ {
if (Precision.AlmostEquals(adjustment.Value, 0)) if (Precision.AlmostEquals(adjustment, 0))
return 0; return 0;
baseRate /= adjustment.Value; baseRate /= adjustment;
} }
return baseRate; return baseRate;

View File

@ -61,7 +61,7 @@ namespace osu.Game.Screens.Play
} }
} }
public IEnumerable<Bindable<double>> NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments; public IEnumerable<double> NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments;
/// <summary> /// <summary>
/// The final clock which is exposed to gameplay components. /// The final clock which is exposed to gameplay components.

View File

@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// All adjustments applied to this clock which don't come from gameplay or mods. /// All adjustments applied to this clock which don't come from gameplay or mods.
/// </summary> /// </summary>
IEnumerable<Bindable<double>> NonGameplayAdjustments { get; } IEnumerable<double> NonGameplayAdjustments { get; }
IBindable<bool> IsPaused { get; } IBindable<bool> IsPaused { get; }
} }

View File

@ -303,7 +303,7 @@ namespace osu.Game.Screens.Play
private class MasterGameplayClock : GameplayClock private class MasterGameplayClock : GameplayClock
{ {
public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>(); public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments; public override IEnumerable<double> NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value);
public MasterGameplayClock(FramedOffsetClock underlyingClock) public MasterGameplayClock(FramedOffsetClock underlyingClock)
: base(underlyingClock) : base(underlyingClock)