// Copyright (c) ppy Pty Ltd . 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.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; namespace osu.Game.Screens.Play { /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// [Cached(typeof(IGameplayClock))] [Cached(typeof(GameplayClockContainer))] public partial class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { public IBindable IsPaused => isPaused; public bool IsRewinding => GameplayClock.IsRewinding; /// /// Invoked when a seek has been performed via /// public event Action? OnSeek; /// /// The time from which the clock should start. Will be seeked to on calling . /// Can be adjusted by calling with a time value. /// /// /// By default, a value of zero will be used. /// Importantly, the value will be inferred from the current beatmap in by default. /// public double StartTime { get; protected set; } public IAdjustableAudioComponent AdjustmentsFromMods { get; } = new AudioAdjustments(); private readonly BindableBool isPaused = new BindableBool(true); /// /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. /// This is the final source exposed to gameplay components via delegation in this class. /// protected readonly FramedBeatmapClock GameplayClock; protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; /// /// Creates a new . /// /// The source used for timing. /// Whether to apply platform, user and beatmap offsets to the mix. /// Whether decoupling logic should be applied on the source clock. public GameplayClockContainer(IClock sourceClock, bool applyOffsets, bool requireDecoupling) { RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] { GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling, sourceClock), Content }; } /// /// Starts gameplay and marks un-paused state. /// public void Start() { if (!isPaused.Value) return; isPaused.Value = false; // The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time. // Because we generally update our own current time quicker than children can query it (via Start/Seek/Update), // this means that the first frame ever exposed to children may have a non-zero current time. // // If the child component is not aware of the parent ElapsedFrameTime (which is the case for FrameStabilityContainer) // they will take on the new CurrentTime with a zero elapsed time. This can in turn cause components to behave incorrectly // if they are intending to trigger events at the precise StartTime (ie. DrawableStoryboardSample). // // By scheduling the start call, children are guaranteed to receive one frame at the original start time, allowing // then to progress with a correct locally calculated elapsed time. SchedulerAfterChildren.Add(() => { if (isPaused.Value) return; StartGameplayClock(); }); } /// /// Seek to a specific time in gameplay. /// /// The destination time to seek to. public virtual void Seek(double time) { Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}"); GameplayClock.Seek(time); OnSeek?.Invoke(); } /// /// Stops gameplay and marks paused state. /// public void Stop() { if (isPaused.Value) return; isPaused.Value = true; StopGameplayClock(); } protected virtual void StartGameplayClock() { Logger.Log($"{nameof(GameplayClockContainer)} started via call to {nameof(StartGameplayClock)}"); GameplayClock.Start(); } protected virtual void StopGameplayClock() { Logger.Log($"{nameof(GameplayClockContainer)} stopped via call to {nameof(StopGameplayClock)}"); GameplayClock.Stop(); } /// /// Resets this and the source to an initial state ready for gameplay. /// /// The time to seek to on resetting. If null, the existing will be used. /// Whether to start the clock immediately. If false and the clock was already paused, the clock will remain paused after this call. /// public void Reset(double? time = null, bool startClock = false) { bool wasPaused = isPaused.Value; // The intention of the Reset method is to get things into a known sane state. // As such, we intentionally stop the underlying clock directly here, bypassing Stop/StopGameplayClock. // This is to avoid any kind of isPaused state checks and frequency ramping (as provided by MasterGameplayClockContainer). GameplayClock.Stop(); if (time != null) StartTime = time.Value; Seek(StartTime); if (!wasPaused || startClock) Start(); } /// /// Changes the source clock. /// /// The new source. protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(sourceClock); #region IAdjustableClock bool IAdjustableClock.Seek(double position) { Seek(position); return true; } void IAdjustableClock.Reset() => Reset(); public virtual void ResetSpeedAdjustments() { } double IAdjustableClock.Rate { get => GameplayClock.Rate; set => throw new NotSupportedException(); } public double Rate => GameplayClock.Rate; public double CurrentTime => GameplayClock.CurrentTime; public bool IsRunning => GameplayClock.IsRunning; #endregion public void ProcessFrame() { // Handled via update. Don't process here to safeguard from external usages potentially processing frames additional times. } public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime; public double FramesPerSecond => GameplayClock.FramesPerSecond; } }