NetworkTime.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. // NetworkTime now uses NetworkClient's snapshot interpolated timeline.
  2. // this gives ideal results & ensures everything is on the same timeline.
  3. // previously, NetworkTransforms were on separate timelines.
  4. //
  5. // however, some of the old NetworkTime code remains for ping time (rtt).
  6. // some users may still be using that.
  7. using System;
  8. using System.Runtime.CompilerServices;
  9. using UnityEngine;
  10. #if !UNITY_2020_3_OR_NEWER
  11. using Stopwatch = System.Diagnostics.Stopwatch;
  12. #endif
  13. namespace Mirror
  14. {
  15. /// <summary>Synchronizes server time to clients.</summary>
  16. public static class NetworkTime
  17. {
  18. /// <summary>Ping message interval, used to calculate latency / RTT and predicted time.</summary>
  19. // 2s was enough to get a good average RTT.
  20. // for prediction, we want to react to latency changes more rapidly.
  21. const float DefaultPingInterval = 0.1f; // for resets
  22. public static float PingInterval = DefaultPingInterval;
  23. /// <summary>Average out the last few results from Ping</summary>
  24. // const because it's used immediately in _rtt constructor.
  25. public const int PingWindowSize = 50; // average over 50 * 100ms = 5s
  26. static double lastPingTime;
  27. static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(PingWindowSize);
  28. /// <summary>Returns double precision clock time _in this system_, unaffected by the network.</summary>
  29. #if UNITY_2020_3_OR_NEWER
  30. public static double localTime
  31. {
  32. // NetworkTime uses unscaled time and ignores Time.timeScale.
  33. // fixes Time.timeScale getting server & client time out of sync:
  34. // https://github.com/MirrorNetworking/Mirror/issues/3409
  35. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  36. get => Time.unscaledTimeAsDouble;
  37. }
  38. #else
  39. // need stopwatch for older Unity versions, but it's quite slow.
  40. // CAREFUL: unlike Time.time, the stopwatch time is not a FRAME time.
  41. // it changes during the frame, so we have an extra step to "cache" it in EarlyUpdate.
  42. static readonly Stopwatch stopwatch = new Stopwatch();
  43. static NetworkTime() => stopwatch.Start();
  44. static double localFrameTime;
  45. public static double localTime => localFrameTime;
  46. #endif
  47. /// <summary>The time in seconds since the server started.</summary>
  48. // via global NetworkClient snapshot interpolated timeline (if client).
  49. // on server, this is simply Time.timeAsDouble.
  50. //
  51. // I measured the accuracy of float and I got this:
  52. // for the same day, accuracy is better than 1 ms
  53. // after 1 day, accuracy goes down to 7 ms
  54. // after 10 days, accuracy is 61 ms
  55. // after 30 days , accuracy is 238 ms
  56. // after 60 days, accuracy is 454 ms
  57. // in other words, if the server is running for 2 months,
  58. // and you cast down to float, then the time will jump in 0.4s intervals.
  59. public static double time
  60. {
  61. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  62. get => NetworkServer.active
  63. ? localTime
  64. : NetworkClient.localTimeline;
  65. }
  66. // prediction //////////////////////////////////////////////////////////
  67. // NetworkTime.time is server time, behind by bufferTime.
  68. // for prediction, we want server time, ahead by latency.
  69. // so that client inputs at predictedTime=2 arrive on server at time=2.
  70. // the more accurate this is, the more closesly will corrections be
  71. // be applied and the less jitter we will see.
  72. //
  73. // we'll use a two step process to calculate predicted time:
  74. // 1. move snapshot interpolated time to server time, without being behind by bufferTime
  75. // 2. constantly send this time to server (included in ping message)
  76. // server replies with how far off it was.
  77. // client averages that offset and applies it to predictedTime to get ever closer.
  78. //
  79. // this is also very easy to test & verify:
  80. // - add LatencySimulation with 50ms latency
  81. // - log predictionError on server in OnServerPing, see if it gets closer to 0
  82. //
  83. // credits: FakeByte, imer, NinjaKickja, mischa
  84. // const because it's used immediately in _predictionError constructor.
  85. static int PredictionErrorWindowSize = 20; // average over 20 * 100ms = 2s
  86. static ExponentialMovingAverage _predictionErrorUnadjusted = new ExponentialMovingAverage(PredictionErrorWindowSize);
  87. public static double predictionErrorUnadjusted => _predictionErrorUnadjusted.Value;
  88. public static double predictionErrorAdjusted { get; private set; } // for debugging
  89. /// <summary>Predicted timeline in order for client inputs to be timestamped with the exact time when they will most likely arrive on the server. This is the basis for all prediction like PredictedRigidbody.</summary>
  90. // on client, this is based on localTime (aka Time.time) instead of the snapshot interpolated timeline.
  91. // this gives much better and immediately accurate results.
  92. // -> snapshot interpolation timeline tries to emulate a server timeline without hard offset corrections.
  93. // -> predictedTime does have hard offset corrections, so might as well use Time.time directly for this.
  94. //
  95. // note that predictedTime over unreliable is enough!
  96. // even with reliable components, it gives better results than if we were
  97. // to implemented predictedTime over reliable channel.
  98. public static double predictedTime
  99. {
  100. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  101. get => NetworkServer.active
  102. ? localTime // server always uses it's own timeline
  103. : localTime + predictionErrorUnadjusted; // add the offset that the server told us we are off by
  104. }
  105. ////////////////////////////////////////////////////////////////////////
  106. /// <summary>Clock difference in seconds between the client and the server. Always 0 on server.</summary>
  107. // original implementation used 'client - server' time. keep it this way.
  108. // TODO obsolete later. people shouldn't worry about this.
  109. public static double offset => localTime - time;
  110. /// <summary>Round trip time (in seconds) that it takes a message to go client->server->client.</summary>
  111. public static double rtt => _rtt.Value;
  112. /// <Summary>Round trip time variance aka jitter, in seconds.</Summary>
  113. // "rttVariance" instead of "rttVar" for consistency with older versions.
  114. public static double rttVariance => _rtt.Variance;
  115. // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
  116. [RuntimeInitializeOnLoadMethod]
  117. public static void ResetStatics()
  118. {
  119. PingInterval = DefaultPingInterval;
  120. lastPingTime = 0;
  121. _rtt = new ExponentialMovingAverage(PingWindowSize);
  122. #if !UNITY_2020_3_OR_NEWER
  123. stopwatch.Restart();
  124. #endif
  125. }
  126. internal static void UpdateClient()
  127. {
  128. // localTime (double) instead of Time.time for accuracy over days
  129. if (localTime >= lastPingTime + PingInterval)
  130. {
  131. // send raw predicted time without the offset applied yet.
  132. // we then apply the offset to it after.
  133. NetworkPingMessage pingMessage = new NetworkPingMessage
  134. (
  135. localTime,
  136. predictedTime
  137. );
  138. NetworkClient.Send(pingMessage, Channels.Unreliable);
  139. lastPingTime = localTime;
  140. }
  141. }
  142. // client rtt calculation //////////////////////////////////////////////
  143. // executed at the server when we receive a ping message
  144. // reply with a pong containing the time from the client
  145. // and time from the server
  146. internal static void OnServerPing(NetworkConnectionToClient conn, NetworkPingMessage message)
  147. {
  148. // calculate the prediction offset that the client needs to apply to unadjusted time to reach server time.
  149. // this will be sent back to client for corrections.
  150. double unadjustedError = localTime - message.localTime;
  151. // to see how well the client's final prediction worked, compare with adjusted time.
  152. // this is purely for debugging.
  153. // >0 means: server is ... seconds ahead of client's prediction (good if small)
  154. // <0 means: server is ... seconds behind client's prediction.
  155. // in other words, client is predicting too far ahead (not good)
  156. double adjustedError = localTime - message.predictedTimeAdjusted;
  157. // Debug.Log($"[Server] unadjustedError:{(unadjustedError*1000):F1}ms adjustedError:{(adjustedError*1000):F1}ms");
  158. // Debug.Log($"OnServerPing conn:{conn}");
  159. NetworkPongMessage pongMessage = new NetworkPongMessage
  160. (
  161. message.localTime,
  162. unadjustedError,
  163. adjustedError
  164. );
  165. conn.Send(pongMessage, Channels.Unreliable);
  166. }
  167. // Executed at the client when we receive a Pong message
  168. // find out how long it took since we sent the Ping
  169. // and update time offset & prediction offset.
  170. internal static void OnClientPong(NetworkPongMessage message)
  171. {
  172. // prevent attackers from sending timestamps which are in the future
  173. if (message.localTime > localTime) return;
  174. // how long did this message take to come back
  175. double newRtt = localTime - message.localTime;
  176. _rtt.Add(newRtt);
  177. // feed unadjusted prediction error into our exponential moving average
  178. // store adjusted prediction error for debug / GUI purposes
  179. _predictionErrorUnadjusted.Add(message.predictionErrorUnadjusted);
  180. predictionErrorAdjusted = message.predictionErrorAdjusted;
  181. // Debug.Log($"[Client] predictionError avg={(_predictionErrorUnadjusted.Value*1000):F1} ms");
  182. }
  183. // server rtt calculation //////////////////////////////////////////////
  184. // Executed at the client when we receive a ping message from the server.
  185. // in other words, this is for server sided ping + rtt calculation.
  186. // reply with a pong containing the time from the server
  187. internal static void OnClientPing(NetworkPingMessage message)
  188. {
  189. // Debug.Log($"OnClientPing conn:{conn}");
  190. NetworkPongMessage pongMessage = new NetworkPongMessage
  191. (
  192. message.localTime,
  193. 0, 0 // server doesn't predict
  194. );
  195. NetworkClient.Send(pongMessage, Channels.Unreliable);
  196. }
  197. // Executed at the server when we receive a Pong message back.
  198. // find out how long it took since we sent the Ping
  199. // and update time offset
  200. internal static void OnServerPong(NetworkConnectionToClient conn, NetworkPongMessage message)
  201. {
  202. // prevent attackers from sending timestamps which are in the future
  203. if (message.localTime > localTime) return;
  204. // how long did this message take to come back
  205. double newRtt = localTime - message.localTime;
  206. conn._rtt.Add(newRtt);
  207. }
  208. internal static void EarlyUpdate()
  209. {
  210. #if !UNITY_2020_3_OR_NEWER
  211. localFrameTime = stopwatch.Elapsed.TotalSeconds;
  212. #endif
  213. }
  214. }
  215. }