Mirror Networking
Mirror.SnapshotInterpolation Class Reference

Static Public Member Functions

static double Timescale (double drift, double catchupSpeed, double slowdownSpeed, double catchupNegativeThreshold, double catchupPositiveThreshold)
 
static double DynamicAdjustment (double sendInterval, double jitterStandardDeviation, double dynamicAdjustmentTolerance)
 
static void Insert< T > (SortedList< double, T > buffer, T snapshot, ref double localTimeline, ref double localTimescale, float sendInterval, double bufferTime, double catchupSpeed, double slowdownSpeed, ref ExponentialMovingAverage driftEma, float catchupNegativeThreshold, float catchupPositiveThreshold, ref ExponentialMovingAverage deliveryTimeEma)
 
static void Sample< T > (SortedList< double, T > buffer, double localTimeline, out int from, out int to, out double t)
 
static bool Step< T > (SortedList< double, T > buffer, double deltaTime, ref double localTimeline, double localTimescale, Func< T, T, double, T > Interpolate, out T computed)
 

Detailed Description

Definition at line 28 of file SnapshotInterpolation.cs.

Member Function Documentation

◆ DynamicAdjustment()

static double Mirror.SnapshotInterpolation.DynamicAdjustment ( double  sendInterval,
double  jitterStandardDeviation,
double  dynamicAdjustmentTolerance 
)
static

Definition at line 65 of file SnapshotInterpolation.cs.

69 {
70 // jitter is equal to delivery time standard variation.
71 // delivery time is made up of 'sendInterval+jitter'.
72 // .Average would be dampened by the constant sendInterval
73 // .StandardDeviation is the changes in 'jitter' that we want
74 // so add it to send interval again.
75 double intervalWithJitter = sendInterval + jitterStandardDeviation;
76
77 // how many multiples of sendInterval is that?
78 // we want to convert to bufferTimeMultiplier later.
79 double multiples = intervalWithJitter / sendInterval;
80
81 // add the tolerance
82 double safezone = multiples + dynamicAdjustmentTolerance;
83 // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}");
84 return safezone;
85 }

◆ Insert< T >()

static void Mirror.SnapshotInterpolation.Insert< T > ( SortedList< double, T >  buffer,
snapshot,
ref double  localTimeline,
ref double  localTimescale,
float  sendInterval,
double  bufferTime,
double  catchupSpeed,
double  slowdownSpeed,
ref ExponentialMovingAverage  driftEma,
float  catchupNegativeThreshold,
float  catchupPositiveThreshold,
ref ExponentialMovingAverage  deliveryTimeEma 
)
static
Type Constraints
T :Snapshot 

Definition at line 89 of file SnapshotInterpolation.cs.

102 : Snapshot
103 {
104 // first snapshot?
105 // initialize local timeline.
106 // we want it to be behind by 'offset'.
107 //
108 // note that the first snapshot may be a lagging packet.
109 // so we would always be behind by that lag.
110 // this requires catchup later.
111 if (buffer.Count == 0)
112 localTimeline = snapshot.remoteTime - bufferTime;
113
114 // insert into the buffer.
115 //
116 // note that we might insert it between our current interpolation
117 // which is fine, it adds another data point for accuracy.
118 //
119 // note that insert may be called twice for the same key.
120 // by default, this would throw.
121 // need to handle it silently.
122 if (!buffer.ContainsKey(snapshot.remoteTime))
123 {
124 buffer.Add(snapshot.remoteTime, snapshot);
125
126 // dynamic buffer adjustment needs delivery interval jitter
127 if (buffer.Count >= 2)
128 {
129 // note that this is not entirely accurate for scrambled inserts.
130 //
131 // we always use the last two, not what we just inserted
132 // even if we were to use the diff for what we just inserted,
133 // a scrambled insert would still not be 100% accurate:
134 // => assume a buffer of AC, with delivery time C-A
135 // => we then insert B, with delivery time B-A
136 // => but then technically the first C-A wasn't correct,
137 // as it would have to be C-B
138 //
139 // in practice, scramble is rare and won't make much difference
140 double previousLocalTime = buffer.Values[buffer.Count - 2].localTime;
141 double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime;
142
143 // this is the delivery time since last snapshot
144 double localDeliveryTime = lastestLocalTime - previousLocalTime;
145
146 // feed the local delivery time to the EMA.
147 // this is what the original stream did too.
148 // our final dynamic buffer adjustment is different though.
149 // we use standard deviation instead of average.
150 deliveryTimeEma.Add(localDeliveryTime);
151 }
152
153 // adjust timescale to catch up / slow down after each insertion
154 // because that is when we add new values to our EMA.
155
156 // we want localTimeline to be about 'bufferTime' behind.
157 // for that, we need the delivery time EMA.
158 // snapshots may arrive out of order, we can not use last-timeline.
159 // we need to use the inserted snapshot's time - timeline.
160 double latestRemoteTime = snapshot.remoteTime;
161 double timeDiff = latestRemoteTime - localTimeline;
162
163 // next, calculate average of a few seconds worth of timediffs.
164 // this gives smoother results.
165 //
166 // to calculate the average, we could simply loop through the
167 // last 'n' seconds worth of timediffs, but:
168 // - our buffer may only store a few snapshots (bufferTime)
169 // - looping through seconds worth of snapshots every time is
170 // expensive
171 //
172 // to solve this, we use an exponential moving average.
173 // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
174 // which is basically fancy math to do the same but faster.
175 // additionally, it allows us to look at more timeDiff values
176 // than we sould have access to in our buffer :)
177 driftEma.Add(timeDiff);
178
179 // next up, calculate how far we are currently away from bufferTime
180 double drift = driftEma.Value - bufferTime;
181
182 // convert relative thresholds to absolute values based on sendInterval
183 double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold;
184 double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold;
185
186 // next, set localTimescale to catchup consistently in Update().
187 // we quantize between default/catchup/slowdown,
188 // this way we have 'default' speed most of the time(!).
189 // and only catch up / slow down for a little bit occasionally.
190 // a consistent multiplier would never be exactly 1.0.
191 localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold);
192
193 // debug logging
194 // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}");
195 }
196 }

◆ Sample< T >()

static void Mirror.SnapshotInterpolation.Sample< T > ( SortedList< double, T >  buffer,
double  localTimeline,
out int  from,
out int  to,
out double  t 
)
static
Type Constraints
T :Snapshot 

Definition at line 202 of file SnapshotInterpolation.cs.

208 : Snapshot
209 {
210 from = -1;
211 to = -1;
212 t = 0;
213
214 // sample from [0,count-1] so we always have two at 'i' and 'i+1'.
215 for (int i = 0; i < buffer.Count - 1; ++i)
216 {
217 // is local time between these two?
218 T first = buffer.Values[i];
219 T second = buffer.Values[i + 1];
220 if (localTimeline >= first.remoteTime &&
221 localTimeline <= second.remoteTime)
222 {
223 // use these two snapshots
224 from = i;
225 to = i + 1;
226 t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline);
227 return;
228 }
229 }
230
231 // didn't find two snapshots around local time.
232 // so pick either the first or last, depending on which is closer.
233
234 // oldest snapshot ahead of local time?
235 if (buffer.Values[0].remoteTime > localTimeline)
236 {
237 from = to = 0;
238 t = 0;
239 }
240 // otherwise initialize both to the last one
241 else
242 {
243 from = to = buffer.Count - 1;
244 t = 0;
245 }
246 }

◆ Step< T >()

static bool Mirror.SnapshotInterpolation.Step< T > ( SortedList< double, T >  buffer,
double  deltaTime,
ref double  localTimeline,
double  localTimescale,
Func< T, T, double, T >  Interpolate,
out T  computed 
)
static
Type Constraints
T :Snapshot 

Definition at line 251 of file SnapshotInterpolation.cs.

258 : Snapshot
259 {
260 computed = default;
261
262 // nothing to do if there are no snapshots at all yet
263 if (buffer.Count == 0)
264 return false;
265
266 // move local forward in time, scaled with catchup / slowdown applied
267 localTimeline += deltaTime * localTimescale;
268
269 // sample snapshot buffer at local interpolation time
270 Sample(buffer, localTimeline, out int from, out int to, out double t);
271
272 // now interpolate between from & to (clamped)
273 T fromSnap = buffer.Values[from];
274 T toSnap = buffer.Values[to];
275 computed = Interpolate(fromSnap, toSnap, t);
276 // UnityEngine.Debug.Log($"step from: {from} to {to}");
277
278 // remove older snapshots that we definitely don't need anymore.
279 // after(!) using the indices.
280 //
281 // if we have 3 snapshots and we are between 2nd and 3rd:
282 // from = 1, to = 2
283 // then we need to remove the first one, which is exactly 'from'.
284 // because 'from-1' = 0 would remove none.
285 buffer.RemoveRange(from);
286
287 // return the interpolated snapshot
288 return true;
289 }

◆ Timescale()

static double Mirror.SnapshotInterpolation.Timescale ( double  drift,
double  catchupSpeed,
double  slowdownSpeed,
double  catchupNegativeThreshold,
double  catchupPositiveThreshold 
)
static

Definition at line 34 of file SnapshotInterpolation.cs.

40 {
41 // if the drift time is too large, it means we are behind more time.
42 // so we need to speed up the timescale.
43 // note the threshold should be sendInterval * catchupThreshold.
44 if (drift > catchupPositiveThreshold)
45 {
46 // localTimeline += 0.001; // too simple, this would ping pong
47 return 1 + catchupSpeed; // n% faster
48 }
49
50 // if the drift time is too small, it means we are ahead of time.
51 // so we need to slow down the timescale.
52 // note the threshold should be sendInterval * catchupThreshold.
53 if (drift < catchupNegativeThreshold)
54 {
55 // localTimeline -= 0.001; // too simple, this would ping pong
56 return 1 - slowdownSpeed; // n% slower
57 }
58
59 // keep constant timescale while within threshold.
60 // this way we have perfectly smooth speed most of the time.
61 return 1;
62 }