NetworkTransformBase.cs 22 KB

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