RizzeBattleRoyale/Assets/TPSBR/Scripts/Gameplay/KCC/BREnvironmentProcessor.cs
2025-09-24 11:24:38 +05:00

377 lines
15 KiB
C#

namespace TPSBR
{
using UnityEngine;
using Fusion.Addons.KCC;
// WARNING!
// This processor is a special variant of EnvironmentProcessor and has optimizations which would normally be considered as KCC anti-pattern,
// but these optimizations are required to shrink down performance overhead to minimum.
// 1. All stages (ISetDynamicVelocity, ISetKinematicDirection, ISetKinematicTangent, ISetKinematicSpeed, ISetKinematicVelocity) were collapsed in one stage (IPrepareData).
// 2. Step-Up and Ground-Snap processors are executed directly from this processor from IAfterMoveStep stage.
public sealed class BREnvironmentProcessor : KCCProcessor, IPrepareData, IAfterMoveStep
{
// CONSTANTS
public static readonly int DefaultPriority = EnvironmentProcessor.DefaultPriority;
// PUBLIC MEMBERS
[Header("General")]
[Tooltip("Maximum allowed speed the KCC can move with player input.")]
public float KinematicSpeed = 8.0f;
[Tooltip("Custom jump multiplier.")]
public float JumpMultiplier = 1.0f;
[Tooltip("Custom gravity. Physics.gravity is used if default.")]
public Vector3 Gravity = default;
[Tooltip("Relative environment priority. Default environment processor priority is 1000.")]
public int RelativePriority = 0;
[Header("Ground")]
[Tooltip("Maximum angle of walkable ground.")]
public float MaxGroundAngle = 60.0f;
[Tooltip("Dynamic velocity is decelerated by actual dynamic speed multiplied by this. The faster KCC moves, the more deceleration is applied.")]
public float DynamicGroundFriction = 20.0f;
[Tooltip("Kinematic velocity is accelerated by calculated kinematic speed multiplied by this.")]
public float KinematicGroundAcceleration = 50.0f;
[Tooltip("Kinematic velocity is decelerated by actual kinematic speed multiplied by this. The faster KCC moves, the more deceleration is applied.")]
public float KinematicGroundFriction = 35.0f;
[Header("Air")]
[Tooltip("Dynamic velocity is decelerated by actual dynamic speed multiplied by this. The faster KCC moves, the more deceleration is applied.")]
public float DynamicAirFriction = 2.0f;
[Tooltip("Kinematic velocity is accelerated by calculated kinematic speed multiplied by this.")]
public float KinematicAirAcceleration = 5.0f;
[Tooltip("Kinematic velocity is decelerated by actual kinematic speed multiplied by this. The faster KCC moves, the more deceleration is applied.")]
public float KinematicAirFriction = 2.0f;
// PRIVATE MEMBERS
[Header("BR200")]
[SerializeField]
private float _speedMultiplier = 1.0f;
[SerializeField]
private float _upGravityMultiplier = 1.0f;
[SerializeField]
private float _downGravityMultiplier = 1.0f;
[SerializeField]
private StepUpProcessor _stepUpProcessor;
[SerializeField]
private GroundSnapProcessor _groundSnapProcessor;
// KCCProcessor INTERFACE
public override float GetPriority(KCC kcc) => DefaultPriority + RelativePriority;
// IPrepareData INTERFACE
public void Execute(PrepareData stage, KCC kcc, KCCData data)
{
data.MaxGroundAngle = MaxGroundAngle;
data.Gravity = Gravity != default ? Gravity : Physics.gravity;
data.Gravity *= kcc.FixedData.RealVelocity.y > 0.0f ? _upGravityMultiplier : _downGravityMultiplier;
SetDynamicVelocity(kcc, data);
data.KinematicDirection = data.InputDirection.OnlyXZ();
SetKinematicVelocity(kcc, data);
}
private void SetDynamicVelocity(KCC kcc, KCCData data)
{
// This code path can be executed in both fixed/render updates.
// Next fixed/render value is based on values from last fixed update to not introduce error accumulation due to render delta time integration.
// ERROR ACCUMULATION EXAMPLE
// ========================================
// Input: x(0) = 1.0f;
// Operation: x(n + 1) = x(n) * (1.0f + data.DeltaTime);
// Delta time for fixed update (60Hz): 0.016667f
// Delta time for render update (240Hz): 0.004167f
// RENDER UPDATE PREDICTION (240Hz, 4 frames)
// ========================================
// Result (frame 1): x(r1) = x(0) + x(0) * 0.004167f = 1.0f * (1.0f + 0.004167f) = 1.004167f
// Result (frame 2): x(r2) = x(r1) + x(r1) * 0.004167f = 1.004167f * (1.0f + 0.004167f) = 1.008351f
// Result (frame 3): x(r3) = x(r2) + x(r2) * 0.004167f = 1.008351f * (1.0f + 0.004167f) = 1.012552f
// Result (frame 4): x(r4) = x(r3) + x(r3) * 0.004167f = 1.012552f * (1.0f + 0.004167f) = 1.016771f
// FIXED UPDATE PREDICTION (60Hz, 1 tick)
// ========================================
// Result (tick 1): x(f1) = x(0) + x(0) * 0.016667f = 1.0f * (1.0f + 0.016667f) = 1.016667f
// THE PROBLEM
// ========================================
// Ideally we want mutiple render updates (total delta time equal to single fixed update) to match the fixed update: x(f1) == x(r4).
// Example above has a render update error: e(r) = x(f1) - x(r4) = 1.016667f - 1.016771f = -0.000104f
// This is only a demonstration and the error can result to a perceivable jitter, depends on error amplification.
// THE SOLUTION
// ========================================
// The trick is to make all intermediate calculations with FIXED update delta time (0.016667f).
// And use RENDER update delta time (0.004167f) only to calculate final position delta.
// This way 4 render position deltas will match 1 fixed position delta.
// This is the reason why fixedData is sometimes used instead of data (except values calculated in recently executed processors chain, these are safe to use).
// We explicitly need data from fixed update.
KCCData fixedData = kcc.FixedData;
float fixedDeltaTime = fixedData.DeltaTime;
Vector3 dynamicVelocity = fixedData.DynamicVelocity;
if (fixedData.IsGrounded == false || (fixedData.IsSteppingUp == false && (fixedData.IsSnappingToGround == true || fixedData.GroundDistance > 0.001f)))
{
// Applying gravity only while in the air (not grounded) and not stepping up, ground snapping can be active.
dynamicVelocity += data.Gravity * fixedDeltaTime;
}
if (data.JumpImpulse.IsZero() == false && JumpMultiplier > 0.0f)
{
Vector3 jumpDirection = data.JumpImpulse.normalized;
// Elimination of dynamic velocity in direction of jump, otherwise the jump trajectory would be distorted.
dynamicVelocity -= Vector3.Scale(dynamicVelocity, jumpDirection);
// Applying jump impulse.
dynamicVelocity += data.JumpImpulse * JumpMultiplier;
// Increase jump counter.
// For fixed updates this counter will be 1 at max.
// For render updates this counter can reach higher values, indicating how many times the JumpImpulse was applied on top of fixed data.
++data.JumpFrames;
}
// Apply external forces.
dynamicVelocity += data.ExternalVelocity;
dynamicVelocity += data.ExternalAcceleration * fixedDeltaTime;
dynamicVelocity += data.ExternalImpulse;
dynamicVelocity += data.ExternalForce * fixedDeltaTime;
if (dynamicVelocity.IsZero() == false)
{
if (dynamicVelocity.IsAlmostZero(0.001f) == true)
{
// Clamping values near zero.
dynamicVelocity = default;
}
else
{
// Applying ground (XYZ) and air (XZ) friction.
if (fixedData.IsGrounded == true)
{
Vector3 frictionAxis = Vector3.one;
if (fixedData.GroundDistance > 0.001f || fixedData.IsSnappingToGround == true)
{
frictionAxis.y = default;
}
dynamicVelocity += KCCPhysicsUtility.GetFriction(dynamicVelocity, dynamicVelocity, frictionAxis, fixedData.GroundNormal, fixedData.KinematicSpeed, true, 0.0f, 0.0f, DynamicGroundFriction, fixedDeltaTime);
}
else
{
dynamicVelocity += KCCPhysicsUtility.GetFriction(dynamicVelocity, dynamicVelocity, new Vector3(1.0f, 0.0f, 1.0f), fixedData.KinematicSpeed, true, 0.0f, 0.0f, DynamicAirFriction, fixedDeltaTime);
}
}
}
data.DynamicVelocity = dynamicVelocity;
if (kcc.IsInFixedUpdate == true)
{
// Consume one-time effects only in fixed update.
// For render prediction we need them to be applied on top of fixed data in all frames.
data.JumpImpulse = default;
data.ExternalVelocity = default;
data.ExternalImpulse = default;
}
// Forces applied over-time are reset always. These are set every tick/frame.
data.ExternalAcceleration = default;
data.ExternalForce = default;
}
private void SetKinematicVelocity(KCC kcc, KCCData data)
{
// SetKinematicTangent
KCCData fixedData = kcc.FixedData;
float fixedDeltaTime = fixedData.DeltaTime;
Vector3 kinematicVelocity = fixedData.KinematicVelocity;
if (fixedData.IsGrounded == true)
{
// The character is grounded.
if (data.KinematicDirection.IsAlmostZero(0.0001f) == false && KCCPhysicsUtility.ProjectOnGround(fixedData.GroundNormal, data.KinematicDirection, out Vector3 projectedMoveDirection) == true)
{
// Use projected kinematic direction on ground when possible.
data.KinematicTangent = projectedMoveDirection.normalized;
}
else
{
// Otherwise use ground tangent => steepest descent.
data.KinematicTangent = fixedData.GroundTangent;
}
}
else
{
// The character is floating in the air.
if (data.KinematicDirection.IsAlmostZero(0.0001f) == false)
{
// Use kinematic direction directly.
data.KinematicTangent = data.KinematicDirection.normalized;
}
else
{
// No direction set, use character forward.
data.KinematicTangent = data.TransformDirection;
}
}
// SetKinematicSpeed
data.KinematicSpeed = kcc.MoveState.GetBaseSpeed((Quaternion.Inverse(data.TransformRotation) * data.KinematicDirection).XZ0().normalized, default);
data.KinematicSpeed *= _speedMultiplier;
// SetKinematicVelocity
// This code path can be executed in both fixed/render updates.
// Next fixed/render value is based on values from last fixed update to not introduce error accumulation due to render delta time integration.
// Details are described above in SetDynamicVelocity().
if (fixedData.IsGrounded == true)
{
// The character is grounded.
if (kinematicVelocity.IsAlmostZero() == false && KCCPhysicsUtility.ProjectOnGround(fixedData.GroundNormal, kinematicVelocity, out Vector3 projectedKinematicVelocity) == true)
{
// Project current velocity on ground.
kinematicVelocity = projectedKinematicVelocity.normalized * kinematicVelocity.magnitude;
}
if (data.KinematicDirection.IsAlmostZero() == true)
{
// No kinematic direction
// Just apply friction (XYZ) and exit early.
data.KinematicVelocity = kinematicVelocity + KCCPhysicsUtility.GetFriction(kinematicVelocity, kinematicVelocity, Vector3.one, fixedData.GroundNormal, data.KinematicSpeed, true, 0.0f, 0.0f, KinematicGroundFriction, fixedDeltaTime);
return;
}
}
else
{
// The character is floating in the air.
if (data.KinematicDirection.IsAlmostZero() == true)
{
// No kinematic direction
// Just apply friction (XZ) and exit early.
data.KinematicVelocity = kinematicVelocity + KCCPhysicsUtility.GetFriction(kinematicVelocity, kinematicVelocity, new Vector3(1.0f, 0.0f, 1.0f), data.KinematicSpeed, true, 0.0f, 0.0f, KinematicAirFriction, fixedDeltaTime);
return;
}
}
// Following section calculates ground/air acceleration and friction relatively to move direction and kinematic tangent.
// And combines them together with base kinematic velocity.
Vector3 moveDirection = kinematicVelocity;
if (moveDirection.IsZero() == true)
{
moveDirection = data.KinematicTangent;
}
Vector3 acceleration;
Vector3 friction;
if (fixedData.IsGrounded == true)
{
acceleration = KCCPhysicsUtility.GetAcceleration(kinematicVelocity, data.KinematicTangent, Vector3.one, data.KinematicSpeed, false, data.KinematicDirection.magnitude, 0.0f, KinematicGroundAcceleration, 0.0f, fixedDeltaTime);
friction = KCCPhysicsUtility.GetFriction(kinematicVelocity, moveDirection, Vector3.one, fixedData.GroundNormal, data.KinematicSpeed, false, 0.0f, 0.0f, KinematicGroundFriction, fixedDeltaTime);
}
else
{
acceleration = KCCPhysicsUtility.GetAcceleration(kinematicVelocity, data.KinematicTangent, Vector3.one, data.KinematicSpeed, false, data.KinematicDirection.magnitude, 0.0f, KinematicAirAcceleration, 0.0f, fixedDeltaTime);
friction = KCCPhysicsUtility.GetFriction(kinematicVelocity, moveDirection, new Vector3(1.0f, 0.0f, 1.0f), data.KinematicSpeed, false, 0.0f, 0.0f, KinematicAirFriction, fixedDeltaTime);
}
kinematicVelocity = KCCPhysicsUtility.CombineAccelerationAndFriction(kinematicVelocity, acceleration, friction);
// Clamp velocity.
if (kinematicVelocity.sqrMagnitude > data.KinematicSpeed * data.KinematicSpeed)
{
kinematicVelocity = kinematicVelocity / Vector3.Magnitude(kinematicVelocity) * data.KinematicSpeed;
}
// Reset Y axis to get stable jump height results even if moving downwards.
if (data.JumpFrames > 0 && kinematicVelocity.y < 0.0f)
{
kinematicVelocity.y = 0.0f;
}
data.KinematicVelocity = kinematicVelocity;
}
// IAfterMoveStep INTERFACE
public void Execute(AfterMoveStep stage, KCC kcc, KCCData data)
{
// This code path can be executed multiple times in single update if CCD is active (Continuous Collision Detection).
KCCData fixedData = kcc.FixedData;
if (data.IsGrounded == true)
{
if (fixedData.WasGrounded == true && data.IsSnappingToGround == false && data.DynamicVelocity.y < 0.0f && data.DynamicVelocity.OnlyXZ().IsAlmostZero() == true)
{
// Reset dynamic velocity Y axis while grounded (to not accumulate gravity indefinitely and clamp to precise zero).
data.DynamicVelocity.y = 0.0f;
}
if (fixedData.WasGrounded == false)
{
if (data.KinematicVelocity.OnlyXZ().IsAlmostZero() == true)
{
// Reset Y axis after getting grounded and there is no horizontal movement.
data.KinematicVelocity.y = 0.0f;
}
else
{
// Otherwise try projecting kinematic velocity onto ground.
if (KCCPhysicsUtility.ProjectOnGround(data.GroundNormal, data.KinematicVelocity, out Vector3 projectedKinematicVelocity) == true)
{
data.KinematicVelocity = projectedKinematicVelocity.normalized * data.KinematicVelocity.magnitude;
}
}
}
}
else
{
if (fixedData.WasGrounded == false && data.DynamicVelocity.y > 0.0f && data.DeltaTime > 0.0f)
{
Vector3 currentVelocity;
if (kcc.IsInFixedUpdate == true)
{
currentVelocity = (data.TargetPosition - data.BasePosition) / data.DeltaTime;
}
else
{
currentVelocity = (data.TargetPosition - fixedData.TargetPosition) / (fixedData.DeltaTime * data.Alpha);
}
if (currentVelocity.y.IsAlmostZero() == true)
{
// Clamping dynamic up velocity if there is no real position change => hitting a roof.
data.DynamicVelocity.y = 0.0f;
}
}
}
_stepUpProcessor.Execute(stage, kcc, data);
_groundSnapProcessor.Execute(stage, kcc, data);
}
}
}