NetworkTransformBase.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. // NetworkTransform V2 aka project Oumuamua by vis2k (2021-07)
  2. // Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/
  3. //
  4. // Base class for NetworkTransform and NetworkTransformChild.
  5. // => simple unreliable sync without any interpolation for now.
  6. // => which means we don't need teleport detection either
  7. //
  8. // NOTE: several functions are virtual in case someone needs to modify a part.
  9. //
  10. // Channel: uses UNRELIABLE at all times.
  11. // -> out of order packets are dropped automatically
  12. // -> it's better than RELIABLE for several reasons:
  13. // * head of line blocking would add delay
  14. // * resending is mostly pointless
  15. // * bigger data race:
  16. // -> if we use a Cmd() at position X over reliable
  17. // -> client gets Cmd() and X at the same time, but buffers X for bufferTime
  18. // -> for unreliable, it would get X before the reliable Cmd(), still
  19. // buffer for bufferTime but end up closer to the original time
  20. using System;
  21. using System.Collections.Generic;
  22. using UnityEngine;
  23. namespace Mirror
  24. {
  25. public abstract class NetworkTransformBase : NetworkBehaviour
  26. {
  27. // TODO SyncDirection { CLIENT_TO_SERVER, SERVER_TO_CLIENT } is easier?
  28. [Header("Authority")]
  29. [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
  30. public bool clientAuthority;
  31. public bool useLocalSpace;
  32. // Is this a client with authority over this transform?
  33. // This component could be on the player object or any object that has been assigned authority to this client.
  34. protected bool IsClientWithAuthority => hasAuthority && clientAuthority;
  35. // target transform to sync. can be on a child.
  36. protected abstract Transform targetComponent { get; }
  37. [Header("Synchronization")]
  38. [Range(0, 1)] public float sendInterval = 0.050f;
  39. public bool syncPosition = true;
  40. public bool syncRotation = true;
  41. // scale sync is rare. off by default.
  42. public bool syncScale = false;
  43. double lastClientSendTime;
  44. double lastServerSendTime;
  45. // not all games need to interpolate. a board game might jump to the
  46. // final position immediately.
  47. [Header("Interpolation")]
  48. public bool interpolatePosition = true;
  49. public bool interpolateRotation = true;
  50. public bool interpolateScale = false;
  51. // "Experimentally I’ve found that the amount of delay that works best
  52. // at 2-5% packet loss is 3X the packet send rate"
  53. // NOTE: we do NOT use a dyanmically changing buffer size.
  54. // it would come with a lot of complications, e.g. buffer time
  55. // advantages/disadvantages for different connections.
  56. // Glenn Fiedler's recommendation seems solid, and should cover
  57. // the vast majority of connections.
  58. // (a player with 2000ms latency will have issues no matter what)
  59. [Header("Buffering")]
  60. [Tooltip("Snapshots are buffered for sendInterval * multiplier seconds. If your expected client base is to run at non-ideal connection quality (2-5% packet loss), 3x supposedly works best.")]
  61. public int bufferTimeMultiplier = 1;
  62. public float bufferTime => sendInterval * bufferTimeMultiplier;
  63. [Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")]
  64. public int bufferSizeLimit = 64;
  65. [Tooltip("Start to accelerate interpolation if buffer size is >= threshold. Needs to be larger than bufferTimeMultiplier.")]
  66. public int catchupThreshold = 4;
  67. [Tooltip("Once buffer is larger catchupThreshold, accelerate by multiplier % per excess entry.")]
  68. [Range(0, 1)] public float catchupMultiplier = 0.10f;
  69. // snapshots sorted by timestamp
  70. // in the original article, glenn fiedler drops any snapshots older than
  71. // the last received snapshot.
  72. // -> instead, we insert into a sorted buffer
  73. // -> the higher the buffer information density, the better
  74. // -> we still drop anything older than the first element in the buffer
  75. // => internal for testing
  76. //
  77. // IMPORTANT: of explicit 'NTSnapshot' type instead of 'Snapshot'
  78. // interface because List<interface> allocates through boxing
  79. internal SortedList<double, NTSnapshot> serverBuffer = new SortedList<double, NTSnapshot>();
  80. internal SortedList<double, NTSnapshot> clientBuffer = new SortedList<double, NTSnapshot>();
  81. // absolute interpolation time, moved along with deltaTime
  82. // (roughly between [0, delta] where delta is snapshot B - A timestamp)
  83. // (can be bigger than delta when overshooting)
  84. double serverInterpolationTime;
  85. double clientInterpolationTime;
  86. // only convert the static Interpolation function to Func<T> once to
  87. // avoid allocations
  88. Func<NTSnapshot, NTSnapshot, double, NTSnapshot> Interpolate = NTSnapshot.Interpolate;
  89. [Header("Debug")]
  90. public bool showGizmos;
  91. public bool showOverlay;
  92. public Color overlayColor = new Color(0, 0, 0, 0.5f);
  93. // snapshot functions //////////////////////////////////////////////////
  94. // construct a snapshot of the current state
  95. // => internal for testing
  96. protected virtual NTSnapshot ConstructSnapshot()
  97. {
  98. if (useLocalSpace)
  99. {
  100. // NetworkTime.localTime for double precision until Unity has it too
  101. return new NTSnapshot(
  102. // our local time is what the other end uses as remote time
  103. NetworkTime.localTime,
  104. // the other end fills out local time itself
  105. 0,
  106. targetComponent.localPosition,
  107. targetComponent.localRotation,
  108. targetComponent.localScale
  109. );
  110. }
  111. else
  112. {
  113. return new NTSnapshot(
  114. // our local time is what the other end uses as remote time
  115. NetworkTime.localTime,
  116. // the other end fills out local time itself
  117. 0,
  118. targetComponent.position,
  119. targetComponent.rotation,
  120. targetComponent.localScale
  121. );
  122. }
  123. }
  124. // apply a snapshot to the Transform.
  125. // -> start, end, interpolated are all passed in caes they are needed
  126. // -> a regular game would apply the 'interpolated' snapshot
  127. // -> a board game might want to jump to 'goal' directly
  128. // (it's easier to always interpolate and then apply selectively,
  129. // instead of manually interpolating x, y, z, ... depending on flags)
  130. // => internal for testing
  131. //
  132. // NOTE: stuck detection is unnecessary here.
  133. // we always set transform.position anyway, we can't get stuck.
  134. protected virtual void ApplySnapshot(NTSnapshot start, NTSnapshot goal, NTSnapshot interpolated)
  135. {
  136. // local position/rotation for VR support
  137. //
  138. // if syncPosition/Rotation/Scale is disabled then we received nulls
  139. // -> current position/rotation/scale would've been added as snapshot
  140. // -> we still interpolated
  141. // -> but simply don't apply it. if the user doesn't want to sync
  142. // scale, then we should not touch scale etc.
  143. if (useLocalSpace)
  144. {
  145. if (syncPosition)
  146. targetComponent.localPosition = interpolatePosition ? interpolated.position : goal.position;
  147. if (syncRotation)
  148. targetComponent.localRotation = interpolateRotation ? interpolated.rotation : goal.rotation;
  149. if (syncScale)
  150. targetComponent.localScale = interpolateScale ? interpolated.scale : goal.scale;
  151. }
  152. else
  153. {
  154. if (syncPosition)
  155. targetComponent.position = interpolatePosition ? interpolated.position : goal.position;
  156. if (syncRotation)
  157. targetComponent.rotation = interpolateRotation ? interpolated.rotation : goal.rotation;
  158. if (syncScale)
  159. targetComponent.localScale = interpolateScale ? interpolated.scale : goal.scale;
  160. }
  161. }
  162. // cmd /////////////////////////////////////////////////////////////////
  163. // only unreliable. see comment above of this file.
  164. [Command(channel = Channels.Unreliable)]
  165. void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
  166. {
  167. OnClientToServerSync(position, rotation, scale);
  168. //For client authority, immediately pass on the client snapshot to all other
  169. //clients instead of waiting for server to send its snapshots.
  170. if (clientAuthority)
  171. {
  172. RpcServerToClientSync(position, rotation, scale);
  173. }
  174. }
  175. // local authority client sends sync message to server for broadcasting
  176. protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
  177. {
  178. // only apply if in client authority mode
  179. if (!clientAuthority) return;
  180. // protect against ever growing buffer size attacks
  181. if (serverBuffer.Count >= bufferSizeLimit) return;
  182. // only player owned objects (with a connection) can send to
  183. // server. we can get the timestamp from the connection.
  184. double timestamp = connectionToClient.remoteTimeStamp;
  185. // position, rotation, scale can have no value if same as last time.
  186. // saves bandwidth.
  187. // but we still need to feed it to snapshot interpolation. we can't
  188. // just have gaps in there if nothing has changed. for example, if
  189. // client sends snapshot at t=0
  190. // client sends nothing for 10s because not moved
  191. // client sends snapshot at t=10
  192. // then the server would assume that it's one super slow move and
  193. // replay it for 10 seconds.
  194. if(useLocalSpace){
  195. if (!position.HasValue) position = targetComponent.localPosition;
  196. if (!rotation.HasValue) rotation = targetComponent.localRotation;
  197. if (!scale.HasValue) scale = targetComponent.localScale;
  198. }else{
  199. if (!position.HasValue) position = targetComponent.position;
  200. if (!rotation.HasValue) rotation = targetComponent.rotation;
  201. if (!scale.HasValue) scale = targetComponent.localScale;
  202. }
  203. // construct snapshot with batch timestamp to save bandwidth
  204. NTSnapshot snapshot = new NTSnapshot(
  205. timestamp,
  206. NetworkTime.localTime,
  207. position.Value, rotation.Value, scale.Value
  208. );
  209. // add to buffer (or drop if older than first element)
  210. SnapshotInterpolation.InsertIfNewEnough(snapshot, serverBuffer);
  211. }
  212. // rpc /////////////////////////////////////////////////////////////////
  213. // only unreliable. see comment above of this file.
  214. [ClientRpc(channel = Channels.Unreliable)]
  215. void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) =>
  216. OnServerToClientSync(position, rotation, scale);
  217. // server broadcasts sync message to all clients
  218. protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
  219. {
  220. // in host mode, the server sends rpcs to all clients.
  221. // the host client itself will receive them too.
  222. // -> host server is always the source of truth
  223. // -> we can ignore any rpc on the host client
  224. // => otherwise host objects would have ever growing clientBuffers
  225. // (rpc goes to clients. if isServer is true too then we are host)
  226. if (isServer) return;
  227. // don't apply for local player with authority
  228. if (IsClientWithAuthority) return;
  229. // protect against ever growing buffer size attacks
  230. if (clientBuffer.Count >= bufferSizeLimit) return;
  231. // on the client, we receive rpcs for all entities.
  232. // not all of them have a connectionToServer.
  233. // but all of them go through NetworkClient.connection.
  234. // we can get the timestamp from there.
  235. double timestamp = NetworkClient.connection.remoteTimeStamp;
  236. // position, rotation, scale can have no value if same as last time.
  237. // saves bandwidth.
  238. // but we still need to feed it to snapshot interpolation. we can't
  239. // just have gaps in there if nothing has changed. for example, if
  240. // client sends snapshot at t=0
  241. // client sends nothing for 10s because not moved
  242. // client sends snapshot at t=10
  243. // then the server would assume that it's one super slow move and
  244. // replay it for 10 seconds.
  245. if(useLocalSpace){
  246. if (!position.HasValue) position = targetComponent.localPosition;
  247. if (!rotation.HasValue) rotation = targetComponent.localRotation;
  248. if (!scale.HasValue) scale = targetComponent.localScale;
  249. }else{
  250. if (!position.HasValue) position = targetComponent.position;
  251. if (!rotation.HasValue) rotation = targetComponent.rotation;
  252. if (!scale.HasValue) scale = targetComponent.localScale;
  253. }
  254. // construct snapshot with batch timestamp to save bandwidth
  255. NTSnapshot snapshot = new NTSnapshot(
  256. timestamp,
  257. NetworkTime.localTime,
  258. position.Value, rotation.Value, scale.Value
  259. );
  260. // add to buffer (or drop if older than first element)
  261. SnapshotInterpolation.InsertIfNewEnough(snapshot, clientBuffer);
  262. }
  263. // update //////////////////////////////////////////////////////////////
  264. void UpdateServer()
  265. {
  266. // broadcast to all clients each 'sendInterval'
  267. // (client with authority will drop the rpc)
  268. // NetworkTime.localTime for double precision until Unity has it too
  269. //
  270. // IMPORTANT:
  271. // snapshot interpolation requires constant sending.
  272. // DO NOT only send if position changed. for example:
  273. // ---
  274. // * client sends first position at t=0
  275. // * ... 10s later ...
  276. // * client moves again, sends second position at t=10
  277. // ---
  278. // * server gets first position at t=0
  279. // * server gets second position at t=10
  280. // * server moves from first to second within a time of 10s
  281. // => would be a super slow move, instead of a wait & move.
  282. //
  283. // IMPORTANT:
  284. // DO NOT send nulls if not changed 'since last send' either. we
  285. // send unreliable and don't know which 'last send' the other end
  286. // received successfully.
  287. //
  288. // Checks to ensure server only sends snapshots if object is
  289. // on server authority(!clientAuthority) mode because on client
  290. // authority mode snapshots are broadcasted right after the authoritative
  291. // client updates server in the command function(see above), OR,
  292. // since host does not send anything to update the server, any client
  293. // authoritative movement done by the host will have to be broadcasted
  294. // here by checking IsClientWithAuthority.
  295. if (NetworkTime.localTime >= lastServerSendTime + sendInterval &&
  296. (!clientAuthority || IsClientWithAuthority))
  297. {
  298. // send snapshot without timestamp.
  299. // receiver gets it from batch timestamp to save bandwidth.
  300. NTSnapshot snapshot = ConstructSnapshot();
  301. RpcServerToClientSync(
  302. // only sync what the user wants to sync
  303. syncPosition ? snapshot.position : new Vector3?(),
  304. syncRotation ? snapshot.rotation : new Quaternion?(),
  305. syncScale ? snapshot.scale : new Vector3?()
  306. );
  307. lastServerSendTime = NetworkTime.localTime;
  308. }
  309. // apply buffered snapshots IF client authority
  310. // -> in server authority, server moves the object
  311. // so no need to apply any snapshots there.
  312. // -> don't apply for host mode player objects either, even if in
  313. // client authority mode. if it doesn't go over the network,
  314. // then we don't need to do anything.
  315. if (clientAuthority && !hasAuthority)
  316. {
  317. // compute snapshot interpolation & apply if any was spit out
  318. // TODO we don't have Time.deltaTime double yet. float is fine.
  319. if (SnapshotInterpolation.Compute(
  320. NetworkTime.localTime, Time.deltaTime,
  321. ref serverInterpolationTime,
  322. bufferTime, serverBuffer,
  323. catchupThreshold, catchupMultiplier,
  324. Interpolate,
  325. out NTSnapshot computed))
  326. {
  327. NTSnapshot start = serverBuffer.Values[0];
  328. NTSnapshot goal = serverBuffer.Values[1];
  329. ApplySnapshot(start, goal, computed);
  330. }
  331. }
  332. }
  333. void UpdateClient()
  334. {
  335. // client authority, and local player (= allowed to move myself)?
  336. if (IsClientWithAuthority)
  337. {
  338. // send to server each 'sendInterval'
  339. // NetworkTime.localTime for double precision until Unity has it too
  340. //
  341. // IMPORTANT:
  342. // snapshot interpolation requires constant sending.
  343. // DO NOT only send if position changed. for example:
  344. // ---
  345. // * client sends first position at t=0
  346. // * ... 10s later ...
  347. // * client moves again, sends second position at t=10
  348. // ---
  349. // * server gets first position at t=0
  350. // * server gets second position at t=10
  351. // * server moves from first to second within a time of 10s
  352. // => would be a super slow move, instead of a wait & move.
  353. //
  354. // IMPORTANT:
  355. // DO NOT send nulls if not changed 'since last send' either. we
  356. // send unreliable and don't know which 'last send' the other end
  357. // received successfully.
  358. if (NetworkTime.localTime >= lastClientSendTime + sendInterval)
  359. {
  360. // send snapshot without timestamp.
  361. // receiver gets it from batch timestamp to save bandwidth.
  362. NTSnapshot snapshot = ConstructSnapshot();
  363. CmdClientToServerSync(
  364. // only sync what the user wants to sync
  365. syncPosition ? snapshot.position : new Vector3?(),
  366. syncRotation ? snapshot.rotation : new Quaternion?(),
  367. syncScale ? snapshot.scale : new Vector3?()
  368. );
  369. lastClientSendTime = NetworkTime.localTime;
  370. }
  371. }
  372. // for all other clients (and for local player if !authority),
  373. // we need to apply snapshots from the buffer
  374. else
  375. {
  376. // compute snapshot interpolation & apply if any was spit out
  377. // TODO we don't have Time.deltaTime double yet. float is fine.
  378. if (SnapshotInterpolation.Compute(
  379. NetworkTime.localTime, Time.deltaTime,
  380. ref clientInterpolationTime,
  381. bufferTime, clientBuffer,
  382. catchupThreshold, catchupMultiplier,
  383. Interpolate,
  384. out NTSnapshot computed))
  385. {
  386. NTSnapshot start = clientBuffer.Values[0];
  387. NTSnapshot goal = clientBuffer.Values[1];
  388. ApplySnapshot(start, goal, computed);
  389. }
  390. }
  391. }
  392. void Update()
  393. {
  394. // if server then always sync to others.
  395. if (isServer) UpdateServer();
  396. // 'else if' because host mode shouldn't send anything to server.
  397. // it is the server. don't overwrite anything there.
  398. else if (isClient) UpdateClient();
  399. }
  400. // common Teleport code for client->server and server->client
  401. protected virtual void OnTeleport(Vector3 destination)
  402. {
  403. // reset any in-progress interpolation & buffers
  404. Reset();
  405. // set the new position.
  406. // interpolation will automatically continue.
  407. targetComponent.position = destination;
  408. // TODO
  409. // what if we still receive a snapshot from before the interpolation?
  410. // it could easily happen over unreliable.
  411. // -> maybe add destionation as first entry?
  412. }
  413. // server->client teleport to force position without interpolation.
  414. // otherwise it would interpolate to a (far away) new position.
  415. // => manually calling Teleport is the only 100% reliable solution.
  416. [ClientRpc]
  417. public void RpcTeleport(Vector3 destination)
  418. {
  419. // NOTE: even in client authority mode, the server is always allowed
  420. // to teleport the player. for example:
  421. // * CmdEnterPortal() might teleport the player
  422. // * Some people use client authority with server sided checks
  423. // so the server should be able to reset position if needed.
  424. // TODO what about host mode?
  425. OnTeleport(destination);
  426. }
  427. // client->server teleport to force position without interpolation.
  428. // otherwise it would interpolate to a (far away) new position.
  429. // => manually calling Teleport is the only 100% reliable solution.
  430. [Command]
  431. public void CmdTeleport(Vector3 destination)
  432. {
  433. // client can only teleport objects that it has authority over.
  434. if (!clientAuthority) return;
  435. // TODO what about host mode?
  436. OnTeleport(destination);
  437. // if a client teleports, we need to broadcast to everyone else too
  438. // TODO the teleported client should ignore the rpc though.
  439. // otherwise if it already moved again after teleporting,
  440. // the rpc would come a little bit later and reset it once.
  441. // TODO or not? if client ONLY calls Teleport(pos), the position
  442. // would only be set after the rpc. unless the client calls
  443. // BOTH Teleport(pos) and targetComponent.position=pos
  444. RpcTeleport(destination);
  445. }
  446. protected virtual void Reset()
  447. {
  448. // disabled objects aren't updated anymore.
  449. // so let's clear the buffers.
  450. serverBuffer.Clear();
  451. clientBuffer.Clear();
  452. // reset interpolation time too so we start at t=0 next time
  453. serverInterpolationTime = 0;
  454. clientInterpolationTime = 0;
  455. }
  456. protected virtual void OnDisable() => Reset();
  457. protected virtual void OnEnable() => Reset();
  458. protected virtual void OnValidate()
  459. {
  460. // make sure that catchup threshold is > buffer multiplier.
  461. // for a buffer multiplier of '3', we usually have at _least_ 3
  462. // buffered snapshots. often 4-5 even.
  463. //
  464. // catchUpThreshold should be a minimum of bufferTimeMultiplier + 3,
  465. // to prevent clashes with SnapshotInterpolation looking for at least
  466. // 3 old enough buffers, else catch up will be implemented while there
  467. // is not enough old buffers, and will result in jitter.
  468. // (validated with several real world tests by ninja & imer)
  469. catchupThreshold = Mathf.Max(bufferTimeMultiplier + 3, catchupThreshold);
  470. // buffer limit should be at least multiplier to have enough in there
  471. bufferSizeLimit = Mathf.Max(bufferTimeMultiplier, bufferSizeLimit);
  472. }
  473. // OnGUI allocates even if it does nothing. avoid in release.
  474. #if UNITY_EDITOR || DEVELOPMENT_BUILD
  475. // debug ///////////////////////////////////////////////////////////////
  476. protected virtual void OnGUI()
  477. {
  478. if (!showOverlay) return;
  479. // show data next to player for easier debugging. this is very useful!
  480. // IMPORTANT: this is basically an ESP hack for shooter games.
  481. // DO NOT make this available with a hotkey in release builds
  482. if (!Debug.isDebugBuild) return;
  483. // project position to screen
  484. Vector3 point = Camera.main.WorldToScreenPoint(targetComponent.position);
  485. // enough alpha, in front of camera and in screen?
  486. if (point.z >= 0 && Utils.IsPointInScreen(point))
  487. {
  488. // catchup is useful to show too
  489. int serverBufferExcess = Mathf.Max(serverBuffer.Count - catchupThreshold, 0);
  490. int clientBufferExcess = Mathf.Max(clientBuffer.Count - catchupThreshold, 0);
  491. float serverCatchup = serverBufferExcess * catchupMultiplier;
  492. float clientCatchup = clientBufferExcess * catchupMultiplier;
  493. GUI.color = overlayColor;
  494. GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100));
  495. // always show both client & server buffers so it's super
  496. // obvious if we accidentally populate both.
  497. GUILayout.Label($"Server Buffer:{serverBuffer.Count}");
  498. if (serverCatchup > 0)
  499. GUILayout.Label($"Server Catchup:{serverCatchup * 100:F2}%");
  500. GUILayout.Label($"Client Buffer:{clientBuffer.Count}");
  501. if (clientCatchup > 0)
  502. GUILayout.Label($"Client Catchup:{clientCatchup * 100:F2}%");
  503. GUILayout.EndArea();
  504. GUI.color = Color.white;
  505. }
  506. }
  507. protected virtual void DrawGizmos(SortedList<double, NTSnapshot> buffer)
  508. {
  509. // only draw if we have at least two entries
  510. if (buffer.Count < 2) return;
  511. // calcluate threshold for 'old enough' snapshots
  512. double threshold = NetworkTime.localTime - bufferTime;
  513. Color oldEnoughColor = new Color(0, 1, 0, 0.5f);
  514. Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f);
  515. // draw the whole buffer for easier debugging.
  516. // it's worth seeing how much we have buffered ahead already
  517. for (int i = 0; i < buffer.Count; ++i)
  518. {
  519. // color depends on if old enough or not
  520. NTSnapshot entry = buffer.Values[i];
  521. bool oldEnough = entry.localTimestamp <= threshold;
  522. Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
  523. Gizmos.DrawCube(entry.position, Vector3.one);
  524. }
  525. // extra: lines between start<->position<->goal
  526. Gizmos.color = Color.green;
  527. Gizmos.DrawLine(buffer.Values[0].position, targetComponent.position);
  528. Gizmos.color = Color.white;
  529. Gizmos.DrawLine(targetComponent.position, buffer.Values[1].position);
  530. }
  531. protected virtual void OnDrawGizmos()
  532. {
  533. // This fires in edit mode but that spams NRE's so check isPlaying
  534. if (!Application.isPlaying) return;
  535. if (!showGizmos) return;
  536. if (isServer) DrawGizmos(serverBuffer);
  537. if (isClient) DrawGizmos(clientBuffer);
  538. }
  539. #endif
  540. }
  541. }