SnapshotInterpolation.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. // snapshot interpolation V2 by mischa
  2. //
  3. // Unity independent to be engine agnostic & easy to test.
  4. // boxing: in C#, uses <T> does not box! passing the interface would box!
  5. //
  6. // credits:
  7. // glenn fiedler: https://gafferongames.com/post/snapshot_interpolation/
  8. // fholm: netcode streams
  9. // fakebyte: standard deviation for dynamic adjustment
  10. // ninjakicka: math & debugging
  11. using System.Collections.Generic;
  12. using System.Runtime.CompilerServices;
  13. namespace Mirror
  14. {
  15. public static class SortedListExtensions
  16. {
  17. // removes the first 'amount' elements from the sorted list
  18. public static void RemoveRange<T, U>(this SortedList<T, U> list, int amount)
  19. {
  20. // remove the first element 'amount' times.
  21. // handles -1 and > count safely.
  22. for (int i = 0; i < amount && i < list.Count; ++i)
  23. list.RemoveAt(0);
  24. }
  25. }
  26. public static class SnapshotInterpolation
  27. {
  28. // calculate timescale for catch-up / slow-down
  29. // note that negative threshold should be <0.
  30. // caller should verify (i.e. Unity OnValidate).
  31. // improves branch prediction.
  32. public static double Timescale(
  33. double drift, // how far we are off from bufferTime
  34. double catchupSpeed, // in % [0,1]
  35. double slowdownSpeed, // in % [0,1]
  36. double absoluteCatchupNegativeThreshold, // in seconds (careful, we may run out of snapshots)
  37. double absoluteCatchupPositiveThreshold) // in seconds
  38. {
  39. // if the drift time is too large, it means we are behind more time.
  40. // so we need to speed up the timescale.
  41. // note the threshold should be sendInterval * catchupThreshold.
  42. if (drift > absoluteCatchupPositiveThreshold)
  43. {
  44. // localTimeline += 0.001; // too simple, this would ping pong
  45. return 1 + catchupSpeed; // n% faster
  46. }
  47. // if the drift time is too small, it means we are ahead of time.
  48. // so we need to slow down the timescale.
  49. // note the threshold should be sendInterval * catchupThreshold.
  50. if (drift < absoluteCatchupNegativeThreshold)
  51. {
  52. // localTimeline -= 0.001; // too simple, this would ping pong
  53. return 1 - slowdownSpeed; // n% slower
  54. }
  55. // keep constant timescale while within threshold.
  56. // this way we have perfectly smooth speed most of the time.
  57. return 1;
  58. }
  59. // calculate dynamic buffer time adjustment
  60. public static double DynamicAdjustment(
  61. double sendInterval,
  62. double jitterStandardDeviation,
  63. double dynamicAdjustmentTolerance)
  64. {
  65. // jitter is equal to delivery time standard variation.
  66. // delivery time is made up of 'sendInterval+jitter'.
  67. // .Average would be dampened by the constant sendInterval
  68. // .StandardDeviation is the changes in 'jitter' that we want
  69. // so add it to send interval again.
  70. double intervalWithJitter = sendInterval + jitterStandardDeviation;
  71. // how many multiples of sendInterval is that?
  72. // we want to convert to bufferTimeMultiplier later.
  73. double multiples = intervalWithJitter / sendInterval;
  74. // add the tolerance
  75. double safezone = multiples + dynamicAdjustmentTolerance;
  76. // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}");
  77. return safezone;
  78. }
  79. // helper function to insert a snapshot if it doesn't exist yet.
  80. // extra function so we can use it for both cases:
  81. // NetworkClient global timeline insertions & adjustments via Insert<T>.
  82. // NetworkBehaviour local insertion without any time adjustments.
  83. public static bool InsertIfNotExists<T>(
  84. SortedList<double, T> buffer, // snapshot buffer
  85. int bufferLimit, // don't grow infinitely
  86. T snapshot) // the newly received snapshot
  87. where T : Snapshot
  88. {
  89. // slow clients may not be able to process incoming snapshots fast enough.
  90. // infinitely growing snapshots would make it even worse.
  91. // for example, run NetworkRigidbodyBenchmark while deep profiling client.
  92. // the client just grows and reallocates the buffer forever.
  93. if (buffer.Count >= bufferLimit) return false;
  94. // SortedList does not allow duplicates.
  95. // we don't need to check ContainsKey (which is expensive).
  96. // simply add and compare count before/after for the return value.
  97. //if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive
  98. // buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists
  99. int before = buffer.Count;
  100. buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists
  101. return buffer.Count > before;
  102. }
  103. // clamp timeline for cases where it gets too far behind.
  104. // for example, a client app may go into the background and get updated
  105. // with 1hz for a while. by the time it's back it's at least 30 frames
  106. // behind, possibly more if the transport also queues up. In this
  107. // scenario, at 1% catch up it took around 20+ seconds to finally catch
  108. // up. For these kinds of scenarios it will be better to snap / clamp.
  109. //
  110. // to reproduce, try snapshot interpolation demo and press the button to
  111. // simulate the client timeline at multiple seconds behind. it'll take
  112. // a long time to catch up if the timeline is a long time behind.
  113. public static double TimelineClamp(
  114. double localTimeline,
  115. double bufferTime,
  116. double latestRemoteTime)
  117. {
  118. // we want local timeline to always be 'bufferTime' behind remote.
  119. double targetTime = latestRemoteTime - bufferTime;
  120. // we define a boundary of 'bufferTime' around the target time.
  121. // this is where catchup / slowdown will happen.
  122. // outside of the area, we clamp.
  123. double lowerBound = targetTime - bufferTime; // how far behind we can get
  124. double upperBound = targetTime + bufferTime; // how far ahead we can get
  125. return Mathd.Clamp(localTimeline, lowerBound, upperBound);
  126. }
  127. // call this for every received snapshot.
  128. // adds / inserts it to the list & initializes local time if needed.
  129. public static void InsertAndAdjust<T>(
  130. SortedList<double, T> buffer, // snapshot buffer
  131. int bufferLimit, // don't grow infinitely
  132. T snapshot, // the newly received snapshot
  133. ref double localTimeline, // local interpolation time based on server time
  134. ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time
  135. float sendInterval, // for debugging
  136. double bufferTime, // offset for buffering
  137. double catchupSpeed, // in % [0,1]
  138. double slowdownSpeed, // in % [0,1]
  139. ref ExponentialMovingAverage driftEma, // for catchup / slowdown
  140. float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
  141. float catchupPositiveThreshold, // in % of sendInterval
  142. ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment
  143. where T : Snapshot
  144. {
  145. // first snapshot?
  146. // initialize local timeline.
  147. // we want it to be behind by 'offset'.
  148. //
  149. // note that the first snapshot may be a lagging packet.
  150. // so we would always be behind by that lag.
  151. // this requires catchup later.
  152. if (buffer.Count == 0)
  153. localTimeline = snapshot.remoteTime - bufferTime;
  154. // insert into the buffer.
  155. //
  156. // note that we might insert it between our current interpolation
  157. // which is fine, it adds another data point for accuracy.
  158. //
  159. // note that insert may be called twice for the same key.
  160. // by default, this would throw.
  161. // need to handle it silently.
  162. if (InsertIfNotExists(buffer, bufferLimit, snapshot))
  163. {
  164. // dynamic buffer adjustment needs delivery interval jitter
  165. if (buffer.Count >= 2)
  166. {
  167. // note that this is not entirely accurate for scrambled inserts.
  168. //
  169. // we always use the last two, not what we just inserted
  170. // even if we were to use the diff for what we just inserted,
  171. // a scrambled insert would still not be 100% accurate:
  172. // => assume a buffer of AC, with delivery time C-A
  173. // => we then insert B, with delivery time B-A
  174. // => but then technically the first C-A wasn't correct,
  175. // as it would have to be C-B
  176. //
  177. // in practice, scramble is rare and won't make much difference
  178. double previousLocalTime = buffer.Values[buffer.Count - 2].localTime;
  179. double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime;
  180. // this is the delivery time since last snapshot
  181. double localDeliveryTime = lastestLocalTime - previousLocalTime;
  182. // feed the local delivery time to the EMA.
  183. // this is what the original stream did too.
  184. // our final dynamic buffer adjustment is different though.
  185. // we use standard deviation instead of average.
  186. deliveryTimeEma.Add(localDeliveryTime);
  187. }
  188. // adjust timescale to catch up / slow down after each insertion
  189. // because that is when we add new values to our EMA.
  190. // we want localTimeline to be about 'bufferTime' behind.
  191. // for that, we need the delivery time EMA.
  192. // snapshots may arrive out of order, we can not use last-timeline.
  193. // we need to use the inserted snapshot's time - timeline.
  194. double latestRemoteTime = snapshot.remoteTime;
  195. // ensure timeline stays within a reasonable bound behind/ahead.
  196. localTimeline = TimelineClamp(localTimeline, bufferTime, latestRemoteTime);
  197. // calculate timediff after localTimeline override changes
  198. double timeDiff = latestRemoteTime - localTimeline;
  199. // next, calculate average of a few seconds worth of timediffs.
  200. // this gives smoother results.
  201. //
  202. // to calculate the average, we could simply loop through the
  203. // last 'n' seconds worth of timediffs, but:
  204. // - our buffer may only store a few snapshots (bufferTime)
  205. // - looping through seconds worth of snapshots every time is
  206. // expensive
  207. //
  208. // to solve this, we use an exponential moving average.
  209. // https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
  210. // which is basically fancy math to do the same but faster.
  211. // additionally, it allows us to look at more timeDiff values
  212. // than we sould have access to in our buffer :)
  213. driftEma.Add(timeDiff);
  214. // timescale depends on driftEma.
  215. // driftEma only changes when inserting.
  216. // therefore timescale only needs to be calculated when inserting.
  217. // saves CPU cycles in Update.
  218. // next up, calculate how far we are currently away from bufferTime
  219. double drift = driftEma.Value - bufferTime;
  220. // convert relative thresholds to absolute values based on sendInterval
  221. double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold;
  222. double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold;
  223. // next, set localTimescale to catchup consistently in Update().
  224. // we quantize between default/catchup/slowdown,
  225. // this way we have 'default' speed most of the time(!).
  226. // and only catch up / slow down for a little bit occasionally.
  227. // a consistent multiplier would never be exactly 1.0.
  228. localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold);
  229. // debug logging
  230. // UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}");
  231. }
  232. }
  233. // sample snapshot buffer to find the pair around the given time.
  234. // returns indices so we can use it with RemoveRange to clear old snaps.
  235. // make sure to use use buffer.Values[from/to], not buffer[from/to].
  236. // make sure to only call this is we have > 0 snapshots.
  237. public static void Sample<T>(
  238. SortedList<double, T> buffer, // snapshot buffer
  239. double localTimeline, // local interpolation time based on server time
  240. out int from, // the snapshot <= time
  241. out int to, // the snapshot >= time
  242. out double t) // interpolation factor
  243. where T : Snapshot
  244. {
  245. from = -1;
  246. to = -1;
  247. t = 0;
  248. // sample from [0,count-1] so we always have two at 'i' and 'i+1'.
  249. for (int i = 0; i < buffer.Count - 1; ++i)
  250. {
  251. // is local time between these two?
  252. T first = buffer.Values[i];
  253. T second = buffer.Values[i + 1];
  254. if (localTimeline >= first.remoteTime &&
  255. localTimeline <= second.remoteTime)
  256. {
  257. // use these two snapshots
  258. from = i;
  259. to = i + 1;
  260. t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline);
  261. return;
  262. }
  263. }
  264. // didn't find two snapshots around local time.
  265. // so pick either the first or last, depending on which is closer.
  266. // oldest snapshot ahead of local time?
  267. if (buffer.Values[0].remoteTime > localTimeline)
  268. {
  269. from = to = 0;
  270. t = 0;
  271. }
  272. // otherwise initialize both to the last one
  273. else
  274. {
  275. from = to = buffer.Count - 1;
  276. t = 0;
  277. }
  278. }
  279. // progress local timeline every update.
  280. //
  281. // ONLY CALL IF SNAPSHOTS.COUNT > 0!
  282. //
  283. // decoupled from Step<T> for easier testing and so we can progress
  284. // time only once in NetworkClient, while stepping for each component.
  285. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  286. public static void StepTime(
  287. double deltaTime, // engine delta time (unscaled)
  288. ref double localTimeline, // local interpolation time based on server time
  289. double localTimescale) // catchup / slowdown is applied to time every update)
  290. {
  291. // move local forward in time, scaled with catchup / slowdown applied
  292. localTimeline += deltaTime * localTimescale;
  293. }
  294. // sample, clear old.
  295. // call this every update.
  296. //
  297. // ONLY CALL IF SNAPSHOTS.COUNT > 0!
  298. //
  299. // returns true if there is anything to apply (requires at least 1 snap)
  300. // from/to/t are out parameters instead of an interpolated 'computed'.
  301. // this allows us to store from/to/t globally (i.e. in NetworkClient)
  302. // and have each component apply the interpolation manually.
  303. // besides, passing "Func Interpolate" would allocate anyway.
  304. public static void StepInterpolation<T>(
  305. SortedList<double, T> buffer, // snapshot buffer
  306. double localTimeline, // local interpolation time based on server time
  307. out T fromSnapshot, // we interpolate 'from' this snapshot
  308. out T toSnapshot, // 'to' this snapshot
  309. out double t) // at ratio 't' [0,1]
  310. where T : Snapshot
  311. {
  312. // check this in caller:
  313. // nothing to do if there are no snapshots at all yet
  314. // if (buffer.Count == 0) return false;
  315. // sample snapshot buffer at local interpolation time
  316. Sample(buffer, localTimeline, out int from, out int to, out t);
  317. // save from/to
  318. fromSnapshot = buffer.Values[from];
  319. toSnapshot = buffer.Values[to];
  320. // remove older snapshots that we definitely don't need anymore.
  321. // after(!) using the indices.
  322. //
  323. // if we have 3 snapshots and we are between 2nd and 3rd:
  324. // from = 1, to = 2
  325. // then we need to remove the first one, which is exactly 'from'.
  326. // because 'from-1' = 0 would remove none.
  327. buffer.RemoveRange(from);
  328. }
  329. // update time, sample, clear old.
  330. // call this every update.
  331. //
  332. // ONLY CALL IF SNAPSHOTS.COUNT > 0!
  333. //
  334. // returns true if there is anything to apply (requires at least 1 snap)
  335. // from/to/t are out parameters instead of an interpolated 'computed'.
  336. // this allows us to store from/to/t globally (i.e. in NetworkClient)
  337. // and have each component apply the interpolation manually.
  338. // besides, passing "Func Interpolate" would allocate anyway.
  339. public static void Step<T>(
  340. SortedList<double, T> buffer, // snapshot buffer
  341. double deltaTime, // engine delta time (unscaled)
  342. ref double localTimeline, // local interpolation time based on server time
  343. double localTimescale, // catchup / slowdown is applied to time every update
  344. out T fromSnapshot, // we interpolate 'from' this snapshot
  345. out T toSnapshot, // 'to' this snapshot
  346. out double t) // at ratio 't' [0,1]
  347. where T : Snapshot
  348. {
  349. StepTime(deltaTime, ref localTimeline, localTimescale);
  350. StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t);
  351. }
  352. }
  353. }