NetworkTime.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. using System;
  2. using System.Runtime.CompilerServices;
  3. using UnityEngine;
  4. #if !UNITY_2020_3_OR_NEWER
  5. using Stopwatch = System.Diagnostics.Stopwatch;
  6. #endif
  7. namespace Mirror
  8. {
  9. /// <summary>Synchronizes server time to clients.</summary>
  10. public static class NetworkTime
  11. {
  12. /// <summary>Ping message frequency, used to calculate network time and RTT</summary>
  13. public static float PingFrequency = 2.0f;
  14. /// <summary>Average out the last few results from Ping</summary>
  15. public static int PingWindowSize = 10;
  16. static double lastPingTime;
  17. static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(10);
  18. static ExponentialMovingAverage _offset = new ExponentialMovingAverage(10);
  19. // the true offset guaranteed to be in this range
  20. static double offsetMin = double.MinValue;
  21. static double offsetMax = double.MaxValue;
  22. /// <summary>Returns double precision clock time _in this system_, unaffected by the network.</summary>
  23. #if UNITY_2020_3_OR_NEWER
  24. public static double localTime
  25. {
  26. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  27. get => Time.timeAsDouble;
  28. }
  29. #else
  30. // need stopwatch for older Unity versions, but it's quite slow.
  31. // CAREFUL: unlike Time.time, this is not a FRAME time.
  32. // it changes during the frame too.
  33. static readonly Stopwatch stopwatch = new Stopwatch();
  34. static NetworkTime() => stopwatch.Start();
  35. public static double localTime => stopwatch.Elapsed.TotalSeconds;
  36. #endif
  37. /// <summary>The time in seconds since the server started.</summary>
  38. //
  39. // I measured the accuracy of float and I got this:
  40. // for the same day, accuracy is better than 1 ms
  41. // after 1 day, accuracy goes down to 7 ms
  42. // after 10 days, accuracy is 61 ms
  43. // after 30 days , accuracy is 238 ms
  44. // after 60 days, accuracy is 454 ms
  45. // in other words, if the server is running for 2 months,
  46. // and you cast down to float, then the time will jump in 0.4s intervals.
  47. //
  48. // TODO consider using Unbatcher's remoteTime for NetworkTime
  49. public static double time
  50. {
  51. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  52. get => localTime - _offset.Value;
  53. }
  54. /// <summary>Time measurement variance. The higher, the less accurate the time is.</summary>
  55. // TODO does this need to be public? user should only need NetworkTime.time
  56. public static double timeVariance => _offset.Var;
  57. /// <summary>Time standard deviation. The highe, the less accurate the time is.</summary>
  58. // TODO does this need to be public? user should only need NetworkTime.time
  59. public static double timeStandardDeviation => Math.Sqrt(timeVariance);
  60. /// <summary>Clock difference in seconds between the client and the server. Always 0 on server.</summary>
  61. public static double offset => _offset.Value;
  62. /// <summary>Round trip time (in seconds) that it takes a message to go client->server->client.</summary>
  63. public static double rtt => _rtt.Value;
  64. /// <summary>Round trip time variance. The higher, the less accurate the rtt is.</summary>
  65. // TODO does this need to be public? user should only need NetworkTime.time
  66. public static double rttVariance => _rtt.Var;
  67. /// <summary>Round trip time standard deviation. The higher, the less accurate the rtt is.</summary>
  68. // TODO does this need to be public? user should only need NetworkTime.time
  69. public static double rttStandardDeviation => Math.Sqrt(rttVariance);
  70. // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
  71. [UnityEngine.RuntimeInitializeOnLoadMethod]
  72. public static void ResetStatics()
  73. {
  74. PingFrequency = 2.0f;
  75. PingWindowSize = 10;
  76. lastPingTime = 0;
  77. _rtt = new ExponentialMovingAverage(PingWindowSize);
  78. _offset = new ExponentialMovingAverage(PingWindowSize);
  79. offsetMin = double.MinValue;
  80. offsetMax = double.MaxValue;
  81. #if !UNITY_2020_3_OR_NEWER
  82. stopwatch.Restart();
  83. #endif
  84. }
  85. internal static void UpdateClient()
  86. {
  87. // localTime (double) instead of Time.time for accuracy over days
  88. if (localTime - lastPingTime >= PingFrequency)
  89. {
  90. NetworkPingMessage pingMessage = new NetworkPingMessage(localTime);
  91. NetworkClient.Send(pingMessage, Channels.Unreliable);
  92. lastPingTime = localTime;
  93. }
  94. }
  95. // executed at the server when we receive a ping message
  96. // reply with a pong containing the time from the client
  97. // and time from the server
  98. internal static void OnServerPing(NetworkConnectionToClient conn, NetworkPingMessage message)
  99. {
  100. // Debug.Log($"OnPingServerMessage conn:{conn}");
  101. NetworkPongMessage pongMessage = new NetworkPongMessage
  102. {
  103. clientTime = message.clientTime,
  104. serverTime = localTime
  105. };
  106. conn.Send(pongMessage, Channels.Unreliable);
  107. }
  108. // Executed at the client when we receive a Pong message
  109. // find out how long it took since we sent the Ping
  110. // and update time offset
  111. internal static void OnClientPong(NetworkPongMessage message)
  112. {
  113. double now = localTime;
  114. // how long did this message take to come back
  115. double newRtt = now - message.clientTime;
  116. _rtt.Add(newRtt);
  117. // the difference in time between the client and the server
  118. // but subtract half of the rtt to compensate for latency
  119. // half of rtt is the best approximation we have
  120. double newOffset = now - newRtt * 0.5f - message.serverTime;
  121. double newOffsetMin = now - newRtt - message.serverTime;
  122. double newOffsetMax = now - message.serverTime;
  123. offsetMin = Math.Max(offsetMin, newOffsetMin);
  124. offsetMax = Math.Min(offsetMax, newOffsetMax);
  125. if (_offset.Value < offsetMin || _offset.Value > offsetMax)
  126. {
  127. // the old offset was offrange, throw it away and use new one
  128. _offset = new ExponentialMovingAverage(PingWindowSize);
  129. _offset.Add(newOffset);
  130. }
  131. else if (newOffset >= offsetMin || newOffset <= offsetMax)
  132. {
  133. // new offset looks reasonable, add to the average
  134. _offset.Add(newOffset);
  135. }
  136. }
  137. }
  138. }