diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs index 590686205..9e4d650a1 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictedRigidbody.cs @@ -175,6 +175,7 @@ protected virtual void CreateGhosts() // add the PredictedRigidbodyPhysical component PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent(); physicsGhostRigidbody.target = tf; + // move the rigidbody component & all colliders to the physics GameObject PredictionUtils.MovePhysicsComponents(gameObject, physicsCopy); @@ -423,10 +424,12 @@ void RecordState() // this is performance critical, avoid calling .transform multiple times. tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than accessing .position + .rotation manually Vector3 currentVelocity = predictedRigidbody.velocity; + Vector3 currentAngularVelocity = predictedRigidbody.angularVelocity; // calculate delta to previous state (if any) Vector3 positionDelta = Vector3.zero; Vector3 velocityDelta = Vector3.zero; + Vector3 angularVelocityDelta = Vector3.zero; // Quaternion rotationDelta = Quaternion.identity; // currently unused if (stateHistory.Count > 0) { @@ -434,6 +437,7 @@ void RecordState() positionDelta = currentPosition - last.position; velocityDelta = currentVelocity - last.velocity; // rotationDelta = currentRotation * Quaternion.Inverse(last.rotation); // this is how you calculate a quaternion delta (currently unused, don't do the computation) + angularVelocityDelta = currentAngularVelocity - last.angularVelocity; // debug draw the recorded state // Debug.DrawLine(last.position, currentPosition, Color.red, lineTime); @@ -447,7 +451,9 @@ void RecordState() // rotationDelta, // currently unused currentRotation, velocityDelta, - currentVelocity + currentVelocity, + angularVelocityDelta, + currentAngularVelocity ); // add state to history @@ -464,12 +470,14 @@ protected virtual void OnCorrected() {} protected virtual void OnBeginPrediction() {} // when the Rigidbody moved above threshold and we created a ghost protected virtual void OnEndPrediction() {} // when the Rigidbody came to rest and we destroyed the ghost - void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity) + void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity) { // fix rigidbodies seemingly dancing in place instead of coming to rest. // hard snap to the position below a threshold velocity. // this is fine because the visual object still smoothly interpolates to it. - if (predictedRigidbody.velocity.magnitude <= snapThreshold) + // => consider both velocity and angular velocity (in case of Rigidbodies only rotating with joints etc.) + if (predictedRigidbody.velocity.magnitude <= snapThreshold && + predictedRigidbody.angularVelocity.magnitude <= snapThreshold) { // Debug.Log($"Prediction: snapped {name} into place because velocity {predictedRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}"); @@ -480,6 +488,7 @@ void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 predictedRigidbody.position = position; predictedRigidbody.rotation = rotation; predictedRigidbody.velocity = velocity; + predictedRigidbody.angularVelocity = angularVelocity; // clear history and insert the exact state we just applied. // this makes future corrections more accurate. @@ -491,7 +500,9 @@ void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 // Quaternion.identity, // rotationDelta: currently unused rotation, Vector3.zero, - velocity + velocity, + Vector3.zero, + angularVelocity )); // user callback @@ -519,6 +530,7 @@ void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 // there's only one way to set velocity predictedRigidbody.velocity = velocity; + predictedRigidbody.angularVelocity = angularVelocity; } // process a received server state. @@ -587,7 +599,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) Debug.LogWarning($"Hard correcting client object {name} because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind."); // force apply the state - ApplyState(state.timestamp, state.position, state.rotation, state.velocity); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); return; } @@ -608,7 +620,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) { double ahead = state.timestamp - newest.timestamp; Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter."); - ApplyState(state.timestamp, state.position, state.rotation, state.velocity); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); } return; } @@ -619,7 +631,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) // something went very wrong. sampling should've worked. // hard correct to recover the error. Debug.LogError($"Failed to sample history of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This should never happen because the timestamp is within history."); - ApplyState(state.timestamp, state.position, state.rotation, state.velocity); + ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity); return; } @@ -652,7 +664,7 @@ void OnReceivedState(double timestamp, RigidbodyState state) // 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(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity); + ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity, recomputed.angularVelocity); // user callback OnCorrected(); @@ -681,6 +693,7 @@ public override void OnSerialize(NetworkWriter writer, bool initialState) writer.WriteVector3(position); writer.WriteQuaternion(rotation); writer.WriteVector3(predictedRigidbody.velocity); + writer.WriteVector3(predictedRigidbody.angularVelocity); } // read the server's state, compare with client state & correct if necessary. @@ -703,12 +716,13 @@ public override void OnDeserialize(NetworkReader reader, bool initialState) if (oneFrameAhead) timestamp += serverDeltaTime; // parse state - Vector3 position = reader.ReadVector3(); - Quaternion rotation = reader.ReadQuaternion(); - Vector3 velocity = reader.ReadVector3(); + Vector3 position = reader.ReadVector3(); + Quaternion rotation = reader.ReadQuaternion(); + Vector3 velocity = reader.ReadVector3(); + Vector3 angularVelocity = reader.ReadVector3(); // process received state - OnReceivedState(timestamp, new RigidbodyState(timestamp, Vector3.zero, position, /*Quaternion.identity,*/ rotation, Vector3.zero, velocity)); + OnReceivedState(timestamp, new RigidbodyState(timestamp, Vector3.zero, position, /*Quaternion.identity,*/ rotation, Vector3.zero, velocity, Vector3.zero, angularVelocity)); } protected override void OnValidate() diff --git a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs index 1b721d4f2..f1da27c6b 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs @@ -23,6 +23,7 @@ public static void MoveRigidbody(GameObject source, GameObject destination) rigidbodyCopy.mass = original.mass; rigidbodyCopy.drag = original.drag; rigidbodyCopy.angularDrag = original.angularDrag; + rigidbodyCopy.angularVelocity = original.angularVelocity; rigidbodyCopy.useGravity = original.useGravity; rigidbodyCopy.isKinematic = original.isKinematic; rigidbodyCopy.interpolation = original.interpolation; diff --git a/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs index d890c22a7..c4e755550 100644 --- a/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs +++ b/Assets/Mirror/Components/PredictedRigidbody/RigidbodyState.cs @@ -18,6 +18,9 @@ public struct RigidbodyState : PredictedState public Vector3 velocityDelta { get; set; } // delta to get from last to this velocity public Vector3 velocity { get; set; } + public Vector3 angularVelocityDelta { get; set; } // delta to get from last to this velocity + public Vector3 angularVelocity { get; set; } + public RigidbodyState( double timestamp, Vector3 positionDelta, @@ -25,7 +28,9 @@ public RigidbodyState( // Quaternion rotationDelta, // currently unused Quaternion rotation, Vector3 velocityDelta, - Vector3 velocity) + Vector3 velocity, + Vector3 angularVelocityDelta, + Vector3 angularVelocity) { this.timestamp = timestamp; this.positionDelta = positionDelta; @@ -34,6 +39,8 @@ public RigidbodyState( this.rotation = rotation; this.velocityDelta = velocityDelta; this.velocity = velocity; + this.angularVelocityDelta = angularVelocityDelta; + this.angularVelocity = angularVelocity; } public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t) @@ -42,7 +49,8 @@ public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, flo { position = Vector3.Lerp(a.position, b.position, t), rotation = Quaternion.Slerp(a.rotation, b.rotation, t), - velocity = Vector3.Lerp(a.velocity, b.velocity, t) + velocity = Vector3.Lerp(a.velocity, b.velocity, t), + angularVelocity = Vector3.Lerp(a.angularVelocity, b.angularVelocity, t) }; } } diff --git a/Assets/Mirror/Core/Prediction/Prediction.cs b/Assets/Mirror/Core/Prediction/Prediction.cs index 214f66f31..2ddc42600 100644 --- a/Assets/Mirror/Core/Prediction/Prediction.cs +++ b/Assets/Mirror/Core/Prediction/Prediction.cs @@ -19,6 +19,9 @@ public interface PredictedState Vector3 velocity { get; set; } Vector3 velocityDelta { get; set; } + + Vector3 angularVelocity { get; set; } + Vector3 angularVelocityDelta { get; set; } } public static class Prediction @@ -136,8 +139,9 @@ public static T CorrectHistory( double multiplier = previousDeltaTime != 0 ? correctedDeltaTime / previousDeltaTime : 0; // 0.5 / 2.0 = 0.25 // recalculate 'after.delta' with the multiplier - after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier); - after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier); + after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier); + after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier); + after.angularVelocityDelta = Vector3.Lerp(Vector3.zero, after.angularVelocityDelta, (float)multiplier); // rotation deltas aren't working yet. instead, we apply the corrected rotation to all entries after the correction below. // this at least syncs the rotations and looks quite decent, compared to not syncing! // after.rotationDelta = Quaternion.Slerp(Quaternion.identity, after.rotationDelta, (float)multiplier); @@ -153,8 +157,9 @@ public static T CorrectHistory( T entry = stateHistory.Values[i]; // correct absolute position based on last + delta. - entry.position = last.position + entry.positionDelta; - entry.velocity = last.velocity + entry.velocityDelta; + entry.position = last.position + entry.positionDelta; + entry.velocity = last.velocity + entry.velocityDelta; + entry.angularVelocity = last.angularVelocity + entry.angularVelocityDelta; // rotation deltas aren't working yet. instead, we apply the corrected rotation to all entries after the correction. // this at least syncs the rotations and looks quite decent, compared to not syncing! // entry.rotation = entry.rotationDelta * last.rotation; // quaternions add delta by multiplying in this order diff --git a/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs b/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs index 8bc244616..8906b8e6e 100644 --- a/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs +++ b/Assets/Mirror/Tests/Editor/Prediction/PredictionTests.cs @@ -19,7 +19,10 @@ struct TestState : PredictedState public Vector3 velocity { get; set; } public Vector3 velocityDelta { get; set; } - public TestState(double timestamp, Vector3 position, Vector3 positionDelta, Vector3 velocity, Vector3 velocityDelta) + public Vector3 angularVelocity { get; set; } + public Vector3 angularVelocityDelta { get; set; } + + public TestState(double timestamp, Vector3 position, Vector3 positionDelta, Vector3 velocity, Vector3 velocityDelta, Vector3 angularVelocity, Vector3 angularVelocityDelta) { this.timestamp = timestamp; this.position = position; @@ -28,6 +31,8 @@ public TestState(double timestamp, Vector3 position, Vector3 positionDelta, Vect // this.rotationDelta = Quaternion.identity; this.velocity = velocity; this.velocityDelta = velocityDelta; + this.angularVelocity = angularVelocity; + this.angularVelocityDelta = angularVelocityDelta; } } @@ -124,21 +129,21 @@ public void CorrectHistory() SortedList history = new SortedList(); // (0,0,0) with delta (0,0,0) from previous: - history.Add(0, new TestState(0, new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0))); + history.Add(0, new TestState(0, new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(0, 0, 0))); // (1,0,0) with delta (1,0,0) from previous: - history.Add(1, new TestState(1, new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0))); + history.Add(1, new TestState(1, new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0))); // (2,0,0) with delta (1,0,0) from previous: - history.Add(2, new TestState(2, new Vector3(2, 0, 0), new Vector3(1, 0, 0), new Vector3(2, 0, 0), new Vector3(1, 0, 0))); + history.Add(2, new TestState(2, new Vector3(2, 0, 0), new Vector3(1, 0, 0), new Vector3(2, 0, 0), new Vector3(1, 0, 0), new Vector3(2, 0, 0), new Vector3(1, 0, 0))); // (3,0,0) with delta (1,0,0) from previous: - history.Add(3, new TestState(3, new Vector3(3, 0, 0), new Vector3(1, 0, 0), new Vector3(3, 0, 0), new Vector3(1, 0, 0))); + history.Add(3, new TestState(3, new Vector3(3, 0, 0), new Vector3(1, 0, 0), new Vector3(3, 0, 0), new Vector3(1, 0, 0), new Vector3(3, 0, 0), new Vector3(1, 0, 0))); // client receives a correction from server between t=1 and t=2. // exactly t=1.5 where position should be 1.5, server says it's +0.1 = 1.6 // deltas are zero because that's how PredictedBody.Serialize sends them, alwasy at zero. - TestState correction = new TestState(1.5, new Vector3(1.6f, 0, 0), Vector3.zero, new Vector3(1.6f, 0, 0), Vector3.zero); + TestState correction = new TestState(1.5, new Vector3(1.6f, 0, 0), Vector3.zero, new Vector3(1.6f, 0, 0), Vector3.zero, new Vector3(1.6f, 0, 0), Vector3.zero); // Sample() will find that the value before correction is at t=1 and after at t=2. Assert.That(Prediction.Sample(history, correction.timestamp, out TestState before, out TestState after, out int afterIndex, out double t), Is.True); @@ -164,6 +169,8 @@ public void CorrectHistory() Assert.That(history.Values[0].positionDelta.x, Is.EqualTo(0)); Assert.That(history.Values[0].velocity.x, Is.EqualTo(0)); Assert.That(history.Values[0].velocityDelta.x, Is.EqualTo(0)); + Assert.That(history.Values[0].angularVelocity.x, Is.EqualTo(0)); + Assert.That(history.Values[0].angularVelocityDelta.x, Is.EqualTo(0)); // second entry at t=1 should be unchanged, since we corrected after that one. Assert.That(history.Keys[1], Is.EqualTo(1)); @@ -171,6 +178,8 @@ public void CorrectHistory() Assert.That(history.Values[1].positionDelta.x, Is.EqualTo(1)); Assert.That(history.Values[1].velocity.x, Is.EqualTo(1)); Assert.That(history.Values[1].velocityDelta.x, Is.EqualTo(1)); + Assert.That(history.Values[1].angularVelocity.x, Is.EqualTo(1)); + Assert.That(history.Values[1].angularVelocityDelta.x, Is.EqualTo(1)); // third entry at t=1.5 should be the received state. // absolute values should be the correction, without any deltas since @@ -180,6 +189,8 @@ public void CorrectHistory() Assert.That(history.Values[2].positionDelta.x, Is.EqualTo(0)); Assert.That(history.Values[2].velocity.x, Is.EqualTo(1.6f).Within(0.001f)); Assert.That(history.Values[2].velocityDelta.x, Is.EqualTo(0)); + Assert.That(history.Values[2].angularVelocity.x, Is.EqualTo(1.6f).Within(0.001f)); + Assert.That(history.Values[2].angularVelocityDelta.x, Is.EqualTo(0)); // fourth entry at t=2: // delta was from t=1.0 @ 1 to t=2.0 @ 2 = 1.0 @@ -192,6 +203,8 @@ public void CorrectHistory() Assert.That(history.Values[3].positionDelta.x, Is.EqualTo(0.5).Within(0.001f)); Assert.That(history.Values[3].velocity.x, Is.EqualTo(2.1).Within(0.001f)); Assert.That(history.Values[3].velocityDelta.x, Is.EqualTo(0.5).Within(0.001f)); + Assert.That(history.Values[3].angularVelocity.x, Is.EqualTo(2.1).Within(0.001f)); + Assert.That(history.Values[3].angularVelocityDelta.x, Is.EqualTo(0.5)); // fifth entry at t=3: // client moved by a delta of 1 here, and that remains unchanged. @@ -202,6 +215,8 @@ public void CorrectHistory() Assert.That(history.Values[4].positionDelta.x, Is.EqualTo(1.0).Within(0.001f)); Assert.That(history.Values[4].velocity.x, Is.EqualTo(3.1).Within(0.001f)); Assert.That(history.Values[4].velocityDelta.x, Is.EqualTo(1.0).Within(0.001f)); + Assert.That(history.Values[4].angularVelocity.x, Is.EqualTo(3.1).Within(0.001f)); + Assert.That(history.Values[4].angularVelocityDelta.x, Is.EqualTo(1.0).Within(0.001f)); } } }