ClientCube.cs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. namespace Mirror.Examples.LagCompensationDemo
  6. {
  7. public class ClientCube : MonoBehaviour
  8. {
  9. [Header("Components")]
  10. public ServerCube server;
  11. public Renderer render;
  12. [Header("Toggle")]
  13. public bool interpolate = true;
  14. // snapshot interpolation settings
  15. [Header("Snapshot Interpolation")]
  16. public SnapshotInterpolationSettings snapshotSettings =
  17. new SnapshotInterpolationSettings();
  18. // runtime settings
  19. public double bufferTime => server.sendInterval * snapshotSettings.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. internal 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. // we use EMA to average the last second worth of snapshot time diffs.
  30. // manually averaging the last second worth of values with a for loop
  31. // would be the same, but a moving average is faster because we only
  32. // ever add one value.
  33. ExponentialMovingAverage driftEma;
  34. ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
  35. // debugging ///////////////////////////////////////////////////////////
  36. [Header("Debug")]
  37. public Color hitColor = Color.blue;
  38. public Color missedColor = Color.magenta;
  39. public Color originalColor = Color.black;
  40. [Header("Simulation")]
  41. bool lowFpsMode;
  42. double accumulatedDeltaTime;
  43. void Awake()
  44. {
  45. // defaultColor = render.sharedMaterial.color;
  46. // initialize EMA with 'emaDuration' seconds worth of history.
  47. // 1 second holds 'sendRate' worth of values.
  48. // multiplied by emaDuration gives n-seconds.
  49. driftEma = new ExponentialMovingAverage(server.sendRate * snapshotSettings.driftEmaDuration);
  50. deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * snapshotSettings.deliveryTimeEmaDuration);
  51. }
  52. // add snapshot & initialize client interpolation time if needed
  53. public void OnMessage(Snapshot3D snap)
  54. {
  55. // set local timestamp (= when it was received on our end)
  56. // Unity 2019 doesn't have Time.timeAsDouble yet
  57. snap.localTime = NetworkTime.localTime;
  58. // (optional) dynamic adjustment
  59. if (snapshotSettings.dynamicAdjustment)
  60. {
  61. // set bufferTime on the fly.
  62. // shows in inspector for easier debugging :)
  63. snapshotSettings.bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
  64. server.sendInterval,
  65. deliveryTimeEma.StandardDeviation,
  66. snapshotSettings.dynamicAdjustmentTolerance
  67. );
  68. }
  69. // insert into the buffer & initialize / adjust / catchup
  70. SnapshotInterpolation.InsertAndAdjust(
  71. snapshots,
  72. snapshotSettings.bufferLimit,
  73. snap,
  74. ref localTimeline,
  75. ref localTimescale,
  76. server.sendInterval,
  77. bufferTime,
  78. snapshotSettings.catchupSpeed,
  79. snapshotSettings.slowdownSpeed,
  80. ref driftEma,
  81. snapshotSettings.catchupNegativeThreshold,
  82. snapshotSettings.catchupPositiveThreshold,
  83. ref deliveryTimeEma);
  84. }
  85. void Update()
  86. {
  87. // accumulated delta allows us to simulate correct low fps + deltaTime
  88. // if necessary in client low fps mode.
  89. accumulatedDeltaTime += Time.unscaledDeltaTime;
  90. // simulate low fps mode. only update once per second.
  91. // to simulate webgl background tabs, etc.
  92. // after a while, disable low fps mode and see how it behaves.
  93. if (lowFpsMode && accumulatedDeltaTime < 1) return;
  94. // only while we have snapshots.
  95. // timeline starts when the first snapshot arrives.
  96. if (snapshots.Count > 0)
  97. {
  98. // snapshot interpolation
  99. if (interpolate)
  100. {
  101. // step
  102. SnapshotInterpolation.Step(
  103. snapshots,
  104. // accumulate delta is Time.unscaledDeltaTime normally.
  105. // and sum of past 10 delta's in low fps mode.
  106. accumulatedDeltaTime,
  107. ref localTimeline,
  108. localTimescale,
  109. out Snapshot3D fromSnapshot,
  110. out Snapshot3D toSnapshot,
  111. out double t);
  112. // interpolate & apply
  113. Snapshot3D computed = Snapshot3D.Interpolate(fromSnapshot, toSnapshot, t);
  114. transform.position = computed.position;
  115. }
  116. // apply raw
  117. else
  118. {
  119. Snapshot3D snap = snapshots.Values[0];
  120. transform.position = snap.position;
  121. snapshots.RemoveAt(0);
  122. }
  123. }
  124. // reset simulation helpers
  125. accumulatedDeltaTime = 0;
  126. }
  127. void OnMouseDown()
  128. {
  129. // send the command.
  130. // only x coordinate matters for this simple example.
  131. if (server.CmdClicked(transform.position))
  132. {
  133. Debug.Log($"Click hit!");
  134. FlashColor(hitColor);
  135. }
  136. else
  137. {
  138. Debug.Log($"Click missed!");
  139. FlashColor(missedColor);
  140. }
  141. }
  142. // simple visual indicator for better feedback.
  143. // changes the cube's color for a short time.
  144. void FlashColor(Color color) =>
  145. StartCoroutine(TemporarilyChangeColorToGreen(color));
  146. IEnumerator TemporarilyChangeColorToGreen(Color color)
  147. {
  148. Renderer r = GetComponentInChildren<Renderer>();
  149. r.material.color = color;
  150. yield return new WaitForSeconds(0.2f);
  151. r.material.color = originalColor;
  152. }
  153. void OnGUI()
  154. {
  155. // display buffer size as number for easier debugging.
  156. // catchup is displayed as color state in Update() already.
  157. const int width = 30; // fit 3 digits
  158. const int height = 20;
  159. Vector2 screen = Camera.main.WorldToScreenPoint(transform.position);
  160. string str = $"{snapshots.Count}";
  161. GUI.Label(new Rect(screen.x - width / 2, screen.y - height / 2, width, height), str);
  162. // client simulation buttons on the bottom of the screen
  163. float areaHeight = 150;
  164. GUILayout.BeginArea(new Rect(0, Screen.height - areaHeight, Screen.width, areaHeight));
  165. GUILayout.Label("Click the black cube. Lag compensation will correct the latency.");
  166. GUILayout.BeginHorizontal();
  167. GUILayout.Label("Client Simulation:");
  168. if (GUILayout.Button((lowFpsMode ? "Disable" : "Enable") + " 1 FPS"))
  169. {
  170. lowFpsMode = !lowFpsMode;
  171. }
  172. GUILayout.Label("|");
  173. if (GUILayout.Button("Timeline 10s behind"))
  174. {
  175. localTimeline -= 10.0;
  176. }
  177. if (GUILayout.Button("Timeline 1s behind"))
  178. {
  179. localTimeline -= 1.0;
  180. }
  181. if (GUILayout.Button("Timeline 0.1s behind"))
  182. {
  183. localTimeline -= 0.1;
  184. }
  185. GUILayout.Label("|");
  186. if (GUILayout.Button("Timeline 0.1s ahead"))
  187. {
  188. localTimeline += 0.1;
  189. }
  190. if (GUILayout.Button("Timeline 1s ahead"))
  191. {
  192. localTimeline += 1.0;
  193. }
  194. if (GUILayout.Button("Timeline 10s ahead"))
  195. {
  196. localTimeline += 10.0;
  197. }
  198. GUILayout.FlexibleSpace();
  199. GUILayout.EndHorizontal();
  200. GUILayout.EndArea();
  201. }
  202. void OnValidate()
  203. {
  204. // thresholds need to be <0 and >0
  205. snapshotSettings.catchupNegativeThreshold = Math.Min(snapshotSettings.catchupNegativeThreshold, 0);
  206. snapshotSettings.catchupPositiveThreshold = Math.Max(snapshotSettings.catchupPositiveThreshold, 0);
  207. }
  208. }
  209. }