mirror of
https://github.com/ppy/osu.git
synced 2025-02-06 21:52:58 +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 PlaybackSettings PlaybackSettings;
|
||||||
|
|
||||||
public readonly VisualSettings VisualSettings;
|
public readonly VisualSettings VisualSettings;
|
||||||
|
|
||||||
//public readonly CollectionSettings CollectionSettings;
|
//public readonly CollectionSettings CollectionSettings;
|
||||||
|
|
||||||
//public readonly DiscussionSettings DiscussionSettings;
|
//public readonly DiscussionSettings DiscussionSettings;
|
||||||
|
|
||||||
public PlayerSettingsOverlay()
|
public PlayerSettingsOverlay()
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
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.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Timing;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@ -40,7 +40,9 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
private static bool hasShownNotificationOnce;
|
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;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
@ -92,11 +94,9 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
Progress.Objects = rulesetContainer.Objects;
|
Progress.Objects = rulesetContainer.Objects;
|
||||||
Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
|
Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
|
||||||
Progress.RequestSeek = pos => adjustableClock.Seek(pos);
|
Progress.RequestSeek = time => RequestSeek(time);
|
||||||
|
|
||||||
ModDisplay.Current.BindTo(working.Mods);
|
ModDisplay.Current.BindTo(working.Mods);
|
||||||
|
|
||||||
PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
|
@ -7,16 +7,13 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
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.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play
|
namespace osu.Game.Screens.Play
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A container which handles pausing children, displaying a pause overlay with choices and processing the clock.
|
/// A container which handles pausing children, displaying an overlay blocking its children during paused state.
|
||||||
/// Exposes a <see cref="GameplayClock"/> to children via DI.
|
|
||||||
/// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PausableGameplayContainer : Container
|
public class PausableGameplayContainer : Container
|
||||||
{
|
{
|
||||||
@ -44,37 +41,23 @@ namespace osu.Game.Screens.Play
|
|||||||
public Action OnRetry;
|
public Action OnRetry;
|
||||||
public Action OnQuit;
|
public Action OnQuit;
|
||||||
|
|
||||||
private readonly FramedClock offsetClock;
|
public Action Stop;
|
||||||
private readonly DecoupleableInterpolatingFramedClock adjustableClock;
|
public Action Start;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The final clock which is exposed to underlying components.
|
|
||||||
/// </summary>
|
|
||||||
[Cached]
|
|
||||||
private readonly GameplayClock gameplayClock;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="PausableGameplayContainer"/>.
|
/// Creates a new <see cref="PausableGameplayContainer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="offsetClock">The gameplay clock. This is the clock that will process frames. Includes user/system offsets.</param>
|
public PausableGameplayContainer()
|
||||||
/// <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)
|
|
||||||
{
|
{
|
||||||
this.offsetClock = offsetClock;
|
|
||||||
this.adjustableClock = adjustableClock;
|
|
||||||
|
|
||||||
gameplayClock = new GameplayClock(offsetClock);
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
AddInternal(content = new Container
|
InternalChildren = new[]
|
||||||
|
{
|
||||||
|
content = new Container
|
||||||
{
|
{
|
||||||
Clock = this.offsetClock,
|
|
||||||
ProcessCustomClock = false,
|
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
});
|
},
|
||||||
|
pauseOverlay = new PauseOverlay
|
||||||
AddInternal(pauseOverlay = new PauseOverlay
|
|
||||||
{
|
{
|
||||||
OnResume = () =>
|
OnResume = () =>
|
||||||
{
|
{
|
||||||
@ -83,7 +66,8 @@ namespace osu.Game.Screens.Play
|
|||||||
},
|
},
|
||||||
OnRetry = () => OnRetry(),
|
OnRetry = () => OnRetry(),
|
||||||
OnQuit = () => OnQuit(),
|
OnQuit = () => OnQuit(),
|
||||||
});
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called.
|
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;
|
if (IsPaused.Value) return;
|
||||||
|
|
||||||
// stop the seekable clock (stops the audio eventually)
|
// stop the seekable clock (stops the audio eventually)
|
||||||
adjustableClock.Stop();
|
Stop?.Invoke();
|
||||||
IsPaused.Value = true;
|
IsPaused.Value = true;
|
||||||
|
|
||||||
pauseOverlay.Show();
|
pauseOverlay.Show();
|
||||||
@ -105,14 +89,12 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
if (!IsPaused.Value) return;
|
if (!IsPaused.Value) return;
|
||||||
|
|
||||||
IsPaused.Value = false;
|
|
||||||
IsResuming = false;
|
IsResuming = false;
|
||||||
lastPauseActionTime = Time.Current;
|
lastPauseActionTime = Time.Current;
|
||||||
|
|
||||||
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
|
IsPaused.Value = false;
|
||||||
// This accounts for the audio clock source potentially taking time to enter a completely stopped state
|
|
||||||
adjustableClock.Seek(adjustableClock.CurrentTime);
|
Start?.Invoke();
|
||||||
adjustableClock.Start();
|
|
||||||
|
|
||||||
pauseOverlay.Hide();
|
pauseOverlay.Hide();
|
||||||
}
|
}
|
||||||
@ -131,9 +113,6 @@ namespace osu.Game.Screens.Play
|
|||||||
if (!game.IsActive.Value && CanPause)
|
if (!game.IsActive.Value && CanPause)
|
||||||
Pause();
|
Pause();
|
||||||
|
|
||||||
if (!IsPaused.Value)
|
|
||||||
offsetClock.ProcessFrame();
|
|
||||||
|
|
||||||
base.Update();
|
base.Update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using osu.Framework;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
@ -16,7 +14,6 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Framework.Timing;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
@ -53,7 +50,6 @@ namespace osu.Game.Screens.Play
|
|||||||
public bool AllowResults { get; set; } = true;
|
public bool AllowResults { get; set; } = true;
|
||||||
|
|
||||||
private Bindable<bool> mouseWheelDisabled;
|
private Bindable<bool> mouseWheelDisabled;
|
||||||
private Bindable<double> userAudioOffset;
|
|
||||||
|
|
||||||
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
|
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
|
||||||
|
|
||||||
@ -62,13 +58,6 @@ namespace osu.Game.Screens.Play
|
|||||||
public CursorContainer Cursor => RulesetContainer.Cursor;
|
public CursorContainer Cursor => RulesetContainer.Cursor;
|
||||||
public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value;
|
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]
|
[Resolved]
|
||||||
private ScoreManager scoreManager { get; set; }
|
private ScoreManager scoreManager { get; set; }
|
||||||
|
|
||||||
@ -98,25 +87,117 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true;
|
public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true;
|
||||||
|
|
||||||
|
private GameplayClockContainer gameplayClockContainer;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
|
private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
|
||||||
{
|
{
|
||||||
this.api = api;
|
this.api = api;
|
||||||
|
|
||||||
WorkingBeatmap working = Beatmap.Value;
|
WorkingBeatmap working = loadBeatmap();
|
||||||
if (working is DummyWorkingBeatmap)
|
|
||||||
|
if (working == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
sampleRestart = audio.Sample.Get(@"Gameplay/restart");
|
sampleRestart = audio.Sample.Get(@"Gameplay/restart");
|
||||||
|
|
||||||
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
|
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
|
try
|
||||||
{
|
{
|
||||||
beatmap = working.Beatmap;
|
var beatmap = working.Beatmap;
|
||||||
|
|
||||||
if (beatmap == null)
|
if (beatmap == null)
|
||||||
throw new InvalidOperationException("Beatmap was not loaded");
|
throw new InvalidOperationException("Beatmap was not loaded");
|
||||||
@ -140,119 +221,17 @@ namespace osu.Game.Screens.Play
|
|||||||
if (!RulesetContainer.Objects.Any())
|
if (!RulesetContainer.Objects.Any())
|
||||||
{
|
{
|
||||||
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
|
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Error(e, "Could not load beatmap sucessfully!");
|
Logger.Error(e, "Could not load beatmap sucessfully!");
|
||||||
//couldn't load, hard abort!
|
//couldn't load, hard abort!
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock();
|
return working;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performUserRequestedExit()
|
private void performUserRequestedExit()
|
||||||
@ -321,7 +300,7 @@ namespace osu.Game.Screens.Play
|
|||||||
if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail))
|
if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
adjustableClock.Stop();
|
gameplayClockContainer.Stop();
|
||||||
|
|
||||||
HasFailed = true;
|
HasFailed = true;
|
||||||
failOverlay.Retries = RestartCount;
|
failOverlay.Retries = RestartCount;
|
||||||
@ -355,24 +334,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
|
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
|
||||||
|
|
||||||
Task.Run(() =>
|
gameplayClockContainer.Restart();
|
||||||
{
|
|
||||||
sourceClock.Reset();
|
|
||||||
|
|
||||||
Schedule(() =>
|
|
||||||
{
|
|
||||||
adjustableClock.ChangeSource(sourceClock);
|
|
||||||
applyRateFromMods();
|
|
||||||
|
|
||||||
this.Delay(750).Schedule(() =>
|
|
||||||
{
|
|
||||||
if (!PausableGameplayContainer.IsPaused.Value)
|
|
||||||
{
|
|
||||||
adjustableClock.Start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
PausableGameplayContainer.Alpha = 0;
|
PausableGameplayContainer.Alpha = 0;
|
||||||
PausableGameplayContainer.FadeIn(750, Easing.OutQuint);
|
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))
|
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.
|
gameplayClockContainer.ResetLocalAdjustments();
|
||||||
applyRateFromMods();
|
|
||||||
fadeOut();
|
fadeOut();
|
||||||
return base.OnExiting(next);
|
return base.OnExiting(next);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using osu.Framework.Bindables;
|
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.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
|
|
||||||
@ -16,7 +15,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
|||||||
|
|
||||||
protected override string Title => @"playback";
|
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;
|
private readonly PlayerSliderBar<double> rateSlider;
|
||||||
|
|
||||||
@ -47,31 +52,13 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rateSlider = new PlayerSliderBar<double>
|
rateSlider = new PlayerSliderBar<double> { Bindable = UserPlaybackRate }
|
||||||
{
|
|
||||||
Bindable = new BindableDouble(1)
|
|
||||||
{
|
|
||||||
Default = 1,
|
|
||||||
MinValue = 0.5,
|
|
||||||
MaxValue = 2,
|
|
||||||
Precision = 0.1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.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);
|
rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user