SnapshotInterpolation.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. // snapshot interpolation algorithms only,
  2. // independent from Unity/NetworkTransform/MonoBehaviour/Mirror/etc.
  3. // the goal is to remove all the magic from it.
  4. // => a standalone snapshot interpolation algorithm
  5. // => that can be simulated with unit tests easily
  6. //
  7. // BOXING: in C#, uses <T> does not box! passing the interface would box!
  8. using System;
  9. using System.Collections.Generic;
  10. namespace Mirror
  11. {
  12. public static class SnapshotInterpolation
  13. {
  14. // insert into snapshot buffer if newer than first entry
  15. // this should ALWAYS be used when inserting into a snapshot buffer!
  16. public static void InsertIfNewEnough<T>(T snapshot, SortedList<double, T> buffer)
  17. where T : Snapshot
  18. {
  19. // we need to drop any snapshot which is older ('<=')
  20. // the snapshots we are already working with.
  21. double timestamp = snapshot.remoteTimestamp;
  22. // if size == 1, then only add snapshots that are newer.
  23. // for example, a snapshot before the first one might have been
  24. // lagging.
  25. if (buffer.Count == 1 &&
  26. timestamp <= buffer.Values[0].remoteTimestamp)
  27. return;
  28. // for size >= 2, we are already interpolating between the first two
  29. // so only add snapshots that are newer than the second entry.
  30. // aka the 'ACB' problem:
  31. // if we have a snapshot A at t=0 and C at t=2,
  32. // we start interpolating between them.
  33. // if suddenly B at t=1 comes in unexpectely,
  34. // we should NOT suddenly steer towards B.
  35. if (buffer.Count >= 2 &&
  36. timestamp <= buffer.Values[1].remoteTimestamp)
  37. return;
  38. // otherwise sort it into the list
  39. // an UDP messages might arrive twice sometimes.
  40. // SortedList throws if key already exists, so check.
  41. if (!buffer.ContainsKey(timestamp))
  42. buffer.Add(timestamp, snapshot);
  43. }
  44. // helper function to check if we have 'bufferTime' worth of snapshots
  45. // to start.
  46. //
  47. // glenn fiedler article:
  48. // "Now for the trick with snapshots. What we do is instead of
  49. // immediately rendering snapshot data received is that we buffer
  50. // snapshots for a short amount of time in an interpolation buffer.
  51. // This interpolation buffer holds on to snapshots for a period of time
  52. // such that you have not only the snapshot you want to render but also,
  53. // statistically speaking, you are very likely to have the next snapshot
  54. // as well."
  55. //
  56. // => 'statistically' implies that we always wait for a fixed amount
  57. // aka LOCAL TIME has passed.
  58. // => it does NOT imply to wait for a remoteTime span of bufferTime.
  59. // that would not be 'statistically'. it would be 'exactly'.
  60. public static bool HasAmountOlderThan<T>(SortedList<double, T> buffer, double threshold, int amount)
  61. where T : Snapshot =>
  62. buffer.Count >= amount &&
  63. buffer.Values[amount - 1].localTimestamp <= threshold;
  64. // for convenience, hide the 'bufferTime worth of snapshots' check in an
  65. // easy to use function. this way we can have several conditions etc.
  66. public static bool HasEnough<T>(SortedList<double, T> buffer, double time, double bufferTime)
  67. where T : Snapshot =>
  68. // two snapshots with local time older than threshold?
  69. HasAmountOlderThan(buffer, time - bufferTime, 2);
  70. // sometimes we need to know if it's still safe to skip past the first
  71. // snapshot.
  72. public static bool HasEnoughWithoutFirst<T>(SortedList<double, T> buffer, double time, double bufferTime)
  73. where T : Snapshot =>
  74. // still two snapshots with local time older than threshold if
  75. // we remove the first one? (in other words, need three older)
  76. HasAmountOlderThan(buffer, time - bufferTime, 3);
  77. // calculate catchup.
  78. // the goal is to buffer 'bufferTime' snapshots.
  79. // for whatever reason, we might see growing buffers.
  80. // in which case we should speed up to avoid ever growing delay.
  81. // -> everything after 'threshold' is multiplied by 'multiplier'
  82. public static double CalculateCatchup<T>(SortedList<double, T> buffer, int catchupThreshold, double catchupMultiplier)
  83. where T : Snapshot
  84. {
  85. // NOTE: we count ALL buffer entires > threshold as excess.
  86. // not just the 'old enough' ones.
  87. // if buffer keeps growing, we have to catch up no matter what.
  88. int excess = buffer.Count - catchupThreshold;
  89. return excess > 0 ? excess * catchupMultiplier : 0;
  90. }
  91. // get first & second buffer entries and delta between them.
  92. // helper function because we use this several times.
  93. // => assumes at least two entries in buffer.
  94. public static void GetFirstSecondAndDelta<T>(SortedList<double, T> buffer, out T first, out T second, out double delta)
  95. where T : Snapshot
  96. {
  97. // get first & second
  98. first = buffer.Values[0];
  99. second = buffer.Values[1];
  100. // delta between first & second is needed a lot
  101. delta = second.remoteTimestamp - first.remoteTimestamp;
  102. }
  103. // the core snapshot interpolation algorithm.
  104. // for a given remoteTime, interpolationTime and buffer,
  105. // we tick the snapshot simulation once.
  106. // => it's the same one on server and client
  107. // => should be called every Update() depending on authority
  108. //
  109. // time: LOCAL time since startup in seconds. like Unity's Time.time.
  110. // deltaTime: Time.deltaTime from Unity. parameter for easier tests.
  111. // interpolationTime: time in interpolation. moved along deltaTime.
  112. // between [0, delta] where delta is snapshot
  113. // B.timestamp - A.timestamp.
  114. // IMPORTANT:
  115. // => we use actual time instead of a relative
  116. // t [0,1] because overshoot is easier to handle.
  117. // if relative t overshoots but next snapshots are
  118. // further apart than the current ones, it's not
  119. // obvious how to calculate it.
  120. // => for example, if t = 3 every time we skip we would have to
  121. // make sure to adjust the subtracted value relative to the
  122. // skipped delta. way too complex.
  123. // => actual time can overshoot without problems.
  124. // we know it's always by actual time.
  125. // bufferTime: time in seconds that we buffer snapshots.
  126. // buffer: our buffer of snapshots.
  127. // Compute() assumes full integrity of the snapshots.
  128. // for example, when interpolating between A=0 and C=2,
  129. // make sure that you don't add B=1 between A and C if that
  130. // snapshot arrived after we already started interpolating.
  131. // => InsertIfNewEnough needs to protect against the 'ACB' problem
  132. // catchupThreshold: amount of buffer entries after which we start to
  133. // accelerate to catch up.
  134. // if 'bufferTime' is 'sendInterval * 3', then try
  135. // a value > 3 like 6.
  136. // catchupMultiplier: catchup by % per additional excess buffer entry
  137. // over the amount of 'catchupThreshold'.
  138. // Interpolate: interpolates one snapshot to another, returns the result
  139. // T Interpolate(T from, T to, double t);
  140. // => needs to be Func<T> instead of a function in the Snapshot
  141. // interface because that would require boxing.
  142. // => make sure to only allocate that function once.
  143. //
  144. // returns
  145. // 'true' if it spit out a snapshot to apply.
  146. // 'false' means computation moved along, but nothing to apply.
  147. public static bool Compute<T>(
  148. double time,
  149. double deltaTime,
  150. ref double interpolationTime,
  151. double bufferTime,
  152. SortedList<double, T> buffer,
  153. int catchupThreshold,
  154. float catchupMultiplier,
  155. Func<T, T, double, T> Interpolate,
  156. out T computed)
  157. where T : Snapshot
  158. {
  159. // we buffer snapshots for 'bufferTime'
  160. // for example:
  161. // * we buffer for 3 x sendInterval = 300ms
  162. // * the idea is to wait long enough so we at least have a few
  163. // snapshots to interpolate between
  164. // * we process anything older 100ms immediately
  165. //
  166. // IMPORTANT: snapshot timestamps are _remote_ time
  167. // we need to interpolate and calculate buffer lifetimes based on it.
  168. // -> we don't know remote's current time
  169. // -> NetworkTime.time fluctuates too much, that's no good
  170. // -> we _could_ calculate an offset when the first snapshot arrives,
  171. // but if there was high latency then we'll always calculate time
  172. // with high latency
  173. // -> at any given time, we are interpolating from snapshot A to B
  174. // => seems like A.timestamp += deltaTime is a good way to do it
  175. computed = default;
  176. //Debug.Log($"{name} snapshotbuffer={buffer.Count}");
  177. // do we have enough buffered to start interpolating?
  178. if (!HasEnough(buffer, time, bufferTime))
  179. return false;
  180. // multiply deltaTime by catchup.
  181. // for example, assuming a catch up of 50%:
  182. // - deltaTime = 1s => 1.5s
  183. // - deltaTime = 0.1s => 0.15s
  184. // in other words, variations in deltaTime don't matter.
  185. // simply multiply. that's just how time works.
  186. // (50% catch up means 0.5, so we multiply by 1.5)
  187. //
  188. // if '0' catchup then we multiply by '1', which changes nothing.
  189. // (faster branch prediction)
  190. double catchup = CalculateCatchup(buffer, catchupThreshold, catchupMultiplier);
  191. deltaTime *= (1 + catchup);
  192. // interpolationTime starts at 0 and we add deltaTime to move
  193. // along the interpolation.
  194. //
  195. // ONLY while we have snapshots to interpolate.
  196. // otherwise we might increase it to infinity which would lead
  197. // to skipping the next snapshots entirely.
  198. //
  199. // IMPORTANT: interpolationTime as actual time instead of
  200. // t [0,1] allows us to overshoot and subtract easily.
  201. // if t was [0,1], and we overshoot by 0.1, that's a
  202. // RELATIVE overshoot for the delta between B.time - A.time.
  203. // => if the next C.time - B.time is not the same delta,
  204. // then the relative overshoot would speed up or slow
  205. // down the interpolation! CAREFUL.
  206. //
  207. // IMPORTANT: we NEVER add deltaTime to 'time'.
  208. // 'time' is already NOW. that's how Unity works.
  209. interpolationTime += deltaTime;
  210. // get first & second & delta
  211. GetFirstSecondAndDelta(buffer, out T first, out T second, out double delta);
  212. // reached goal and have more old enough snapshots in buffer?
  213. // then skip and move to next.
  214. // for example, if we have snapshots at t=1,2,3
  215. // and we are at interpolationTime = 2.5, then
  216. // we should skip the first one, subtract delta and interpolate
  217. // between 2,3 instead.
  218. //
  219. // IMPORTANT: we only ever use old enough snapshots.
  220. // if we wouldn't check for old enough, then we would
  221. // move to the next one, interpolate a little bit,
  222. // and then in next compute() wait again because it
  223. // wasn't old enough yet.
  224. while (interpolationTime >= delta &&
  225. HasEnoughWithoutFirst(buffer, time, bufferTime))
  226. {
  227. // subtract exactly delta from interpolation time
  228. // instead of setting to '0', where we would lose the
  229. // overshoot part and see jitter again.
  230. //
  231. // IMPORTANT: subtracting delta TIME works perfectly.
  232. // subtracting '1' from a ratio of t [0,1] would
  233. // leave the overshoot as relative between the
  234. // next delta. if next delta is different, then
  235. // overshoot would be bigger than planned and
  236. // speed up the interpolation.
  237. interpolationTime -= delta;
  238. //Debug.LogWarning($"{name} overshot and is now at: {interpolationTime}");
  239. // remove first, get first, second & delta again after change.
  240. buffer.RemoveAt(0);
  241. GetFirstSecondAndDelta(buffer, out first, out second, out delta);
  242. // NOTE: it's worth consider spitting out all snapshots
  243. // that we skipped, in case someone still wants to move
  244. // along them to avoid physics collisions.
  245. // * for NetworkTransform it's unnecessary as we always
  246. // set transform.position, which can go anywhere.
  247. // * for CharacterController it's worth considering
  248. }
  249. // interpolationTime is actual time, NOT a 't' ratio [0,1].
  250. // we need 't' between [0,1] relative.
  251. // InverseLerp calculates just that.
  252. // InverseLerp CLAMPS between [0,1] and DOES NOT extrapolate!
  253. // => we already skipped ahead as many as possible above.
  254. // => we do NOT extrapolate for the reasons below.
  255. //
  256. // IMPORTANT:
  257. // we should NOT extrapolate & predict while waiting for more
  258. // snapshots as this would introduce a whole range of issues:
  259. // * player might be extrapolated WAY out if we wait for long
  260. // * player might be extrapolated behind walls
  261. // * once we receive a new snapshot, we would interpolate
  262. // not from the last valid position, but from the
  263. // extrapolated position. this could be ANYWHERE. the
  264. // player might get stuck in walls, etc.
  265. // => we are NOT doing client side prediction & rollback here
  266. // => we are simply interpolating with known, valid positions
  267. //
  268. // SEE TEST: Compute_Step5_OvershootWithoutEnoughSnapshots_NeverExtrapolates()
  269. double t = Mathd.InverseLerp(first.remoteTimestamp, second.remoteTimestamp, first.remoteTimestamp + interpolationTime);
  270. //Debug.Log($"InverseLerp({first.remoteTimestamp:F2}, {second.remoteTimestamp:F2}, {first.remoteTimestamp} + {interpolationTime:F2}) = {t:F2} snapshotbuffer={buffer.Count}");
  271. // interpolate snapshot, return true to indicate we computed one
  272. computed = Interpolate(first, second, t);
  273. // interpolationTime:
  274. // overshooting is ONLY allowed for smooth transitions when
  275. // immediately moving to the NEXT snapshot afterwards.
  276. //
  277. // if there is ANY break, for example:
  278. // * reached second snapshot and waiting for more
  279. // * reached second snapshot and next one isn't old enough yet
  280. //
  281. // then we SHOULD NOT overshoot because:
  282. // * increasing interpolationTime by deltaTime while waiting
  283. // would make it grow HUGE to 100+.
  284. // * once we have more snapshots, we would skip most of them
  285. // instantly instead of actually interpolating through them.
  286. //
  287. // in other words: cap time if we WOULDN'T have enough after removing
  288. if (!HasEnoughWithoutFirst(buffer, time, bufferTime))
  289. {
  290. // interpolationTime is always from 0..delta.
  291. // so we cap it at delta.
  292. // DO NOT cap it at second.remoteTimestamp.
  293. // (that's why when interpolating the third parameter is
  294. // first.time + interpolationTime)
  295. // => covered with test:
  296. // Compute_Step5_OvershootWithEnoughSnapshots_NextIsntOldEnough()
  297. interpolationTime = Math.Min(interpolationTime, delta);
  298. }
  299. return true;
  300. }
  301. }
  302. }