// 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 System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Framework.Utils; 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))] public class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock { /// /// Whether gameplay is paused. /// public IBindable IsPaused => isPaused; /// /// The source clock. Should generally not be used for any timekeeping purposes. /// public IClock SourceClock { get; private set; } /// /// 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 . /// Settting a start time will to the new 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 => startTime; set { startTime = value; Reset(); } } private double startTime; public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty(); 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 clock exposed to gameplay components as an . /// 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. public GameplayClockContainer(IClock sourceClock, bool applyOffsets = false) { SourceClock = sourceClock; RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] { GameplayClock = new FramedBeatmapClock(sourceClock, applyOffsets) { IsCoupled = false }, Content }; IsPaused.BindValueChanged(OnIsPausedChanged); } /// /// Starts gameplay and marks un-paused state. /// public virtual void Start() { ensureSourceClockSet(); isPaused.Value = false; // the clock may be stopped via internal means (ie. not via `IsPaused`). if (!GameplayClock.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); GameplayClock.Start(); } } /// /// Seek to a specific time in gameplay. /// /// The destination time to seek to. public 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() => isPaused.Value = true; /// /// Resets this and the source to an initial state ready for gameplay. /// /// Whether to start the clock immediately, if not already started. public void Reset(bool startClock = false) { // Manually stop the source in order to not affect the IsPaused state. GameplayClock.Stop(); ensureSourceClockSet(); Seek(StartTime); if (!IsPaused.Value || startClock) Start(); } /// /// Changes the source clock. /// /// The new source. protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(SourceClock = sourceClock); /// /// Ensures that the is set to , if it hasn't been given a source yet. /// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode, /// but not the actual source clock. /// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor, /// but it is not yet set on the adjustable source there. /// private void ensureSourceClockSet() { if (GameplayClock.Source == null) ChangeSource(SourceClock); } /// /// 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) GameplayClock.Stop(); else GameplayClock.Start(); } #region IAdjustableClock bool IAdjustableClock.Seek(double position) { Seek(position); return true; } void IAdjustableClock.Reset() => Reset(); public void ResetSpeedAdjustments() => throw new NotImplementedException(); 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; public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo; public double TrueGameplayRate { get { double baseRate = Rate; foreach (double adjustment in NonGameplayAdjustments) { if (Precision.AlmostEquals(adjustment, 0)) return 0; baseRate /= adjustment; } return baseRate; } } } }