NetworkTransformBase.cs 25 KB

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