A game framework written with osu! in mind.
at master 6.9 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4#nullable enable 5 6using System; 7 8namespace osu.Framework.Timing 9{ 10 /// <summary> 11 /// Adds the ability to keep the clock running even when the underlying source has stopped or cannot handle the current time range. 12 /// This is handled by performing seeks on the underlying source and checking whether they were successful or not. 13 /// On failure to seek, we take over with an internal clock until control can be returned to the actual source. 14 /// 15 /// This clock type removes the requirement of having a source set. 16 /// 17 /// If a <see cref="InterpolatingFramedClock.Source"/> is set, it is presumed that we have exclusive control over operations on it. 18 /// This is used to our advantage to allow correct <see cref="IsRunning"/> state tracking in the event of cross-thread communication delays (with an audio thread, for instance). 19 /// </summary> 20 public class DecoupleableInterpolatingFramedClock : InterpolatingFramedClock, IAdjustableClock 21 { 22 /// <summary> 23 /// Specify whether we are coupled 1:1 to SourceClock. If not, we can independently continue operation. 24 /// </summary> 25 public bool IsCoupled = true; 26 27 /// <summary> 28 /// In some cases we should always use the interpolated source. 29 /// </summary> 30 private bool useInterpolatedSourceTime => IsRunning && FramedSourceClock?.IsRunning == true; 31 32 private readonly FramedClock decoupledClock; 33 private readonly StopwatchClock decoupledStopwatch; 34 35 /// <summary> 36 /// We need to be able to pass on adjustments to the source if it supports them. 37 /// </summary> 38 private IAdjustableClock? adjustableSource => Source as IAdjustableClock; 39 40 public override double CurrentTime => currentTime; 41 42 private double currentTime; 43 44 public double ProposedCurrentTime => useInterpolatedSourceTime ? base.CurrentTime : decoupledClock.CurrentTime; 45 46 public double ProposedElapsedTime => useInterpolatedSourceTime ? base.ElapsedFrameTime : decoupledClock.ElapsedFrameTime; 47 48 public override bool IsRunning => decoupledClock.IsRunning; // we always want to use our local IsRunning state, as it is more correct. 49 50 private double elapsedFrameTime; 51 52 public override double ElapsedFrameTime => elapsedFrameTime; 53 54 public override double Rate 55 { 56 get => Source?.Rate ?? 1; 57 set 58 { 59 if (adjustableSource == null) 60 throw new NotSupportedException("Source is not adjustable."); 61 62 adjustableSource.Rate = value; 63 } 64 } 65 66 public void ResetSpeedAdjustments() => Rate = 1; 67 68 public DecoupleableInterpolatingFramedClock() 69 { 70 decoupledClock = new FramedClock(decoupledStopwatch = new StopwatchClock()); 71 } 72 73 public override void ProcessFrame() 74 { 75 base.ProcessFrame(); 76 77 bool sourceRunning = Source?.IsRunning ?? false; 78 79 decoupledStopwatch.Rate = adjustableSource?.Rate ?? 1; 80 81 // if interpolating based on the source, keep the decoupled clock in sync with the interpolated time. 82 if (IsCoupled && sourceRunning) 83 decoupledStopwatch.Seek(base.CurrentTime); 84 85 // process the decoupled clock to update the current proposed time. 86 decoupledClock.ProcessFrame(); 87 88 // if the source clock is started as a result of becoming capable of handling the decoupled time, the proposed time may change to reflect the interpolated source time. 89 // however the interpolated source time that was calculated inside base.ProcessFrame() (above) did not consider the current (post-seek) time of the source. 90 // in all other cases the proposed time will match before and after clocks are started/stopped. 91 double proposedTime = ProposedCurrentTime; 92 double elapsedTime = ProposedElapsedTime; 93 94 if (IsRunning) 95 { 96 if (IsCoupled) 97 { 98 // when coupled, we want to stop when our source clock stops. 99 if (!sourceRunning) 100 Stop(); 101 } 102 else 103 { 104 // when decoupled and running, we should try to start the source clock it if it's capable of handling the current time. 105 if (!sourceRunning) 106 Start(); 107 } 108 } 109 else if (IsCoupled && sourceRunning) 110 { 111 // when coupled and not running, we want to start when the source clock starts. 112 Start(); 113 } 114 115 elapsedFrameTime = elapsedTime; 116 117 // the source may be started during playback but remain behind the current time in the playback direction for a number of frames. 118 // in such cases, the current time should remain paused until the source time catches up. 119 currentTime = elapsedFrameTime < 0 ? Math.Min(currentTime, proposedTime) : Math.Max(currentTime, proposedTime); 120 } 121 122 public override void ChangeSource(IClock? source) 123 { 124 if (source == null) return; 125 126 // transfer our value to the source clock. 127 (source as IAdjustableClock)?.Seek(CurrentTime); 128 129 base.ChangeSource(source); 130 } 131 132 public void Reset() 133 { 134 IsCoupled = true; 135 136 adjustableSource?.Reset(); 137 decoupledStopwatch.Reset(); 138 } 139 140 public void Start() 141 { 142 if (adjustableSource?.IsRunning == false) 143 { 144 if (adjustableSource.Seek(ProposedCurrentTime)) 145 //only start the source clock if our time values match. 146 //this handles the case where we seeked to an unsupported value and the source clock is out of sync. 147 adjustableSource.Start(); 148 } 149 150 decoupledStopwatch.Start(); 151 } 152 153 public void Stop() 154 { 155 decoupledStopwatch.Stop(); 156 adjustableSource?.Stop(); 157 } 158 159 public bool Seek(double position) 160 { 161 try 162 { 163 bool success = adjustableSource?.Seek(position) != false; 164 165 if (IsCoupled) 166 return success; 167 168 if (!success) 169 //if we failed to seek then stop the source and use decoupled mode. 170 adjustableSource?.Stop(); 171 172 return decoupledStopwatch.Seek(position); 173 } 174 finally 175 { 176 ProcessFrame(); 177 } 178 } 179 } 180}