A game framework written with osu! in mind.
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 /// A clock which uses an internal stopwatch to interpolate (smooth out) a source.
12 /// Note that this will NOT function unless a source has been set.
13 /// </summary>
14 public class InterpolatingFramedClock : IFrameBasedClock, ISourceChangeableClock
15 {
16 private readonly FramedClock clock = new FramedClock(new StopwatchClock(true));
17
18 public IClock? Source { get; private set; }
19
20 protected IFrameBasedClock? FramedSourceClock;
21 protected double LastInterpolatedTime;
22 protected double CurrentInterpolatedTime;
23
24 public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
25
26 public double FramesPerSecond => 0;
27
28 public virtual void ChangeSource(IClock? source)
29 {
30 if (source != null)
31 {
32 Source = source;
33 FramedSourceClock = Source as IFrameBasedClock ?? new FramedClock(Source);
34 }
35
36 LastInterpolatedTime = 0;
37 CurrentInterpolatedTime = 0;
38 }
39
40 public InterpolatingFramedClock(IClock? source = null)
41 {
42 ChangeSource(source);
43 }
44
45 public virtual double CurrentTime => currentTime;
46
47 private double currentTime;
48
49 /// <summary>
50 /// The amount of error that is allowed between the source and interpolated time before the interpolated time is ignored and the source time is used.
51 /// </summary>
52 public virtual double AllowableErrorMilliseconds => 1000.0 / 60 * 2 * Rate;
53
54 private bool sourceIsRunning;
55
56 public virtual double Rate
57 {
58 get => FramedSourceClock?.Rate ?? 1;
59 set => throw new NotSupportedException();
60 }
61
62 public virtual bool IsRunning => sourceIsRunning;
63
64 public virtual double Drift => CurrentTime - (FramedSourceClock?.CurrentTime ?? 0);
65
66 public virtual double ElapsedFrameTime => CurrentInterpolatedTime - LastInterpolatedTime;
67
68 /// <summary>
69 /// Whether time is being interpolated for the frame currently being processed.
70 /// </summary>
71 public bool IsInterpolating { get; private set; }
72
73 public virtual void ProcessFrame()
74 {
75 if (FramedSourceClock == null) return;
76
77 clock.ProcessFrame();
78 FramedSourceClock.ProcessFrame();
79
80 sourceIsRunning = FramedSourceClock.IsRunning;
81
82 LastInterpolatedTime = currentTime;
83
84 if (FramedSourceClock.IsRunning)
85 {
86 if (FramedSourceClock.ElapsedFrameTime != 0)
87 IsInterpolating = true;
88
89 CurrentInterpolatedTime += clock.ElapsedFrameTime * Rate;
90
91 if (!IsInterpolating || Math.Abs(FramedSourceClock.CurrentTime - CurrentInterpolatedTime) > AllowableErrorMilliseconds)
92 {
93 // if we've exceeded the allowable error, we should use the source clock's time value.
94 // seeking backwards should only be allowed if the source is explicitly doing that.
95 CurrentInterpolatedTime = FramedSourceClock.ElapsedFrameTime < 0 ? FramedSourceClock.CurrentTime : Math.Max(LastInterpolatedTime, FramedSourceClock.CurrentTime);
96
97 // once interpolation fails, we don't want to resume interpolating until the source clock starts to move again.
98 IsInterpolating = false;
99 }
100 else
101 {
102 //if we differ from the elapsed time of the source, let's adjust for the difference.
103 CurrentInterpolatedTime += (FramedSourceClock.CurrentTime - CurrentInterpolatedTime) / 8;
104
105 // limit the direction of travel to avoid seeking against the flow.
106 CurrentInterpolatedTime = Rate >= 0 ? Math.Max(LastInterpolatedTime, CurrentInterpolatedTime) : Math.Min(LastInterpolatedTime, CurrentInterpolatedTime);
107 }
108 }
109
110 currentTime = sourceIsRunning ? CurrentInterpolatedTime : FramedSourceClock.CurrentTime;
111 }
112 }
113}