ClientCube.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. namespace Mirror.Examples.SnapshotInterpolationDemo
  5. {
  6. public class ClientCube : MonoBehaviour
  7. {
  8. [Header("Components")]
  9. public ServerCube server;
  10. public Renderer render;
  11. [Header("Toggle")]
  12. public bool interpolate = true;
  13. // decrease bufferTime at runtime to see the catchup effect.
  14. // increase to see slowdown.
  15. // 'double' so we can have very precise dynamic adjustment without rounding
  16. [Header("Buffering")]
  17. [Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")]
  18. public double bufferTimeMultiplier = 2;
  19. public double bufferTime => server.sendInterval * bufferTimeMultiplier;
  20. // <servertime, snaps>
  21. public SortedList<double, Snapshot3D> snapshots = new SortedList<double, Snapshot3D>();
  22. // for smooth interpolation, we need to interpolate along server time.
  23. // any other time (arrival on client, client local time, etc.) is not
  24. // going to give smooth results.
  25. double localTimeline;
  26. // catchup / slowdown adjustments are applied to timescale,
  27. // to be adjusted in every update instead of when receiving messages.
  28. double localTimescale = 1;
  29. // catchup /////////////////////////////////////////////////////////////
  30. // catchup thresholds in 'frames'.
  31. // half a frame might be too aggressive.
  32. [Header("Catchup / Slowdown")]
  33. [Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")]
  34. public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
  35. [Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")]
  36. public float catchupPositiveThreshold = 1;
  37. [Tooltip("Local timeline acceleration in % while catching up.")]
  38. [Range(0, 1)]
  39. public double catchupSpeed = 0.01f; // 1%
  40. [Tooltip("Local timeline slowdown in % while slowing down.")]
  41. [Range(0, 1)]
  42. public double slowdownSpeed = 0.01f; // 1%
  43. [Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
  44. public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
  45. // we use EMA to average the last second worth of snapshot time diffs.
  46. // manually averaging the last second worth of values with a for loop
  47. // would be the same, but a moving average is faster because we only
  48. // ever add one value.
  49. ExponentialMovingAverage driftEma;
  50. // dynamic buffer time adjustment //////////////////////////////////////
  51. // dynamically adjusts bufferTimeMultiplier for smooth results.
  52. // to understand how this works, try this manually:
  53. //
  54. // - disable dynamic adjustment
  55. // - set jitter = 0.2 (20% is a lot!)
  56. // - notice some stuttering
  57. // - disable interpolation to see just how much jitter this really is(!)
  58. // - enable interpolation again
  59. // - manually increase bufferTimeMultiplier to 3-4
  60. // ... the cube slows down (blue) until it's smooth
  61. // - with dynamic adjustment enabled, it will set 4 automatically
  62. // ... the cube slows down (blue) until it's smooth as well
  63. //
  64. // note that 20% jitter is extreme.
  65. // for this to be perfectly smooth, set the safety tolerance to '2'.
  66. // but realistically this is not necessary, and '1' is enough.
  67. [Header("Dynamic Adjustment")]
  68. [Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")]
  69. public bool dynamicAdjustment = true;
  70. [Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")]
  71. public float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments)
  72. [Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")]
  73. public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
  74. ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
  75. // debugging ///////////////////////////////////////////////////////////
  76. [Header("Debug")]
  77. public Color catchupColor = Color.green; // green traffic light = go fast
  78. public Color slowdownColor = Color.red; // red traffic light = go slow
  79. Color defaultColor;
  80. void Awake()
  81. {
  82. defaultColor = render.sharedMaterial.color;
  83. // initialize EMA with 'emaDuration' seconds worth of history.
  84. // 1 second holds 'sendRate' worth of values.
  85. // multiplied by emaDuration gives n-seconds.
  86. driftEma = new ExponentialMovingAverage(server.sendRate * driftEmaDuration);
  87. deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * deliveryTimeEmaDuration);
  88. }
  89. // add snapshot & initialize client interpolation time if needed
  90. public void OnMessage(Snapshot3D snap)
  91. {
  92. // set local timestamp (= when it was received on our end)
  93. #if !UNITY_2020_3_OR_NEWER
  94. snap.localTime = NetworkTime.localTime;
  95. #else
  96. snap.localTime = Time.timeAsDouble;
  97. #endif
  98. // (optional) dynamic adjustment
  99. if (dynamicAdjustment)
  100. {
  101. // set bufferTime on the fly.
  102. // shows in inspector for easier debugging :)
  103. bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
  104. server.sendInterval,
  105. deliveryTimeEma.StandardDeviation,
  106. dynamicAdjustmentTolerance
  107. );
  108. }
  109. // insert into the buffer & initialize / adjust / catchup
  110. SnapshotInterpolation.InsertAndAdjust(
  111. snapshots,
  112. snap,
  113. ref localTimeline,
  114. ref localTimescale,
  115. server.sendInterval,
  116. bufferTime,
  117. catchupSpeed,
  118. slowdownSpeed,
  119. ref driftEma,
  120. catchupNegativeThreshold,
  121. catchupPositiveThreshold,
  122. ref deliveryTimeEma);
  123. }
  124. void Update()
  125. {
  126. // only while we have snapshots.
  127. // timeline starts when the first snapshot arrives.
  128. if (snapshots.Count > 0)
  129. {
  130. // snapshot interpolation
  131. if (interpolate)
  132. {
  133. // step
  134. SnapshotInterpolation.Step(
  135. snapshots,
  136. Time.unscaledDeltaTime,
  137. ref localTimeline,
  138. localTimescale,
  139. out Snapshot3D fromSnapshot,
  140. out Snapshot3D toSnapshot,
  141. out double t);
  142. // interpolate & apply
  143. Snapshot3D computed = Snapshot3D.Interpolate(fromSnapshot, toSnapshot, t);
  144. transform.position = computed.position;
  145. }
  146. // apply raw
  147. else
  148. {
  149. Snapshot3D snap = snapshots.Values[0];
  150. transform.position = snap.position;
  151. snapshots.RemoveAt(0);
  152. }
  153. }
  154. // color material while catching up / slowing down
  155. if (localTimescale < 1)
  156. render.material.color = slowdownColor;
  157. else if (localTimescale > 1)
  158. render.material.color = catchupColor;
  159. else
  160. render.material.color = defaultColor;
  161. }
  162. void OnGUI()
  163. {
  164. // display buffer size as number for easier debugging.
  165. // catchup is displayed as color state in Update() already.
  166. const int width = 30; // fit 3 digits
  167. const int height = 20;
  168. Vector2 screen = Camera.main.WorldToScreenPoint(transform.position);
  169. string str = $"{snapshots.Count}";
  170. GUI.Label(new Rect(screen.x - width / 2, screen.y - height / 2, width, height), str);
  171. }
  172. void OnValidate()
  173. {
  174. // thresholds need to be <0 and >0
  175. catchupNegativeThreshold = Math.Min(catchupNegativeThreshold, 0);
  176. catchupPositiveThreshold = Math.Max(catchupPositiveThreshold, 0);
  177. }
  178. }
  179. }