PlayerPredicted.cs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. namespace Mirror.Examples.BilliardsPredicted
  5. {
  6. // example input for this exact demo.
  7. // not general purpose yet.
  8. public struct PlayerInput
  9. {
  10. public double timestamp;
  11. public Vector3 force;
  12. public PlayerInput(double timestamp, Vector3 force)
  13. {
  14. this.timestamp = timestamp;
  15. this.force = force;
  16. }
  17. }
  18. public class PlayerPredicted : NetworkBehaviour
  19. {
  20. // white ball component
  21. WhiteBallPredicted whiteBall;
  22. // keep a history of inputs with timestamp
  23. public int inputHistorySize = 64;
  24. readonly SortedList<double, PlayerInput> inputs = new SortedList<double, PlayerInput>();
  25. void Awake()
  26. {
  27. // find the white ball once
  28. #if UNITY_2021_3_OR_NEWER
  29. whiteBall = FindAnyObjectByType<WhiteBallPredicted>();
  30. #else
  31. // Deprecated in Unity 2023.1
  32. whiteBall = FindObjectOfType<WhiteBallPredicted>();
  33. #endif
  34. }
  35. // apply force to white ball.
  36. // common function to ensure we apply it the same way on server & client!
  37. void ApplyForceToWhite(Vector3 force)
  38. {
  39. // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Rigidbody.AddForce.html
  40. // this is buffered until the next FixedUpdate.
  41. // AddForce has different force modes, see this excellent diagram:
  42. // https://www.reddit.com/r/Unity3D/comments/psukm1/know_the_difference_between_forcemodes_a_little/
  43. // for prediction it's extremely important(!) to apply the correct mode:
  44. // 'Force' makes server & client drift significantly here
  45. // 'Impulse' is correct usage with significantly less drift
  46. whiteBall.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
  47. }
  48. // called when the local player dragged the white ball.
  49. // we reuse the white ball's OnMouseDrag and forward the event to here.
  50. public void OnDraggedBall(Vector3 force)
  51. {
  52. // record the input for reconciliation if needed
  53. if (inputs.Count >= inputHistorySize) inputs.RemoveAt(0);
  54. inputs.Add(NetworkTime.time, new PlayerInput(NetworkTime.time, force));
  55. Debug.Log($"Inputs.Count={inputs.Count}");
  56. // apply force locally immediately
  57. ApplyForceToWhite(force);
  58. // apply on server as well.
  59. // not necessary in host mode, otherwise we would apply it twice.
  60. if (!isServer) CmdApplyForce(force, NetworkTime.predictedTime);
  61. }
  62. // while prediction is applied on clients immediately,
  63. // we still want to validate every input on the server and reject it if necessary.
  64. // this way we can latency free yet cheat safe movement.
  65. // this should include a certain tolerance so players aren't hard corrected
  66. // for their local movement all the time.
  67. // TODO this should be on some kind of base class for reuse, but perhaps independent of parameters?
  68. bool IsValidMove(Vector3 force) => true;
  69. // TODO send over unreliable with ack, notify, etc. later
  70. [Command]
  71. void CmdApplyForce(Vector3 force, double predictedTime)
  72. {
  73. if (!IsValidMove(force))
  74. {
  75. Debug.Log($"Server rejected move: {force}");
  76. return;
  77. }
  78. // client is on a predicted timeline.
  79. // double check the prediction - it should arrive at server time.
  80. //
  81. // there are multiple reasons why this may be off:
  82. // - time prediction may still be adjusting itself
  83. // - time prediction may have an issue
  84. // - server or client may be lagging or under heavy load temporarily
  85. // - unreliable vs. reliable channel latencies are signifcantly different
  86. // for example, if latency simulation is only applied to one channel!
  87. double delta = NetworkTime.time - predictedTime;
  88. if (delta < -0.010)
  89. {
  90. Debug.LogWarning($"Cmd predictedTime was {(delta*1000):F0}ms behind the server time. This could occasionally happen if the time prediction is off. If it happens consistently, check that unreliable NetworkTime and reliable [Command]s have the same latency. If they are off, this will cause heavy jitter.");
  91. }
  92. else if (delta > 0.010)
  93. {
  94. // TODO consider buffering inputs which are ahead, apply next frame
  95. Debug.LogWarning($"Cmd predictedTime was {(delta*1000):F0}ms ahead of the server time. This could occasionally happen if the time prediction is off. If it happens consistently, check that unreliable NetworkTime and reliable [Command]s have the same latency. If they are off, this will cause heavy jitter. If reliable & unreliable latency are similar and this still happens a lot, consider buffering inputs for the next frame.");
  96. }
  97. else
  98. {
  99. Debug.Log($"Cmd predictedTime was {(delta*1000):F0}ms close to the server time.");
  100. }
  101. // apply force
  102. ApplyForceToWhite(force);
  103. }
  104. }
  105. }