377 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
		
		
			
		
	
	
			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); | ||
|  | 		} | ||
|  | 	} | ||
|  | } |