mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 19:10:32 +00:00
Prediction: sync angularVelocity too
This commit is contained in:
parent
6cb4017deb
commit
05787f3853
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user