// 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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; namespace osu.Game.Screens.Play { /// /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. /// public abstract class GameplayClockContainer : Container, IAdjustableClock { /// /// The final clock which is exposed to gameplay components. /// public GameplayClock GameplayClock { get; private set; } /// /// Whether gameplay is paused. /// public readonly BindableBool IsPaused = new BindableBool(); /// /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. /// protected readonly DecoupleableInterpolatingFramedClock AdjustableSource; /// /// Creates a new . /// /// The source used for timing. protected GameplayClockContainer(IClock sourceClock) { RelativeSizeAxes = Axes.Both; AdjustableSource = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; AdjustableSource.ChangeSource(sourceClock); IsPaused.BindValueChanged(OnIsPausedChanged); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); GameplayClock.IsPaused.BindTo(IsPaused); return dependencies; } /// /// Starts gameplay. /// public virtual void Start() { if (!AdjustableSource.IsRunning) { // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time // This accounts for the clock source potentially taking time to enter a completely stopped state Seek(GameplayClock.CurrentTime); AdjustableSource.Start(); } IsPaused.Value = false; } /// /// Seek to a specific time in gameplay. /// /// The destination time to seek to. public virtual void Seek(double time) => AdjustableSource.Seek(time); /// /// Stops gameplay. /// public virtual void Stop() => IsPaused.Value = true; /// /// Restarts gameplay. /// public virtual void Restart() { AdjustableSource.Seek(0); AdjustableSource.Stop(); if (!IsPaused.Value) Start(); } protected override void Update() { if (!IsPaused.Value) GameplayClock.UnderlyingClock.ProcessFrame(); base.Update(); } /// /// Invoked when the value of is changed to start or stop the clock. /// /// Whether the clock should now be paused. protected virtual void OnIsPausedChanged(ValueChangedEvent isPaused) { if (isPaused.NewValue) AdjustableSource.Stop(); else AdjustableSource.Start(); } /// /// Creates the final which is exposed via DI to be used by gameplay components. /// /// /// Any intermediate clocks such as platform offsets should be applied here. /// /// The providing the source time. /// The final . protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source); #region IAdjustableClock bool IAdjustableClock.Seek(double position) { Seek(position); return true; } void IAdjustableClock.Reset() { Restart(); Stop(); } public void ResetSpeedAdjustments() { } double IAdjustableClock.Rate { get => GameplayClock.Rate; set => throw new NotSupportedException(); } double IClock.Rate => GameplayClock.Rate; public double CurrentTime => GameplayClock.CurrentTime; public bool IsRunning => GameplayClock.IsRunning; #endregion } }