mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 08:02:55 +08:00
Move all clock related gameplay logic inside GameplayClockContainer
This commit is contained in:
parent
0b4f10950f
commit
58ef397f4f
151
osu.Game/Screens/Play/GameplayClockContainer.cs
Normal file
151
osu.Game/Screens/Play/GameplayClockContainer.cs
Normal file
@ -0,0 +1,151 @@
|
||||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the various intricacies of the gameplay clock.
|
||||
/// </summary>
|
||||
public class GameplayClockContainer : Container
|
||||
{
|
||||
private readonly WorkingBeatmap beatmap;
|
||||
|
||||
/// <summary>
|
||||
/// The original source (usually a <see cref="WorkingBeatmap"/>'s track).
|
||||
/// </summary>
|
||||
private readonly IAdjustableClock sourceClock;
|
||||
|
||||
public readonly BindableBool IsPaused = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// The decoupled clock used for gameplay. Should be used for seeks and clock control.
|
||||
/// </summary>
|
||||
private readonly DecoupleableInterpolatingFramedClock adjustableClock;
|
||||
|
||||
/// <summary>
|
||||
/// The final clock which is exposed to underlying components.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
private readonly GameplayClock gameplayClock;
|
||||
|
||||
public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 0.5,
|
||||
MaxValue = 2,
|
||||
Precision = 0.1,
|
||||
};
|
||||
|
||||
private Bindable<double> userAudioOffset;
|
||||
|
||||
private readonly FramedOffsetClock offsetClock;
|
||||
|
||||
public GameplayClockContainer(WorkingBeatmap beatmap, bool allowLeadIn, double gameplayStartTime)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
this.beatmap = beatmap;
|
||||
|
||||
sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock();
|
||||
|
||||
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
|
||||
|
||||
adjustableClock.Seek(allowLeadIn
|
||||
? Math.Min(0, gameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
|
||||
: gameplayStartTime);
|
||||
|
||||
adjustableClock.ProcessFrame();
|
||||
|
||||
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
|
||||
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
||||
var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
|
||||
|
||||
// the final usable gameplay clock with user-set offsets applied.
|
||||
offsetClock = new FramedOffsetClock(platformOffsetClock);
|
||||
|
||||
// the clock to be exposed via DI to children.
|
||||
gameplayClock = new GameplayClock(offsetClock);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
userAudioOffset.BindValueChanged(offset => offsetClock.Offset = offset.NewValue, true);
|
||||
|
||||
UserPlaybackRate.ValueChanged += _ => updateRate();
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
sourceClock.Reset();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
adjustableClock.ChangeSource(sourceClock);
|
||||
updateRate();
|
||||
|
||||
this.Delay(750).Schedule(() =>
|
||||
{
|
||||
if (!IsPaused.Value)
|
||||
{
|
||||
adjustableClock.Start();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
|
||||
// This accounts for the audio clock source potentially taking time to enter a completely stopped state
|
||||
adjustableClock.Seek(adjustableClock.CurrentTime);
|
||||
adjustableClock.Start();
|
||||
}
|
||||
|
||||
public void Seek(double time) => adjustableClock.Seek(time);
|
||||
|
||||
public void Stop() => adjustableClock.Stop();
|
||||
|
||||
public void ResetLocalAdjustments()
|
||||
{
|
||||
// In the case of replays, we may have changed the playback rate.
|
||||
UserPlaybackRate.Value = 1;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (!IsPaused.Value)
|
||||
offsetClock.ProcessFrame();
|
||||
|
||||
base.Update();
|
||||
}
|
||||
|
||||
private void updateRate()
|
||||
{
|
||||
if (sourceClock == null) return;
|
||||
|
||||
sourceClock.Rate = 1;
|
||||
foreach (var mod in beatmap.Mods.Value.OfType<IApplicableToClock>())
|
||||
mod.ApplyToClock(sourceClock);
|
||||
|
||||
sourceClock.Rate *= UserPlaybackRate.Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
public readonly PlaybackSettings PlaybackSettings;
|
||||
|
||||
public readonly VisualSettings VisualSettings;
|
||||
|
||||
//public readonly CollectionSettings CollectionSettings;
|
||||
|
||||
//public readonly DiscussionSettings DiscussionSettings;
|
||||
|
||||
public PlayerSettingsOverlay()
|
||||
|
@ -1,12 +1,12 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -40,7 +40,9 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private static bool hasShownNotificationOnce;
|
||||
|
||||
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IAdjustableClock adjustableClock)
|
||||
public Action<double> RequestSeek;
|
||||
|
||||
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@ -92,11 +94,9 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
Progress.Objects = rulesetContainer.Objects;
|
||||
Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
|
||||
Progress.RequestSeek = pos => adjustableClock.Seek(pos);
|
||||
Progress.RequestSeek = time => RequestSeek(time);
|
||||
|
||||
ModDisplay.Current.BindTo(working.Mods);
|
||||
|
||||
PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
|
@ -7,16 +7,13 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which handles pausing children, displaying a pause overlay with choices and processing the clock.
|
||||
/// Exposes a <see cref="GameplayClock"/> to children via DI.
|
||||
/// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/>
|
||||
/// A container which handles pausing children, displaying an overlay blocking its children during paused state.
|
||||
/// </summary>
|
||||
public class PausableGameplayContainer : Container
|
||||
{
|
||||
@ -44,46 +41,33 @@ namespace osu.Game.Screens.Play
|
||||
public Action OnRetry;
|
||||
public Action OnQuit;
|
||||
|
||||
private readonly FramedClock offsetClock;
|
||||
private readonly DecoupleableInterpolatingFramedClock adjustableClock;
|
||||
|
||||
/// <summary>
|
||||
/// The final clock which is exposed to underlying components.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
private readonly GameplayClock gameplayClock;
|
||||
public Action Stop;
|
||||
public Action Start;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PausableGameplayContainer"/>.
|
||||
/// </summary>
|
||||
/// <param name="offsetClock">The gameplay clock. This is the clock that will process frames. Includes user/system offsets.</param>
|
||||
/// <param name="adjustableClock">The seekable clock. This is the clock that will be paused and resumed. Should not be processed (it is processed automatically by <see cref="offsetClock"/>).</param>
|
||||
public PausableGameplayContainer(FramedClock offsetClock, DecoupleableInterpolatingFramedClock adjustableClock)
|
||||
public PausableGameplayContainer()
|
||||
{
|
||||
this.offsetClock = offsetClock;
|
||||
this.adjustableClock = adjustableClock;
|
||||
|
||||
gameplayClock = new GameplayClock(offsetClock);
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
AddInternal(content = new Container
|
||||
InternalChildren = new[]
|
||||
{
|
||||
Clock = this.offsetClock,
|
||||
ProcessCustomClock = false,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
|
||||
AddInternal(pauseOverlay = new PauseOverlay
|
||||
{
|
||||
OnResume = () =>
|
||||
content = new Container
|
||||
{
|
||||
IsResuming = true;
|
||||
this.Delay(400).Schedule(Resume);
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
OnRetry = () => OnRetry(),
|
||||
OnQuit = () => OnQuit(),
|
||||
});
|
||||
pauseOverlay = new PauseOverlay
|
||||
{
|
||||
OnResume = () =>
|
||||
{
|
||||
IsResuming = true;
|
||||
this.Delay(400).Schedule(Resume);
|
||||
},
|
||||
OnRetry = () => OnRetry(),
|
||||
OnQuit = () => OnQuit(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called.
|
||||
@ -93,7 +77,7 @@ namespace osu.Game.Screens.Play
|
||||
if (IsPaused.Value) return;
|
||||
|
||||
// stop the seekable clock (stops the audio eventually)
|
||||
adjustableClock.Stop();
|
||||
Stop?.Invoke();
|
||||
IsPaused.Value = true;
|
||||
|
||||
pauseOverlay.Show();
|
||||
@ -105,14 +89,12 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
if (!IsPaused.Value) return;
|
||||
|
||||
IsPaused.Value = false;
|
||||
IsResuming = false;
|
||||
lastPauseActionTime = Time.Current;
|
||||
|
||||
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
|
||||
// This accounts for the audio clock source potentially taking time to enter a completely stopped state
|
||||
adjustableClock.Seek(adjustableClock.CurrentTime);
|
||||
adjustableClock.Start();
|
||||
IsPaused.Value = false;
|
||||
|
||||
Start?.Invoke();
|
||||
|
||||
pauseOverlay.Hide();
|
||||
}
|
||||
@ -131,9 +113,6 @@ namespace osu.Game.Screens.Play
|
||||
if (!game.IsActive.Value && CanPause)
|
||||
Pause();
|
||||
|
||||
if (!IsPaused.Value)
|
||||
offsetClock.ProcessFrame();
|
||||
|
||||
base.Update();
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@ -16,7 +14,6 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@ -53,7 +50,6 @@ namespace osu.Game.Screens.Play
|
||||
public bool AllowResults { get; set; } = true;
|
||||
|
||||
private Bindable<bool> mouseWheelDisabled;
|
||||
private Bindable<double> userAudioOffset;
|
||||
|
||||
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
|
||||
|
||||
@ -62,13 +58,6 @@ namespace osu.Game.Screens.Play
|
||||
public CursorContainer Cursor => RulesetContainer.Cursor;
|
||||
public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value;
|
||||
|
||||
private IAdjustableClock sourceClock;
|
||||
|
||||
/// <summary>
|
||||
/// The decoupled clock used for gameplay. Should be used for seeks and clock control.
|
||||
/// </summary>
|
||||
private DecoupleableInterpolatingFramedClock adjustableClock;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; }
|
||||
|
||||
@ -98,25 +87,117 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true;
|
||||
|
||||
private GameplayClockContainer gameplayClockContainer;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
|
||||
{
|
||||
this.api = api;
|
||||
|
||||
WorkingBeatmap working = Beatmap.Value;
|
||||
if (working is DummyWorkingBeatmap)
|
||||
WorkingBeatmap working = loadBeatmap();
|
||||
|
||||
if (working == null)
|
||||
return;
|
||||
|
||||
sampleRestart = audio.Sample.Get(@"Gameplay/restart");
|
||||
|
||||
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
|
||||
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
|
||||
IBeatmap beatmap;
|
||||
ScoreProcessor = RulesetContainer.CreateScoreProcessor();
|
||||
if (!ScoreProcessor.Mode.Disabled)
|
||||
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
|
||||
|
||||
InternalChild = gameplayClockContainer = new GameplayClockContainer(working, AllowLeadIn, RulesetContainer.GameplayStartTime);
|
||||
|
||||
gameplayClockContainer.Children = new Drawable[]
|
||||
{
|
||||
PausableGameplayContainer = new PausableGameplayContainer
|
||||
{
|
||||
Retries = RestartCount,
|
||||
OnRetry = restart,
|
||||
OnQuit = performUserRequestedExit,
|
||||
Start = gameplayClockContainer.Start,
|
||||
Stop = gameplayClockContainer.Stop,
|
||||
|
||||
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
|
||||
Children = new Container[]
|
||||
{
|
||||
StoryboardContainer = CreateStoryboardContainer(),
|
||||
new ScalingContainer(ScalingMode.Gameplay)
|
||||
{
|
||||
Child = new LocalSkinOverrideContainer(working.Skin)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = RulesetContainer
|
||||
}
|
||||
},
|
||||
new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Breaks = working.Beatmap.Breaks
|
||||
},
|
||||
new ScalingContainer(ScalingMode.Gameplay)
|
||||
{
|
||||
Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
|
||||
},
|
||||
HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working)
|
||||
{
|
||||
RequestSeek = gameplayClockContainer.Seek,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new SkipOverlay(RulesetContainer.GameplayStartTime)
|
||||
{
|
||||
RequestSeek = gameplayClockContainer.Seek
|
||||
},
|
||||
}
|
||||
},
|
||||
failOverlay = new FailOverlay
|
||||
{
|
||||
OnRetry = restart,
|
||||
OnQuit = performUserRequestedExit,
|
||||
},
|
||||
new HotkeyRetryOverlay
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
|
||||
fadeOut(true);
|
||||
restart();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// bind clock into components that require it
|
||||
PausableGameplayContainer.IsPaused.BindTo(gameplayClockContainer.IsPaused);
|
||||
RulesetContainer.IsPaused.BindTo(gameplayClockContainer.IsPaused);
|
||||
HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTo(gameplayClockContainer.UserPlaybackRate);
|
||||
|
||||
HUDOverlay.HoldToQuit.Action = performUserRequestedExit;
|
||||
HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
|
||||
|
||||
if (ShowStoryboard.Value)
|
||||
initializeStoryboard(false);
|
||||
|
||||
// Bind ScoreProcessor to ourselves
|
||||
ScoreProcessor.AllJudged += onCompletion;
|
||||
ScoreProcessor.Failed += onFail;
|
||||
|
||||
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>())
|
||||
mod.ApplyToScoreProcessor(ScoreProcessor);
|
||||
}
|
||||
|
||||
private WorkingBeatmap loadBeatmap()
|
||||
{
|
||||
WorkingBeatmap working = Beatmap.Value;
|
||||
if (working is DummyWorkingBeatmap)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
beatmap = working.Beatmap;
|
||||
var beatmap = working.Beatmap;
|
||||
|
||||
if (beatmap == null)
|
||||
throw new InvalidOperationException("Beatmap was not loaded");
|
||||
@ -140,119 +221,17 @@ namespace osu.Game.Screens.Play
|
||||
if (!RulesetContainer.Objects.Any())
|
||||
{
|
||||
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Could not load beatmap sucessfully!");
|
||||
//couldn't load, hard abort!
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock();
|
||||
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
|
||||
|
||||
adjustableClock.Seek(AllowLeadIn
|
||||
? Math.Min(0, RulesetContainer.GameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
|
||||
: RulesetContainer.GameplayStartTime);
|
||||
|
||||
adjustableClock.ProcessFrame();
|
||||
|
||||
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
|
||||
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
||||
var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
|
||||
|
||||
// the final usable gameplay clock with user-set offsets applied.
|
||||
var offsetClock = new FramedOffsetClock(platformOffsetClock);
|
||||
|
||||
userAudioOffset.ValueChanged += offset => offsetClock.Offset = offset.NewValue;
|
||||
userAudioOffset.TriggerChange();
|
||||
|
||||
ScoreProcessor = RulesetContainer.CreateScoreProcessor();
|
||||
if (!ScoreProcessor.Mode.Disabled)
|
||||
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
PausableGameplayContainer = new PausableGameplayContainer(offsetClock, adjustableClock)
|
||||
{
|
||||
Retries = RestartCount,
|
||||
OnRetry = restart,
|
||||
OnQuit = performUserRequestedExit,
|
||||
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
|
||||
Children = new Container[]
|
||||
{
|
||||
StoryboardContainer = CreateStoryboardContainer(),
|
||||
new ScalingContainer(ScalingMode.Gameplay)
|
||||
{
|
||||
Child = new LocalSkinOverrideContainer(working.Skin)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = RulesetContainer
|
||||
}
|
||||
},
|
||||
new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Breaks = beatmap.Breaks
|
||||
},
|
||||
new ScalingContainer(ScalingMode.Gameplay)
|
||||
{
|
||||
Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
|
||||
},
|
||||
HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, adjustableClock)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
new SkipOverlay(RulesetContainer.GameplayStartTime)
|
||||
{
|
||||
RequestSeek = time => adjustableClock.Seek(time)
|
||||
},
|
||||
}
|
||||
},
|
||||
failOverlay = new FailOverlay
|
||||
{
|
||||
OnRetry = restart,
|
||||
OnQuit = performUserRequestedExit,
|
||||
},
|
||||
new HotkeyRetryOverlay
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
if (!this.IsCurrentScreen()) return;
|
||||
|
||||
fadeOut(true);
|
||||
restart();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
HUDOverlay.HoldToQuit.Action = performUserRequestedExit;
|
||||
HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
|
||||
|
||||
RulesetContainer.IsPaused.BindTo(PausableGameplayContainer.IsPaused);
|
||||
|
||||
if (ShowStoryboard.Value)
|
||||
initializeStoryboard(false);
|
||||
|
||||
// Bind ScoreProcessor to ourselves
|
||||
ScoreProcessor.AllJudged += onCompletion;
|
||||
ScoreProcessor.Failed += onFail;
|
||||
|
||||
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>())
|
||||
mod.ApplyToScoreProcessor(ScoreProcessor);
|
||||
}
|
||||
|
||||
private void applyRateFromMods()
|
||||
{
|
||||
if (sourceClock == null) return;
|
||||
|
||||
sourceClock.Rate = 1;
|
||||
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToClock>())
|
||||
mod.ApplyToClock(sourceClock);
|
||||
return working;
|
||||
}
|
||||
|
||||
private void performUserRequestedExit()
|
||||
@ -321,7 +300,7 @@ namespace osu.Game.Screens.Play
|
||||
if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail))
|
||||
return false;
|
||||
|
||||
adjustableClock.Stop();
|
||||
gameplayClockContainer.Stop();
|
||||
|
||||
HasFailed = true;
|
||||
failOverlay.Retries = RestartCount;
|
||||
@ -355,24 +334,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
sourceClock.Reset();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
adjustableClock.ChangeSource(sourceClock);
|
||||
applyRateFromMods();
|
||||
|
||||
this.Delay(750).Schedule(() =>
|
||||
{
|
||||
if (!PausableGameplayContainer.IsPaused.Value)
|
||||
{
|
||||
adjustableClock.Start();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
gameplayClockContainer.Restart();
|
||||
|
||||
PausableGameplayContainer.Alpha = 0;
|
||||
PausableGameplayContainer.FadeIn(750, Easing.OutQuint);
|
||||
@ -395,8 +357,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true))
|
||||
{
|
||||
// In the case of replays, we may have changed the playback rate.
|
||||
applyRateFromMods();
|
||||
gameplayClockContainer.ResetLocalAdjustments();
|
||||
|
||||
fadeOut();
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
@ -16,7 +15,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
protected override string Title => @"playback";
|
||||
|
||||
public IAdjustableClock AdjustableClock { set; get; }
|
||||
public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 0.5,
|
||||
MaxValue = 2,
|
||||
Precision = 0.1,
|
||||
};
|
||||
|
||||
private readonly PlayerSliderBar<double> rateSlider;
|
||||
|
||||
@ -47,31 +52,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
}
|
||||
},
|
||||
},
|
||||
rateSlider = new PlayerSliderBar<double>
|
||||
{
|
||||
Bindable = new BindableDouble(1)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 0.5,
|
||||
MaxValue = 2,
|
||||
Precision = 0.1,
|
||||
},
|
||||
}
|
||||
rateSlider = new PlayerSliderBar<double> { Bindable = UserPlaybackRate }
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (AdjustableClock == null)
|
||||
return;
|
||||
|
||||
var clockRate = AdjustableClock.Rate;
|
||||
|
||||
// can't trigger this line instantly as the underlying clock may not be ready to accept adjustments yet.
|
||||
rateSlider.Bindable.ValueChanged += multiplier => AdjustableClock.Rate = clockRate * multiplier.NewValue;
|
||||
|
||||
rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user