This commit is contained in:
mischa 2024-04-02 17:12:16 +08:00
parent 2c04d50a3d
commit bf9fe3880d
4 changed files with 50 additions and 36 deletions

View File

@ -10,6 +10,13 @@
namespace Mirror 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))] [RequireComponent(typeof(Rigidbody))]
public class ForecastRigidbody : NetworkBehaviour public class ForecastRigidbody : NetworkBehaviour
{ {
@ -25,6 +32,7 @@ public class ForecastRigidbody : NetworkBehaviour
[Header("Blending")] [Header("Blending")]
[Range(0.01f, 1)] public float blendPerSync = 0.1f; [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. // 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. // 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."); if (predictedRigidbody == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component.");
predictedRigidbodyTransform = predictedRigidbody.transform; 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. // in fast mode, we need to force enable Rigidbody.interpolation.
// otherwise there's not going to be any smoothing whatsoever. // otherwise there's not going to be any smoothing whatsoever.
predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate; predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate;
@ -91,6 +103,17 @@ protected virtual void Awake()
positionCorrectionThresholdSqr = positionCorrectionThreshold * positionCorrectionThreshold; 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() void UpdateServer()
{ {
// bandwidth optimization while idle. // bandwidth optimization while idle.
@ -127,37 +150,26 @@ protected virtual bool IsMoving() =>
// when using Fast mode, we don't create any ghosts. // when using Fast mode, we don't create any ghosts.
// but we still want to check IsMoving() in order to support the same // but we still want to check IsMoving() in order to support the same
// user callbacks. // user callbacks.
bool lastMoving = false; void UpdateClient()
void UpdateState()
{ {
// perf: enough to check ghosts every few frames. if (state == ForecastState.PREDICT)
// PredictionBenchmark: only checking every 4th frame: 770 => 800 FPS
if (Time.frameCount % checkGhostsEveryNthFrame != 0) return;
bool moving = IsMoving();
// started moving?
if (moving && !lastMoving)
{ {
OnBeginPrediction();
lastMoving = true;
} }
// stopped moving? else if (state == ForecastState.BLEND)
else if (!moving && lastMoving)
{ {
// ensure a minimum time since starting to move, to avoid on/off/on effects.
if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance) }
{ else if (state == ForecastState.FOLLOW)
OnEndPrediction(); {
lastMoving = false;
}
} }
} }
void Update() void Update()
{ {
if (isServer) UpdateServer(); if (isServer) UpdateServer();
if (isClientOnly) UpdateState(); if (isClientOnly) UpdateClient();
} }
void FixedUpdate() void FixedUpdate()
@ -468,8 +480,8 @@ void OnReceivedState(double timestamp, RigidbodyState state)//, bool sleeping)
// blend the final correction towards current server state over time. // blend the final correction towards current server state over time.
// this is the idea of ForecastRigidbody. // this is the idea of ForecastRigidbody.
// TODO once we are at server state, let snapshot interpolation take over. // TODO once we are at server state, let snapshot interpolation take over.
RigidbodyState blended = RigidbodyState.Interpolate(recomputed, state, blendPerSync); // RigidbodyState blended = RigidbodyState.Interpolate(recomputed, state, blendPerSync);
Debug.DrawLine(recomputed.position, blended.position, Color.green, 10.0f); // Debug.DrawLine(recomputed.position, blended.position, Color.green, 10.0f);
// log, draw & apply the final position. // log, draw & apply the final position.
// always do this here, not when iterating above, in case we aren't iterating. // 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; // int correctedAmount = stateHistory.Count - afterIndex;
// Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}"); // 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); //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 // user callback
OnCorrected(); OnCorrected();

View File

@ -165,6 +165,7 @@ MonoBehaviour:
syncMode: 0 syncMode: 0
syncInterval: 0 syncInterval: 0
predictedRigidbody: {fileID: -177125271246800426} predictedRigidbody: {fileID: -177125271246800426}
blendPerSync: 0.1
motionSmoothingVelocityThreshold: 0.1 motionSmoothingVelocityThreshold: 0.1
motionSmoothingAngularVelocityThreshold: 5 motionSmoothingAngularVelocityThreshold: 5
motionSmoothingTimeTolerance: 0.5 motionSmoothingTimeTolerance: 0.5
@ -177,7 +178,5 @@ MonoBehaviour:
oneFrameAhead: 1 oneFrameAhead: 1
snapThreshold: 2 snapThreshold: 2
checkGhostsEveryNthFrame: 4 checkGhostsEveryNthFrame: 4
positionInterpolationSpeed: 15
rotationInterpolationSpeed: 10
teleportDistanceMultiplier: 10 teleportDistanceMultiplier: 10
reduceSendsWhileIdle: 1 reduceSendsWhileIdle: 1

View File

@ -302,6 +302,7 @@ MonoBehaviour:
syncMode: 0 syncMode: 0
syncInterval: 0 syncInterval: 0
predictedRigidbody: {fileID: 1848203816128897140} predictedRigidbody: {fileID: 1848203816128897140}
blendPerSync: 0.1
motionSmoothingVelocityThreshold: 0.1 motionSmoothingVelocityThreshold: 0.1
motionSmoothingAngularVelocityThreshold: 5 motionSmoothingAngularVelocityThreshold: 5
motionSmoothingTimeTolerance: 0.5 motionSmoothingTimeTolerance: 0.5
@ -314,7 +315,5 @@ MonoBehaviour:
oneFrameAhead: 1 oneFrameAhead: 1
snapThreshold: 2 snapThreshold: 2
checkGhostsEveryNthFrame: 4 checkGhostsEveryNthFrame: 4
positionInterpolationSpeed: 15
rotationInterpolationSpeed: 10
teleportDistanceMultiplier: 10 teleportDistanceMultiplier: 10
reduceSendsWhileIdle: 1 reduceSendsWhileIdle: 1

View File

@ -36,7 +36,7 @@ void Awake()
// apply force to white ball. // apply force to white ball.
// common function to ensure we apply it the same way on server & client! // 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 // https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Rigidbody.AddForce.html
// this is buffered until the next FixedUpdate. // this is buffered until the next FixedUpdate.
@ -44,26 +44,25 @@ void ApplyForceToWhite(Vector3 force)
// get the white ball's Rigidbody. // get the white ball's Rigidbody.
// prediction sometimes moves this out of the object for a while, // prediction sometimes moves this out of the object for a while,
// so we need to grab it this way: // so we need to grab it this way:
Rigidbody rb = whiteBall.GetComponent<ForecastRigidbody>().predictedRigidbody; ForecastRigidbody forecast = whiteBall.GetComponent<ForecastRigidbody>();
// AddForce has different force modes, see this excellent diagram: // AddForce has different force modes, see this excellent diagram:
// https://www.reddit.com/r/Unity3D/comments/psukm1/know_the_difference_between_forcemodes_a_little/ // 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: // for prediction it's extremely important(!) to apply the correct mode:
// 'Force' makes server & client drift significantly here // 'Force' makes server & client drift significantly here
// 'Impulse' is correct usage with significantly less drift // '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. // called when the local player dragged the white ball.
// we reuse the white ball's OnMouseDrag and forward the event to here. // we reuse the white ball's OnMouseDrag and forward the event to here.
public void OnDraggedBall(Vector3 force) public void OnDraggedBall(Vector3 force)
{ {
// apply force locally immediately // apply force on client (not in host mode)
ApplyForceToWhite(force); if (!isServer) ClientApplyForceToWhite(force);
// apply on server as well. // apply on server as well.
// not necessary in host mode, otherwise we would apply it twice. CmdApplyForce(force);
if (!isServer) CmdApplyForce(force);
} }
// while prediction is applied on clients immediately, // while prediction is applied on clients immediately,
@ -85,7 +84,8 @@ void CmdApplyForce(Vector3 force)
} }
// apply force // apply force
ApplyForceToWhite(force); Rigidbody rb = whiteBall.GetComponent<Rigidbody>();
rb.AddForce(force, ForceMode.Impulse);
} }
} }
} }