using UnityEngine; namespace RebootKit.Engine.Services.Simulation.Characters { public class CharacterLocomotion : MonoBehaviour { [SerializeField] private CharacterController _characterController; public float MaxMovementSpeed = 4.0f; public float MaxSprintSpeed = 15.0f; public float JumpHeight = 1.0f; public float Gravity = 10f; public float MaxFallSpeed = 20f; public float Damping = 20.0f; private Vector3 _pendingInputValue; private Vector3 _currentVelocity; private bool _isSprinting; private bool _jumpRequested; private bool _isFalling; public bool IsGrounded => _characterController.isGrounded; public Vector3 Velocity => _currentVelocity; private void Update() { ConsumePendingInput(); UpdateVerticalVelocity(); _characterController.Move(_currentVelocity * Time.deltaTime); ApplyFriction(); DetectFall(); } private void DetectFall() { if (_isFalling && _characterController.isGrounded) { _isFalling = false; } else if (!_characterController.isGrounded) { _isFalling = true; } } private void ConsumePendingInput() { if (!IsGrounded) { _pendingInputValue = Vector3.zero; return; } _pendingInputValue.y = 0.0f; float pendingInputMagnitude = _pendingInputValue.magnitude; Vector3 direction = Vector3.zero; if (pendingInputMagnitude > 0.0f) { // normalize vector, reusing magnitude to avoid multiple sqrt calls direction = _pendingInputValue / pendingInputMagnitude; } float movementSpeed = _isSprinting ? MaxSprintSpeed : MaxMovementSpeed; Vector3 movementVelocity = _currentVelocity; movementVelocity.y = 0.0f; movementVelocity += direction * (movementSpeed * pendingInputMagnitude); movementVelocity.y = 0.0f; // Clamp speed float movementVelocityMagnitude = movementVelocity.magnitude; Vector3 movementVelocityDirection = movementVelocityMagnitude > 0.0f ? movementVelocity / movementVelocityMagnitude : Vector3.zero; if (movementVelocityMagnitude > movementSpeed) { movementVelocityMagnitude = movementSpeed; } movementVelocity = movementVelocityDirection * movementVelocityMagnitude; _currentVelocity.x = movementVelocity.x; _currentVelocity.z = movementVelocity.z; _pendingInputValue = Vector3.zero; } private void UpdateVerticalVelocity() { if (_characterController.isGrounded) { if (_jumpRequested) { _currentVelocity.y = Mathf.Sqrt(2.0f * Gravity * JumpHeight); _jumpRequested = false; } else { _currentVelocity.y = -1f; } } else { _currentVelocity.y -= Gravity * Time.deltaTime; _currentVelocity.y = Mathf.Max(_currentVelocity.y, -MaxFallSpeed); } } private void ApplyFriction() { if (!IsGrounded) { return; } Vector3 movementVelocity = _currentVelocity; movementVelocity.y = 0.0f; movementVelocity = Vector3.MoveTowards(movementVelocity, Vector3.zero, Damping * Time.deltaTime); _currentVelocity.x = movementVelocity.x; _currentVelocity.z = movementVelocity.z; } public void AddVelocity(Vector3 velocity) { _currentVelocity += velocity; } public void AddMovementInput(Vector3 input, float scale) { _pendingInputValue += input * scale; } public void Jump() { if (!_characterController.isGrounded) { return; } _jumpRequested = true; } public void StartSprint() { _isSprinting = true; } public void StopSprint() { _isSprinting = false; } } }