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
{
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();

View File

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

View File

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

View File

@ -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<ForecastRigidbody>().predictedRigidbody;
ForecastRigidbody forecast = whiteBall.GetComponent<ForecastRigidbody>();
// 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<Rigidbody>();
rb.AddForce(force, ForceMode.Impulse);
}
}
}