ClientCube.cs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. // snapshot interpolation settings
  14. [Header("Snapshot Interpolation")]
  15. public SnapshotInterpolationSettings snapshotSettings =
  16. new SnapshotInterpolationSettings();
  17. // runtime settings
  18. public double bufferTime => server.sendInterval * snapshotSettings.bufferTimeMultiplier;
  19. // <servertime, snaps>
  20. public SortedList<double, Snapshot3D> snapshots = new SortedList<double, Snapshot3D>();
  21. // for smooth interpolation, we need to interpolate along server time.
  22. // any other time (arrival on client, client local time, etc.) is not
  23. // going to give smooth results.
  24. double localTimeline;
  25. // catchup / slowdown adjustments are applied to timescale,
  26. // to be adjusted in every update instead of when receiving messages.
  27. double localTimescale = 1;
  28. // we use EMA to average the last second worth of snapshot time diffs.
  29. // manually averaging the last second worth of values with a for loop
  30. // would be the same, but a moving average is faster because we only
  31. // ever add one value.
  32. ExponentialMovingAverage driftEma;
  33. ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
  34. // debugging ///////////////////////////////////////////////////////////
  35. [Header("Debug")]
  36. public Color catchupColor = Color.green; // green traffic light = go fast
  37. public Color slowdownColor = Color.red; // red traffic light = go slow
  38. Color defaultColor;
  39. [Header("Simulation")]
  40. bool lowFpsMode;
  41. double accumulatedDeltaTime;
  42. void Awake()
  43. {
  44. // show vsync reminder. too easy to forget.
  45. Debug.Log("Reminder: Snapshot interpolation is smoothest & easiest to debug with Vsync off.");
  46. defaultColor = render.sharedMaterial.color;
  47. // initialize EMA with 'emaDuration' seconds worth of history.
  48. // 1 second holds 'sendRate' worth of values.
  49. // multiplied by emaDuration gives n-seconds.
  50. driftEma = new ExponentialMovingAverage(server.sendRate * snapshotSettings.driftEmaDuration);
  51. deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * snapshotSettings.deliveryTimeEmaDuration);
  52. }
  53. // add snapshot & initialize client interpolation time if needed
  54. public void OnMessage(Snapshot3D snap)
  55. {
  56. // set local timestamp (= when it was received on our end)
  57. // Unity 2019 doesn't have Time.timeAsDouble yet
  58. snap.localTime = NetworkTime.localTime;
  59. // (optional) dynamic adjustment
  60. if (snapshotSettings.dynamicAdjustment)
  61. {
  62. // set bufferTime on the fly.
  63. // shows in inspector for easier debugging :)
  64. snapshotSettings.bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
  65. server.sendInterval,
  66. deliveryTimeEma.StandardDeviation,
  67. snapshotSettings.dynamicAdjustmentTolerance
  68. );
  69. }
  70. // insert into the buffer & initialize / adjust / catchup
  71. SnapshotInterpolation.InsertAndAdjust(
  72. snapshots,
  73. snapshotSettings.bufferLimit,
  74. snap,
  75. ref localTimeline,
  76. ref localTimescale,
  77. server.sendInterval,
  78. bufferTime,
  79. snapshotSettings.catchupSpeed,
  80. snapshotSettings.slowdownSpeed,
  81. ref driftEma,
  82. snapshotSettings.catchupNegativeThreshold,
  83. snapshotSettings.catchupPositiveThreshold,
  84. ref deliveryTimeEma);
  85. }
  86. void Update()
  87. {
  88. // accumulated delta allows us to simulate correct low fps + deltaTime
  89. // if necessary in client low fps mode.
  90. accumulatedDeltaTime += Time.unscaledDeltaTime;
  91. // simulate low fps mode. only update once per second.
  92. // to simulate webgl background tabs, etc.
  93. // after a while, disable low fps mode and see how it behaves.
  94. if (lowFpsMode && accumulatedDeltaTime < 1) return;
  95. // only while we have snapshots.
  96. // timeline starts when the first snapshot arrives.
  97. if (snapshots.Count > 0)
  98. {
  99. // snapshot interpolation
  100. if (interpolate)
  101. {
  102. // step
  103. SnapshotInterpolation.Step(
  104. snapshots,
  105. // accumulate delta is Time.unscaledDeltaTime normally.
  106. // and sum of past 10 delta's in low fps mode.
  107. accumulatedDeltaTime,
  108. ref localTimeline,
  109. localTimescale,
  110. out Snapshot3D fromSnapshot,
  111. out Snapshot3D toSnapshot,
  112. out double t);
  113. // interpolate & apply
  114. Snapshot3D computed = Snapshot3D.Interpolate(fromSnapshot, toSnapshot, t);
  115. transform.position = computed.position;
  116. }
  117. // apply raw
  118. else
  119. {
  120. Snapshot3D snap = snapshots.Values[0];
  121. transform.position = snap.position;
  122. snapshots.RemoveAt(0);
  123. }
  124. }
  125. // reset simulation helpers
  126. accumulatedDeltaTime = 0;
  127. // color material while catching up / slowing down
  128. if (localTimescale < 1)
  129. render.material.color = slowdownColor;
  130. else if (localTimescale > 1)
  131. render.material.color = catchupColor;
  132. else
  133. render.material.color = defaultColor;
  134. }
  135. void OnGUI()
  136. {
  137. // display buffer size as number for easier debugging.
  138. // catchup is displayed as color state in Update() already.
  139. const int width = 30; // fit 3 digits
  140. const int height = 20;
  141. Vector2 screen = Camera.main.WorldToScreenPoint(transform.position);
  142. string str = $"{snapshots.Count}";
  143. GUI.Label(new Rect(screen.x - width / 2, screen.y - height / 2, width, height), str);
  144. // client simulation buttons on the bottom of the screen
  145. float areaHeight = 100;
  146. GUILayout.BeginArea(new Rect(0, Screen.height - areaHeight, Screen.width, areaHeight));
  147. GUILayout.BeginHorizontal();
  148. GUILayout.Label("Client Simulation:");
  149. if (GUILayout.Button((lowFpsMode ? "Disable" : "Enable") + " 1 FPS"))
  150. {
  151. lowFpsMode = !lowFpsMode;
  152. }
  153. GUILayout.Label("|");
  154. if (GUILayout.Button("Timeline 10s behind"))
  155. {
  156. localTimeline -= 10.0;
  157. }
  158. if (GUILayout.Button("Timeline 1s behind"))
  159. {
  160. localTimeline -= 1.0;
  161. }
  162. if (GUILayout.Button("Timeline 0.1s behind"))
  163. {
  164. localTimeline -= 0.1;
  165. }
  166. GUILayout.Label("|");
  167. if (GUILayout.Button("Timeline 0.1s ahead"))
  168. {
  169. localTimeline += 0.1;
  170. }
  171. if (GUILayout.Button("Timeline 1s ahead"))
  172. {
  173. localTimeline += 1.0;
  174. }
  175. if (GUILayout.Button("Timeline 10s ahead"))
  176. {
  177. localTimeline += 10.0;
  178. }
  179. GUILayout.FlexibleSpace();
  180. GUILayout.EndHorizontal();
  181. GUILayout.EndArea();
  182. }
  183. void OnValidate()
  184. {
  185. // thresholds need to be <0 and >0
  186. snapshotSettings.catchupNegativeThreshold = Math.Min(snapshotSettings.catchupNegativeThreshold, 0);
  187. snapshotSettings.catchupPositiveThreshold = Math.Max(snapshotSettings.catchupPositiveThreshold, 0);
  188. }
  189. }
  190. }