using UnityEngine; namespace MegaKoop.Game { [RequireComponent(typeof(UnityEngine.CharacterController))] public class ThirdPersonCharacterController : MonoBehaviour { [Header("Movement")] [SerializeField] private float moveSpeed = 5f; [SerializeField] private float rotationSharpness = 12f; [Header("Air Control")] [SerializeField] private float airControlResponsiveness = 50f; [Header("Jump")] [SerializeField] private float jumpHeight = 1.6f; [SerializeField] private float gravity = -20f; [SerializeField] private float groundedGravity = -5f; [Header("Camera Reference")] [SerializeField] private Transform cameraTransform; private UnityEngine.CharacterController characterController; private Vector3 planarVelocity; private float verticalVelocity; private bool isGrounded; private MegaKoop.Game.Networking.ICharacterInputSource inputSource; private void Awake() { characterController = GetComponent(); if (cameraTransform == null) { Camera mainCamera = Camera.main; if (mainCamera != null) { cameraTransform = mainCamera.transform; } } isGrounded = characterController.isGrounded; if (isGrounded) { verticalVelocity = groundedGravity; } } private void Update() { Vector2 moveInput = ReadMovementInput(); Vector3 desiredMove = CalculateDesiredMove(moveInput); bool hasMoveInput = desiredMove.sqrMagnitude > 0f; UpdatePlanarVelocity(desiredMove, hasMoveInput); TryRotateTowardsMovement(desiredMove, hasMoveInput); UpdateGroundedStateBeforeGravity(); HandleJumpInput(); ApplyGravity(); Vector3 velocity = planarVelocity; velocity.y = verticalVelocity; CollisionFlags collisionFlags = characterController.Move(velocity * Time.deltaTime); isGrounded = (collisionFlags & CollisionFlags.Below) != 0; if (isGrounded && verticalVelocity < 0f) { verticalVelocity = groundedGravity; } } public void SetInputSource(MegaKoop.Game.Networking.ICharacterInputSource source) { inputSource = source; } private Vector2 ReadMovementInput() { if (inputSource != null) { Vector2 sourceInput = inputSource.MoveInput; return Vector2.ClampMagnitude(sourceInput, 1f); } float horizontal = Input.GetAxisRaw("Horizontal"); float vertical = Input.GetAxisRaw("Vertical"); Vector2 input = new Vector2(horizontal, vertical); input = Vector2.ClampMagnitude(input, 1f); return input; } private Vector3 CalculateDesiredMove(Vector2 input) { Vector3 forward = Vector3.forward; Vector3 right = Vector3.right; if (cameraTransform != null) { forward = cameraTransform.forward; right = cameraTransform.right; } forward.y = 0f; right.y = 0f; forward.Normalize(); right.Normalize(); Vector3 desiredMove = forward * input.y + right * input.x; if (desiredMove.sqrMagnitude > 1f) { desiredMove.Normalize(); } return desiredMove; } private void UpdatePlanarVelocity(Vector3 desiredMove, bool hasMoveInput) { if (isGrounded) { planarVelocity = hasMoveInput ? desiredMove * moveSpeed : Vector3.zero; return; } if (airControlResponsiveness <= 0f) { return; } Vector3 targetVelocity = hasMoveInput ? desiredMove * moveSpeed : Vector3.zero; float maxDelta = airControlResponsiveness * Time.deltaTime; planarVelocity = Vector3.MoveTowards(planarVelocity, targetVelocity, maxDelta); } private void TryRotateTowardsMovement(Vector3 desiredMove, bool hasMoveInput) { if (!hasMoveInput) { return; } Quaternion targetRotation = Quaternion.LookRotation(desiredMove, Vector3.up); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSharpness * Time.deltaTime); } private void UpdateGroundedStateBeforeGravity() { if (isGrounded && verticalVelocity < 0f) { verticalVelocity = groundedGravity; } } private void HandleJumpInput() { if (!isGrounded) { return; } if (ShouldJumpThisFrame()) { verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity); isGrounded = false; } } private bool ShouldJumpThisFrame() { if (inputSource != null) { return inputSource.JumpPressed; } return Input.GetButtonDown("Jump"); } private void ApplyGravity() { verticalVelocity += gravity * Time.deltaTime; } private void OnValidate() { moveSpeed = Mathf.Max(0f, moveSpeed); rotationSharpness = Mathf.Max(0f, rotationSharpness); jumpHeight = Mathf.Max(0f, jumpHeight); airControlResponsiveness = Mathf.Max(0f, airControlResponsiveness); gravity = Mathf.Min(-0.01f, gravity); groundedGravity = Mathf.Clamp(groundedGravity, gravity, 0f); } } }