LagCompensation.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. // standalone lag compensation algorithm
  2. // based on the Valve Networking Model:
  3. // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
  4. using System.Collections.Generic;
  5. namespace Mirror
  6. {
  7. public static class LagCompensation
  8. {
  9. // history is of <timestamp, capture>.
  10. // Queue allows for fast 'remove first' and 'append last'.
  11. //
  12. // make sure to always insert in order.
  13. // inserting out of order like [1,2,4,3] would cause issues.
  14. // can't safeguard this because Queue doesn't have .Last access.
  15. public static void Insert<T>(
  16. Queue<KeyValuePair<double, T>> history,
  17. int historyLimit,
  18. double timestamp,
  19. T capture)
  20. where T : Capture
  21. {
  22. // make space according to history limit.
  23. // do this before inserting, to avoid resizing past capacity.
  24. if (history.Count >= historyLimit)
  25. history.Dequeue();
  26. // insert
  27. history.Enqueue(new KeyValuePair<double, T>(timestamp, capture));
  28. }
  29. // get the two snapshots closest to a given timestamp.
  30. // those can be used to interpolate the exact snapshot at that time.
  31. // if timestamp is newer than the newest history entry, then we extrapolate.
  32. // 't' will be between 1 and 2, before is second last, after is last.
  33. // callers should Lerp(before, after, t=1.5) to extrapolate the hit.
  34. // see comments below for extrapolation.
  35. public static bool Sample<T>(
  36. Queue<KeyValuePair<double, T>> history,
  37. double timestamp, // current server time
  38. double interval, // capture interval
  39. out T before,
  40. out T after,
  41. out double t) // interpolation factor
  42. where T : Capture
  43. {
  44. before = default;
  45. after = default;
  46. t = 0;
  47. // can't sample an empty history
  48. // interpolation needs at least one entry.
  49. // extrapolation needs at least two entries.
  50. // can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0.
  51. if(history.Count < 2) {
  52. return false;
  53. }
  54. // older than oldest
  55. if (timestamp < history.Peek().Key) {
  56. return false;
  57. }
  58. // iterate through the history
  59. // TODO faster version: guess start index by how many 'intervals' we are behind.
  60. // search around that area.
  61. // should be O(1) most of the time, unless sampling was off.
  62. KeyValuePair<double, T> prev = new KeyValuePair<double, T>();
  63. KeyValuePair<double, T> prevPrev = new KeyValuePair<double, T>();
  64. foreach(KeyValuePair<double, T> entry in history) {
  65. // exact match?
  66. if (timestamp == entry.Key) {
  67. before = entry.Value;
  68. after = entry.Value;
  69. t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
  70. return true;
  71. }
  72. // did we check beyond timestamp? then return the previous two.
  73. if (entry.Key > timestamp) {
  74. before = prev.Value;
  75. after = entry.Value;
  76. t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
  77. return true;
  78. }
  79. // remember the last two for extrapolation.
  80. // Queue doesn't have access to .Last.
  81. prevPrev = prev;
  82. prev = entry;
  83. }
  84. // newer than newest: extrapolate up to one interval.
  85. // let's say we capture every 100 ms:
  86. // 100, 200, 300, 400
  87. // and the server is at 499
  88. // if a client sends CmdFire at time 480, then there's no history entry.
  89. // => adding the current entry every time would be too expensive.
  90. // worst case we would capture at 401, 402, 403, 404, ... 100 times
  91. // => not extrapolating isn't great. low latency clients would be
  92. // punished by missing their targets since no entry at 'time' was found.
  93. // => extrapolation is the best solution. make sure this works as
  94. // expected and within limits.
  95. if (prev.Key < timestamp && timestamp <= prev.Key + interval) {
  96. // return the last two valid snapshots.
  97. // can't just return (after, after) because we can't extrapolate
  98. // if their distance is 0.
  99. before = prevPrev.Value;
  100. after = prev.Value;
  101. // InverseLerp will give [after, after+interval].
  102. // but we return [before, after, t].
  103. // so add +1 for the distance from before->after
  104. t = 1 + Mathd.InverseLerp(after.timestamp, after.timestamp + interval, timestamp);
  105. return true;
  106. }
  107. return false;
  108. }
  109. // never trust the client.
  110. // we estimate when a message was sent.
  111. // don't trust the client to tell us the time.
  112. // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
  113. // Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation
  114. // => lag compensation demo estimation is off by only ~6ms
  115. public static double EstimateTime(double serverTime, double rtt, double bufferTime)
  116. {
  117. // packet latency is one trip from client to server, so rtt / 2
  118. // client view interpolation is the snapshot interpolation buffer time
  119. double latency = rtt / 2;
  120. return serverTime - latency - bufferTime;
  121. }
  122. // convenience function to draw all history gizmos.
  123. // this should be called from OnDrawGizmos.
  124. public static void DrawGizmos<T>(Queue<KeyValuePair<double, T>> history)
  125. where T : Capture
  126. {
  127. foreach (KeyValuePair<double, T> entry in history)
  128. entry.Value.DrawGizmo();
  129. }
  130. }
  131. }