perf(PredictedRigidbody): introduce FAST mode where physics & renderers aren't separated

This commit is contained in:
mischa 2024-03-28 14:09:08 +08:00
parent b815c87a6d
commit 970b7e774f

View File

@ -15,6 +15,8 @@
namespace Mirror namespace Mirror
{ {
public enum PredictionMode { Smooth, Fast }
// [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it. // [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it.
public class PredictedRigidbody : NetworkBehaviour public class PredictedRigidbody : NetworkBehaviour
{ {
@ -32,6 +34,8 @@ public class PredictedRigidbody : NetworkBehaviour
// this only starts at a given velocity and ends when stopped moving. // this only starts at a given velocity and ends when stopped moving.
// to avoid constant on/off/on effects, it also stays on for a minimum time. // to avoid constant on/off/on effects, it also stays on for a minimum time.
[Header("Motion Smoothing")] [Header("Motion Smoothing")]
[Tooltip("Prediction supports two different modes: Smooth and Fast:\n\nSmooth: Physics are separated from the GameObject & applied in the background. Rendering smoothly follows the physics for perfectly smooth interpolation results. Much softer, can be even too soft where sharp collisions won't look as sharp (i.e. Billiard balls avoid the wall before even hitting it).\n\nFast: Physics remain on the GameObject and corrections are applied hard. Much faster since we don't need to update a separate GameObject, a bit harsher, more precise.")]
public PredictionMode mode = PredictionMode.Smooth;
[Tooltip("Smoothing via Ghost-following only happens on demand, while moving with a minimum velocity.")] [Tooltip("Smoothing via Ghost-following only happens on demand, while moving with a minimum velocity.")]
public float motionSmoothingVelocityThreshold = 0.1f; public float motionSmoothingVelocityThreshold = 0.1f;
float motionSmoothingVelocityThresholdSqr; // ² cached in Awake float motionSmoothingVelocityThresholdSqr; // ² cached in Awake
@ -118,6 +122,13 @@ 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;
// in fast mode, we need to force enable Rigidbody.interpolation.
// otherwise there's not going to be any smoothing whatsoever.
if (mode == PredictionMode.Fast)
{
predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate;
}
// cache some threshold to avoid calculating them in LateUpdate // cache some threshold to avoid calculating them in LateUpdate
float colliderSize = GetComponentInChildren<Collider>().bounds.size.magnitude; float colliderSize = GetComponentInChildren<Collider>().bounds.size.magnitude;
smoothFollowThreshold = colliderSize * teleportDistanceMultiplier; smoothFollowThreshold = colliderSize * teleportDistanceMultiplier;
@ -461,13 +472,13 @@ void UpdateGhosting()
void Update() void Update()
{ {
if (isServer) UpdateServer(); if (isServer) UpdateServer();
if (isClientOnly) UpdateGhosting(); if (isClientOnly && mode == PredictionMode.Smooth) UpdateGhosting();
} }
void LateUpdate() void LateUpdate()
{ {
// only follow on client-only, not in server or host mode // only follow on client-only, not in server or host mode
if (isClientOnly && physicsCopy) SmoothFollowPhysicsCopy(); if (isClientOnly && mode == PredictionMode.Smooth && physicsCopy) SmoothFollowPhysicsCopy();
} }
void FixedUpdate() void FixedUpdate()
@ -633,22 +644,24 @@ void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3
// call it before applying pos/rot/vel in case we need to set kinematic etc. // call it before applying pos/rot/vel in case we need to set kinematic etc.
OnBeforeApplyState(); OnBeforeApplyState();
// Rigidbody .position teleports, while .MovePosition interpolates. // apply the state to the Rigidbody
// we always want to teleport to corrections. if (mode == PredictionMode.Smooth)
// otherwise if there A is moving to the right and is blocked by B, {
// it would never get there causing objects at rest to jiggle around. // Smooth mode separates Physics from Renderering.
// if (correctionMode == CorrectionMode.Move) // Rendering smoothly follows Physics in SmoothFollowPhysicsCopy().
// { // this allows us to be able to hard teleport to the correction.
// predictedRigidbody.MovePosition(position); // which gives most accurate results since the Rigidbody can't
// predictedRigidbody.MoveRotation(rotation); // be stopped by another object when trying to correct.
// } predictedRigidbody.position = position;
// else if (correctionMode == CorrectionMode.Set) predictedRigidbody.rotation = rotation;
// { }
// predictedRigidbody.position = position; else if (mode == PredictionMode.Fast)
// predictedRigidbody.rotation = rotation; {
// } // Fast mode doesn't separate physics from rendering.
predictedRigidbody.position = position; // The only smoothing we get is from Rigidbody.MovePosition.
predictedRigidbody.rotation = rotation; predictedRigidbody.MovePosition(position);
predictedRigidbody.MoveRotation(rotation);
}
// there's only one way to set velocity. // there's only one way to set velocity.
// (projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error) // (projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error)