diff --git a/Assets/Mirror/Components/ForecastRigidbody/ForecastRigidbody.cs b/Assets/Mirror/Components/ForecastRigidbody/ForecastRigidbody.cs index e561efc77..950bf58e7 100644 --- a/Assets/Mirror/Components/ForecastRigidbody/ForecastRigidbody.cs +++ b/Assets/Mirror/Components/ForecastRigidbody/ForecastRigidbody.cs @@ -10,6 +10,13 @@ namespace Mirror { + enum ForecastState + { + PREDICT, // 100% client side physics prediction + BLEND, // blending client prediction with server state + FOLLOW, // 100% server sided physics, client only follows .transform + } + [RequireComponent(typeof(Rigidbody))] public class ForecastRigidbody : NetworkBehaviour { @@ -25,6 +32,7 @@ public class ForecastRigidbody : NetworkBehaviour [Header("Blending")] [Range(0.01f, 1)] public float blendPerSync = 0.1f; + ForecastState state = ForecastState.FOLLOW; // follow until the player interacts // motion smoothing happen on-demand, because it requires moving physics components to another GameObject. // this only starts at a given velocity and ends when stopped moving. @@ -81,6 +89,10 @@ protected virtual void Awake() if (predictedRigidbody == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component."); predictedRigidbodyTransform = predictedRigidbody.transform; + // set Rigidbody as kinematic by default. + // it's only dynamic while predicting. + predictedRigidbody.isKinematic = true; + // in fast mode, we need to force enable Rigidbody.interpolation. // otherwise there's not going to be any smoothing whatsoever. predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate; @@ -91,6 +103,17 @@ protected virtual void Awake() positionCorrectionThresholdSqr = positionCorrectionThreshold * positionCorrectionThreshold; } + // client prediction API + public void AddPredictedForce(Vector3 force, ForceMode mode) + { + // explicitly start predicting physics + predictedRigidbody.isKinematic = false; + predictedRigidbody.AddForce(force, mode); + state = ForecastState.PREDICT; + OnBeginPrediction(); + Debug.Log($"{name} PREDICTING"); + } + void UpdateServer() { // bandwidth optimization while idle. @@ -127,37 +150,26 @@ protected virtual bool IsMoving() => // when using Fast mode, we don't create any ghosts. // but we still want to check IsMoving() in order to support the same // user callbacks. - bool lastMoving = false; - void UpdateState() + void UpdateClient() { - // perf: enough to check ghosts every few frames. - // PredictionBenchmark: only checking every 4th frame: 770 => 800 FPS - if (Time.frameCount % checkGhostsEveryNthFrame != 0) return; - - bool moving = IsMoving(); - - // started moving? - if (moving && !lastMoving) + if (state == ForecastState.PREDICT) { - OnBeginPrediction(); - lastMoving = true; + } - // stopped moving? - else if (!moving && lastMoving) + else if (state == ForecastState.BLEND) { - // ensure a minimum time since starting to move, to avoid on/off/on effects. - if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance) - { - OnEndPrediction(); - lastMoving = false; - } + + } + else if (state == ForecastState.FOLLOW) + { + } } void Update() { if (isServer) UpdateServer(); - if (isClientOnly) UpdateState(); + if (isClientOnly) UpdateClient(); } void FixedUpdate() @@ -468,8 +480,8 @@ void OnReceivedState(double timestamp, RigidbodyState state)//, bool sleeping) // blend the final correction towards current server state over time. // this is the idea of ForecastRigidbody. // TODO once we are at server state, let snapshot interpolation take over. - RigidbodyState blended = RigidbodyState.Interpolate(recomputed, state, blendPerSync); - Debug.DrawLine(recomputed.position, blended.position, Color.green, 10.0f); + // RigidbodyState blended = RigidbodyState.Interpolate(recomputed, state, blendPerSync); + // Debug.DrawLine(recomputed.position, blended.position, Color.green, 10.0f); // log, draw & apply the final position. // always do this here, not when iterating above, in case we aren't iterating. @@ -477,7 +489,11 @@ void OnReceivedState(double timestamp, RigidbodyState state)//, bool sleeping) // int correctedAmount = stateHistory.Count - afterIndex; // Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}"); //Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime); - ApplyState(blended.timestamp, blended.position, blended.rotation, blended.velocity, blended.angularVelocity); + ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity, recomputed.angularVelocity); + + // insert the blended state into the history. + // this makes it permanent, instead of blending every time but rarely recording. + RecordState(); // user callback OnCorrected(); diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedPredicted.prefab b/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedPredicted.prefab index a8e749b80..8de15656e 100644 --- a/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedPredicted.prefab +++ b/Assets/Mirror/Examples/BilliardsPredicted/Ball/RedPredicted.prefab @@ -165,6 +165,7 @@ MonoBehaviour: syncMode: 0 syncInterval: 0 predictedRigidbody: {fileID: -177125271246800426} + blendPerSync: 0.1 motionSmoothingVelocityThreshold: 0.1 motionSmoothingAngularVelocityThreshold: 5 motionSmoothingTimeTolerance: 0.5 @@ -177,7 +178,5 @@ MonoBehaviour: oneFrameAhead: 1 snapThreshold: 2 checkGhostsEveryNthFrame: 4 - positionInterpolationSpeed: 15 - rotationInterpolationSpeed: 10 teleportDistanceMultiplier: 10 reduceSendsWhileIdle: 1 diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhitePredicted.prefab b/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhitePredicted.prefab index 2ec9a5b89..eb34fc74e 100644 --- a/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhitePredicted.prefab +++ b/Assets/Mirror/Examples/BilliardsPredicted/Ball/WhitePredicted.prefab @@ -302,6 +302,7 @@ MonoBehaviour: syncMode: 0 syncInterval: 0 predictedRigidbody: {fileID: 1848203816128897140} + blendPerSync: 0.1 motionSmoothingVelocityThreshold: 0.1 motionSmoothingAngularVelocityThreshold: 5 motionSmoothingTimeTolerance: 0.5 @@ -314,7 +315,5 @@ MonoBehaviour: oneFrameAhead: 1 snapThreshold: 2 checkGhostsEveryNthFrame: 4 - positionInterpolationSpeed: 15 - rotationInterpolationSpeed: 10 teleportDistanceMultiplier: 10 reduceSendsWhileIdle: 1 diff --git a/Assets/Mirror/Examples/BilliardsPredicted/Player/PlayerPredicted.cs b/Assets/Mirror/Examples/BilliardsPredicted/Player/PlayerPredicted.cs index 0263917cd..b209560e9 100644 --- a/Assets/Mirror/Examples/BilliardsPredicted/Player/PlayerPredicted.cs +++ b/Assets/Mirror/Examples/BilliardsPredicted/Player/PlayerPredicted.cs @@ -36,7 +36,7 @@ void Awake() // apply force to white ball. // common function to ensure we apply it the same way on server & client! - void ApplyForceToWhite(Vector3 force) + void ClientApplyForceToWhite(Vector3 force) { // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Rigidbody.AddForce.html // this is buffered until the next FixedUpdate. @@ -44,26 +44,25 @@ void ApplyForceToWhite(Vector3 force) // get the white ball's Rigidbody. // prediction sometimes moves this out of the object for a while, // so we need to grab it this way: - Rigidbody rb = whiteBall.GetComponent().predictedRigidbody; + ForecastRigidbody forecast = whiteBall.GetComponent(); // AddForce has different force modes, see this excellent diagram: // https://www.reddit.com/r/Unity3D/comments/psukm1/know_the_difference_between_forcemodes_a_little/ // for prediction it's extremely important(!) to apply the correct mode: // 'Force' makes server & client drift significantly here // 'Impulse' is correct usage with significantly less drift - rb.AddForce(force, ForceMode.Impulse); + forecast.AddPredictedForce(force, ForceMode.Impulse); } // called when the local player dragged the white ball. // we reuse the white ball's OnMouseDrag and forward the event to here. public void OnDraggedBall(Vector3 force) { - // apply force locally immediately - ApplyForceToWhite(force); + // apply force on client (not in host mode) + if (!isServer) ClientApplyForceToWhite(force); // apply on server as well. - // not necessary in host mode, otherwise we would apply it twice. - if (!isServer) CmdApplyForce(force); + CmdApplyForce(force); } // while prediction is applied on clients immediately, @@ -85,7 +84,8 @@ void CmdApplyForce(Vector3 force) } // apply force - ApplyForceToWhite(force); + Rigidbody rb = whiteBall.GetComponent(); + rb.AddForce(force, ForceMode.Impulse); } } }