Prediction: sync angularVelocity too

This commit is contained in:
mischa 2024-02-14 10:34:37 +01:00 committed by MrGadget
parent 6cb4017deb
commit 05787f3853
5 changed files with 67 additions and 24 deletions

View File

@ -175,6 +175,7 @@ protected virtual void CreateGhosts()
// add the PredictedRigidbodyPhysical component
PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent<PredictedRigidbodyPhysicsGhost>();
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.
@ -706,9 +719,10 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
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()

View File

@ -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;

View File

@ -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)
};
}
}

View File

@ -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
@ -138,6 +141,7 @@ public static T CorrectHistory<T>(
// 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.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);
@ -155,6 +159,7 @@ public static T CorrectHistory<T>(
// correct absolute position based on last + delta.
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

View File

@ -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<double, TestState> history = new SortedList<double, TestState>();
// (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));
}
}
}