NetworkTransformBase.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. // vis2k:
  2. // base class for NetworkTransform and NetworkTransformChild.
  3. // New method is simple and stupid. No more 1500 lines of code.
  4. //
  5. // Server sends current data.
  6. // Client saves it and interpolates last and latest data points.
  7. // Update handles transform movement / rotation
  8. // FixedUpdate handles rigidbody movement / rotation
  9. //
  10. // Notes:
  11. // * Built-in Teleport detection in case of lags / teleport / obstacles
  12. // * Quaternion > EulerAngles because gimbal lock and Quaternion.Slerp
  13. // * Syncs XYZ. Works 3D and 2D. Saving 4 bytes isn't worth 1000 lines of code.
  14. // * Initial delay might happen if server sends packet immediately after moving
  15. // just 1cm, hence we move 1cm and then wait 100ms for next packet
  16. // * Only way for smooth movement is to use a fixed movement speed during
  17. // interpolation. interpolation over time is never that good.
  18. //
  19. using System;
  20. using UnityEngine;
  21. namespace Mirror.Experimental
  22. {
  23. // Deprecated 2022-01-18
  24. [Obsolete("Use the default NetworkTransform instead, it has proper snapshot interpolation.")]
  25. public abstract class NetworkTransformBase : NetworkBehaviour
  26. {
  27. // target transform to sync. can be on a child.
  28. protected abstract Transform targetTransform { get; }
  29. [Header("Authority")]
  30. [Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
  31. [SyncVar]
  32. public bool clientAuthority;
  33. [Tooltip("Set to true if updates from server should be ignored by owner")]
  34. [SyncVar]
  35. public bool excludeOwnerUpdate = true;
  36. [Header("Synchronization")]
  37. [Tooltip("Set to true if position should be synchronized")]
  38. [SyncVar]
  39. public bool syncPosition = true;
  40. [Tooltip("Set to true if rotation should be synchronized")]
  41. [SyncVar]
  42. public bool syncRotation = true;
  43. [Tooltip("Set to true if scale should be synchronized")]
  44. [SyncVar]
  45. public bool syncScale = true;
  46. [Header("Interpolation")]
  47. [Tooltip("Set to true if position should be interpolated")]
  48. [SyncVar]
  49. public bool interpolatePosition = true;
  50. [Tooltip("Set to true if rotation should be interpolated")]
  51. [SyncVar]
  52. public bool interpolateRotation = true;
  53. [Tooltip("Set to true if scale should be interpolated")]
  54. [SyncVar]
  55. public bool interpolateScale = true;
  56. // Sensitivity is added for VR where human players tend to have micro movements so this can quiet down
  57. // the network traffic. Additionally, rigidbody drift should send less traffic, e.g very slow sliding / rolling.
  58. [Header("Sensitivity")]
  59. [Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
  60. [SyncVar]
  61. public float localPositionSensitivity = .01f;
  62. [Tooltip("If rotation exceeds this angle, it will be transmitted on the network")]
  63. [SyncVar]
  64. public float localRotationSensitivity = .01f;
  65. [Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
  66. [SyncVar]
  67. public float localScaleSensitivity = .01f;
  68. [Header("Diagnostics")]
  69. // server
  70. public Vector3 lastPosition;
  71. public Quaternion lastRotation;
  72. public Vector3 lastScale;
  73. // client
  74. // use local position/rotation for VR support
  75. [Serializable]
  76. public struct DataPoint
  77. {
  78. public float timeStamp;
  79. public Vector3 localPosition;
  80. public Quaternion localRotation;
  81. public Vector3 localScale;
  82. public float movementSpeed;
  83. public bool isValid => timeStamp != 0;
  84. }
  85. // Is this a client with authority over this transform?
  86. // This component could be on the player object or any object that has been assigned authority to this client.
  87. bool IsOwnerWithClientAuthority => hasAuthority && clientAuthority;
  88. // interpolation start and goal
  89. public DataPoint start = new DataPoint();
  90. public DataPoint goal = new DataPoint();
  91. // We need to store this locally on the server so clients can't request Authority when ever they like
  92. bool clientAuthorityBeforeTeleport;
  93. void FixedUpdate()
  94. {
  95. // if server then always sync to others.
  96. // let the clients know that this has moved
  97. if (isServer && HasEitherMovedRotatedScaled())
  98. {
  99. ServerUpdate();
  100. }
  101. if (isClient)
  102. {
  103. // send to server if we have local authority (and aren't the server)
  104. // -> only if connectionToServer has been initialized yet too
  105. if (IsOwnerWithClientAuthority)
  106. {
  107. ClientAuthorityUpdate();
  108. }
  109. else if (goal.isValid)
  110. {
  111. ClientRemoteUpdate();
  112. }
  113. }
  114. }
  115. void ServerUpdate()
  116. {
  117. RpcMove(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
  118. }
  119. void ClientAuthorityUpdate()
  120. {
  121. if (!isServer && HasEitherMovedRotatedScaled())
  122. {
  123. // serialize
  124. // local position/rotation for VR support
  125. // send to server
  126. CmdClientToServerSync(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
  127. }
  128. }
  129. void ClientRemoteUpdate()
  130. {
  131. // teleport or interpolate
  132. if (NeedsTeleport())
  133. {
  134. // local position/rotation for VR support
  135. ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
  136. // reset data points so we don't keep interpolating
  137. start = new DataPoint();
  138. goal = new DataPoint();
  139. }
  140. else
  141. {
  142. // local position/rotation for VR support
  143. ApplyPositionRotationScale(InterpolatePosition(start, goal, targetTransform.localPosition),
  144. InterpolateRotation(start, goal, targetTransform.localRotation),
  145. InterpolateScale(start, goal, targetTransform.localScale));
  146. }
  147. }
  148. // moved or rotated or scaled since last time we checked it?
  149. bool HasEitherMovedRotatedScaled()
  150. {
  151. // Save last for next frame to compare only if change was detected, otherwise
  152. // slow moving objects might never sync because of C#'s float comparison tolerance.
  153. // See also: https://github.com/vis2k/Mirror/pull/428)
  154. bool changed = HasMoved || HasRotated || HasScaled;
  155. if (changed)
  156. {
  157. // local position/rotation for VR support
  158. if (syncPosition) lastPosition = targetTransform.localPosition;
  159. if (syncRotation) lastRotation = targetTransform.localRotation;
  160. if (syncScale) lastScale = targetTransform.localScale;
  161. }
  162. return changed;
  163. }
  164. // local position/rotation for VR support
  165. // SqrMagnitude is faster than Distance per Unity docs
  166. // https://docs.unity3d.com/ScriptReference/Vector3-sqrMagnitude.html
  167. bool HasMoved => syncPosition && Vector3.SqrMagnitude(lastPosition - targetTransform.localPosition) > localPositionSensitivity * localPositionSensitivity;
  168. bool HasRotated => syncRotation && Quaternion.Angle(lastRotation, targetTransform.localRotation) > localRotationSensitivity;
  169. bool HasScaled => syncScale && Vector3.SqrMagnitude(lastScale - targetTransform.localScale) > localScaleSensitivity * localScaleSensitivity;
  170. // teleport / lag / stuck detection
  171. // - checking distance is not enough since there could be just a tiny fence between us and the goal
  172. // - checking time always works, this way we just teleport if we still didn't reach the goal after too much time has elapsed
  173. bool NeedsTeleport()
  174. {
  175. // calculate time between the two data points
  176. float startTime = start.isValid ? start.timeStamp : Time.time - Time.fixedDeltaTime;
  177. float goalTime = goal.isValid ? goal.timeStamp : Time.time;
  178. float difference = goalTime - startTime;
  179. float timeSinceGoalReceived = Time.time - goalTime;
  180. return timeSinceGoalReceived > difference * 5;
  181. }
  182. // local authority client sends sync message to server for broadcasting
  183. [Command(channel = Channels.Unreliable)]
  184. void CmdClientToServerSync(Vector3 position, uint packedRotation, Vector3 scale)
  185. {
  186. // Ignore messages from client if not in client authority mode
  187. if (!clientAuthority)
  188. return;
  189. // deserialize payload
  190. SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
  191. // server-only mode does no interpolation to save computations, but let's set the position directly
  192. if (isServer && !isClient)
  193. ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
  194. RpcMove(position, packedRotation, scale);
  195. }
  196. [ClientRpc(channel = Channels.Unreliable)]
  197. void RpcMove(Vector3 position, uint packedRotation, Vector3 scale)
  198. {
  199. if (hasAuthority && excludeOwnerUpdate) return;
  200. if (!isServer)
  201. SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
  202. }
  203. // serialization is needed by OnSerialize and by manual sending from authority
  204. void SetGoal(Vector3 position, Quaternion rotation, Vector3 scale)
  205. {
  206. // put it into a data point immediately
  207. DataPoint temp = new DataPoint
  208. {
  209. // deserialize position
  210. localPosition = position,
  211. localRotation = rotation,
  212. localScale = scale,
  213. timeStamp = Time.time
  214. };
  215. // movement speed: based on how far it moved since last time has to be calculated before 'start' is overwritten
  216. temp.movementSpeed = EstimateMovementSpeed(goal, temp, targetTransform, Time.fixedDeltaTime);
  217. // reassign start wisely
  218. // first ever data point? then make something up for previous one so that we can start interpolation without waiting for next.
  219. if (start.timeStamp == 0)
  220. {
  221. start = new DataPoint
  222. {
  223. timeStamp = Time.time - Time.fixedDeltaTime,
  224. // local position/rotation for VR support
  225. localPosition = targetTransform.localPosition,
  226. localRotation = targetTransform.localRotation,
  227. localScale = targetTransform.localScale,
  228. movementSpeed = temp.movementSpeed
  229. };
  230. }
  231. // second or nth data point? then update previous
  232. // but: we start at where ever we are right now, so that it's perfectly smooth and we don't jump anywhere
  233. //
  234. // example if we are at 'x':
  235. //
  236. // A--x->B
  237. //
  238. // and then receive a new point C:
  239. //
  240. // A--x--B
  241. // |
  242. // |
  243. // C
  244. //
  245. // then we don't want to just jump to B and start interpolation:
  246. //
  247. // x
  248. // |
  249. // |
  250. // C
  251. //
  252. // we stay at 'x' and interpolate from there to C:
  253. //
  254. // x..B
  255. // \ .
  256. // \.
  257. // C
  258. //
  259. else
  260. {
  261. float oldDistance = Vector3.Distance(start.localPosition, goal.localPosition);
  262. float newDistance = Vector3.Distance(goal.localPosition, temp.localPosition);
  263. start = goal;
  264. // local position/rotation for VR support
  265. // teleport / lag / obstacle detection: only continue at current position if we aren't too far away
  266. // XC < AB + BC (see comments above)
  267. if (Vector3.Distance(targetTransform.localPosition, start.localPosition) < oldDistance + newDistance)
  268. {
  269. start.localPosition = targetTransform.localPosition;
  270. start.localRotation = targetTransform.localRotation;
  271. start.localScale = targetTransform.localScale;
  272. }
  273. }
  274. // set new destination in any case. new data is best data.
  275. goal = temp;
  276. }
  277. // try to estimate movement speed for a data point based on how far it moved since the previous one
  278. // - if this is the first time ever then we use our best guess:
  279. // - delta based on transform.localPosition
  280. // - elapsed based on send interval hoping that it roughly matches
  281. static float EstimateMovementSpeed(DataPoint from, DataPoint to, Transform transform, float sendInterval)
  282. {
  283. Vector3 delta = to.localPosition - (from.localPosition != transform.localPosition ? from.localPosition : transform.localPosition);
  284. float elapsed = from.isValid ? to.timeStamp - from.timeStamp : sendInterval;
  285. // avoid NaN
  286. return elapsed > 0 ? delta.magnitude / elapsed : 0;
  287. }
  288. // set position carefully depending on the target component
  289. void ApplyPositionRotationScale(Vector3 position, Quaternion rotation, Vector3 scale)
  290. {
  291. // local position/rotation for VR support
  292. if (syncPosition) targetTransform.localPosition = position;
  293. if (syncRotation) targetTransform.localRotation = rotation;
  294. if (syncScale) targetTransform.localScale = scale;
  295. }
  296. // where are we in the timeline between start and goal? [0,1]
  297. Vector3 InterpolatePosition(DataPoint start, DataPoint goal, Vector3 currentPosition)
  298. {
  299. if (!interpolatePosition)
  300. return currentPosition;
  301. if (start.movementSpeed != 0)
  302. {
  303. // Option 1: simply interpolate based on time, but stutter will happen, it's not that smooth.
  304. // This is especially noticeable if the camera automatically follows the player
  305. // - Tell SonarCloud this isn't really commented code but actual comments and to stfu about it
  306. // - float t = CurrentInterpolationFactor();
  307. // - return Vector3.Lerp(start.position, goal.position, t);
  308. // Option 2: always += speed
  309. // speed is 0 if we just started after idle, so always use max for best results
  310. float speed = Mathf.Max(start.movementSpeed, goal.movementSpeed);
  311. return Vector3.MoveTowards(currentPosition, goal.localPosition, speed * Time.deltaTime);
  312. }
  313. return currentPosition;
  314. }
  315. Quaternion InterpolateRotation(DataPoint start, DataPoint goal, Quaternion defaultRotation)
  316. {
  317. if (!interpolateRotation)
  318. return defaultRotation;
  319. if (start.localRotation != goal.localRotation)
  320. {
  321. float t = CurrentInterpolationFactor(start, goal);
  322. return Quaternion.Slerp(start.localRotation, goal.localRotation, t);
  323. }
  324. return defaultRotation;
  325. }
  326. Vector3 InterpolateScale(DataPoint start, DataPoint goal, Vector3 currentScale)
  327. {
  328. if (!interpolateScale)
  329. return currentScale;
  330. if (start.localScale != goal.localScale)
  331. {
  332. float t = CurrentInterpolationFactor(start, goal);
  333. return Vector3.Lerp(start.localScale, goal.localScale, t);
  334. }
  335. return currentScale;
  336. }
  337. static float CurrentInterpolationFactor(DataPoint start, DataPoint goal)
  338. {
  339. if (start.isValid)
  340. {
  341. float difference = goal.timeStamp - start.timeStamp;
  342. // the moment we get 'goal', 'start' is supposed to start, so elapsed time is based on:
  343. float elapsed = Time.time - goal.timeStamp;
  344. // avoid NaN
  345. return difference > 0 ? elapsed / difference : 1;
  346. }
  347. return 1;
  348. }
  349. #region Server Teleport (force move player)
  350. /// <summary>
  351. /// This method will override this GameObject's current Transform.localPosition to the specified Vector3 and update all clients.
  352. /// <para>NOTE: position must be in LOCAL space if the transform has a parent</para>
  353. /// </summary>
  354. /// <param name="localPosition">Where to teleport this GameObject</param>
  355. [Server]
  356. public void ServerTeleport(Vector3 localPosition)
  357. {
  358. Quaternion localRotation = targetTransform.localRotation;
  359. ServerTeleport(localPosition, localRotation);
  360. }
  361. /// <summary>
  362. /// This method will override this GameObject's current Transform.localPosition and Transform.localRotation
  363. /// to the specified Vector3 and Quaternion and update all clients.
  364. /// <para>NOTE: localPosition must be in LOCAL space if the transform has a parent</para>
  365. /// <para>NOTE: localRotation must be in LOCAL space if the transform has a parent</para>
  366. /// </summary>
  367. /// <param name="localPosition">Where to teleport this GameObject</param>
  368. /// <param name="localRotation">Which rotation to set this GameObject</param>
  369. [Server]
  370. public void ServerTeleport(Vector3 localPosition, Quaternion localRotation)
  371. {
  372. // To prevent applying the position updates received from client (if they have ClientAuth) while being teleported.
  373. // clientAuthorityBeforeTeleport defaults to false when not teleporting, if it is true then it means that teleport
  374. // was previously called but not finished therefore we should keep it as true so that 2nd teleport call doesn't clear authority
  375. clientAuthorityBeforeTeleport = clientAuthority || clientAuthorityBeforeTeleport;
  376. clientAuthority = false;
  377. DoTeleport(localPosition, localRotation);
  378. // tell all clients about new values
  379. RpcTeleport(localPosition, Compression.CompressQuaternion(localRotation), clientAuthorityBeforeTeleport);
  380. }
  381. void DoTeleport(Vector3 newLocalPosition, Quaternion newLocalRotation)
  382. {
  383. targetTransform.localPosition = newLocalPosition;
  384. targetTransform.localRotation = newLocalRotation;
  385. // Since we are overriding the position we don't need a goal and start.
  386. // Reset them to null for fresh start
  387. goal = new DataPoint();
  388. start = new DataPoint();
  389. lastPosition = newLocalPosition;
  390. lastRotation = newLocalRotation;
  391. }
  392. [ClientRpc(channel = Channels.Unreliable)]
  393. void RpcTeleport(Vector3 newPosition, uint newPackedRotation, bool isClientAuthority)
  394. {
  395. DoTeleport(newPosition, Compression.DecompressQuaternion(newPackedRotation));
  396. // only send finished if is owner and is ClientAuthority on server
  397. if (hasAuthority && isClientAuthority)
  398. CmdTeleportFinished();
  399. }
  400. /// <summary>
  401. /// This RPC will be invoked on server after client finishes overriding the position.
  402. /// </summary>
  403. /// <param name="initialAuthority"></param>
  404. [Command(channel = Channels.Unreliable)]
  405. void CmdTeleportFinished()
  406. {
  407. if (clientAuthorityBeforeTeleport)
  408. {
  409. clientAuthority = true;
  410. // reset value so doesn't effect future calls, see note in ServerTeleport
  411. clientAuthorityBeforeTeleport = false;
  412. }
  413. else
  414. {
  415. Debug.LogWarning("Client called TeleportFinished when clientAuthority was false on server", this);
  416. }
  417. }
  418. #endregion
  419. #region Debug Gizmos
  420. // draw the data points for easier debugging
  421. void OnDrawGizmos()
  422. {
  423. // draw start and goal points and a line between them
  424. if (start.localPosition != goal.localPosition)
  425. {
  426. DrawDataPointGizmo(start, Color.yellow);
  427. DrawDataPointGizmo(goal, Color.green);
  428. DrawLineBetweenDataPoints(start, goal, Color.cyan);
  429. }
  430. }
  431. static void DrawDataPointGizmo(DataPoint data, Color color)
  432. {
  433. // use a little offset because transform.localPosition might be in the ground in many cases
  434. Vector3 offset = Vector3.up * 0.01f;
  435. // draw position
  436. Gizmos.color = color;
  437. Gizmos.DrawSphere(data.localPosition + offset, 0.5f);
  438. // draw forward and up like unity move tool
  439. Gizmos.color = Color.blue;
  440. Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.forward);
  441. Gizmos.color = Color.green;
  442. Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.up);
  443. }
  444. static void DrawLineBetweenDataPoints(DataPoint data1, DataPoint data2, Color color)
  445. {
  446. Gizmos.color = color;
  447. Gizmos.DrawLine(data1.localPosition, data2.localPosition);
  448. }
  449. #endregion
  450. }
  451. }