355 lines
12 KiB
C#
355 lines
12 KiB
C#
using UnityEngine;
|
|
|
|
namespace MegaKoop.Game
|
|
{
|
|
[RequireComponent(typeof(UnityEngine.CharacterController))]
|
|
[RequireComponent(typeof(Animator))]
|
|
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;
|
|
[SerializeField, Range(0f, 0.3f)] private float jumpBufferTime = 0.1f;
|
|
[SerializeField, Range(0f, 0.3f)] private float coyoteTime = 0.1f;
|
|
[SerializeField, Min(0.5f)] private float upwardGravityMultiplier = 1f;
|
|
[SerializeField, Min(1f)] private float fallGravityMultiplier = 2.5f;
|
|
|
|
[Header("Camera Reference")]
|
|
[SerializeField] private Transform cameraTransform;
|
|
|
|
[Header("Animation")]
|
|
[SerializeField] private Animator animator;
|
|
[SerializeField] private float animationDamping = 0.075f;
|
|
[SerializeField] private KeyCode crouchKey = KeyCode.LeftControl;
|
|
|
|
private UnityEngine.CharacterController characterController;
|
|
private Vector3 planarVelocity;
|
|
private float verticalVelocity;
|
|
private bool isGrounded;
|
|
private bool lastGrounded;
|
|
private bool isDead;
|
|
private bool isJumping;
|
|
private MegaKoop.Game.Networking.ICharacterInputSource inputSource;
|
|
private float lastJumpPressedTime = float.NegativeInfinity;
|
|
private float lastTimeGrounded = float.NegativeInfinity;
|
|
|
|
// Animator parameter hashes
|
|
private int hashMoveX;
|
|
private int hashMoveZ;
|
|
private int hashSpeed;
|
|
private int hashMoveSpeedNormalized;
|
|
private int hashIsGrounded;
|
|
private int hashIsCrouching;
|
|
private int hashIsDead;
|
|
private int hashIsJumping;
|
|
private bool animatorHashesInitialized;
|
|
|
|
private void Reset()
|
|
{
|
|
characterController = GetComponent<UnityEngine.CharacterController>();
|
|
animator = GetComponent<Animator>();
|
|
InitializeAnimatorHashes();
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
characterController = GetComponent<UnityEngine.CharacterController>();
|
|
EnsureAnimatorReference();
|
|
|
|
if (cameraTransform == null)
|
|
{
|
|
Camera mainCamera = Camera.main;
|
|
if (mainCamera != null)
|
|
{
|
|
cameraTransform = mainCamera.transform;
|
|
}
|
|
}
|
|
InitializeAnimatorHashes();
|
|
|
|
isGrounded = characterController.isGrounded;
|
|
lastGrounded = isGrounded;
|
|
if (isGrounded)
|
|
{
|
|
verticalVelocity = groundedGravity;
|
|
}
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
EnsureAnimatorReference();
|
|
InitializeAnimatorHashes();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
EnsureAnimatorReference();
|
|
InitializeAnimatorHashes();
|
|
|
|
Vector2 moveInput = ReadMovementInput();
|
|
TrackJumpInput();
|
|
|
|
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;
|
|
}
|
|
|
|
UpdateAnimator();
|
|
}
|
|
|
|
public void SetInputSource(MegaKoop.Game.Networking.ICharacterInputSource source)
|
|
{
|
|
inputSource = source;
|
|
}
|
|
|
|
public void SetCameraTransform(Transform cam)
|
|
{
|
|
cameraTransform = cam;
|
|
}
|
|
|
|
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)
|
|
{
|
|
lastTimeGrounded = Time.time;
|
|
}
|
|
}
|
|
|
|
private void HandleJumpInput()
|
|
{
|
|
bool bufferedJump = Time.time - lastJumpPressedTime <= jumpBufferTime;
|
|
bool coyoteAvailable = Time.time - lastTimeGrounded <= coyoteTime;
|
|
|
|
if (!bufferedJump)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (isGrounded || coyoteAvailable)
|
|
{
|
|
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
|
isGrounded = false;
|
|
isJumping = true;
|
|
lastJumpPressedTime = float.NegativeInfinity;
|
|
}
|
|
}
|
|
|
|
private void TrackJumpInput()
|
|
{
|
|
bool jumpPressed = false;
|
|
|
|
if (inputSource != null)
|
|
{
|
|
jumpPressed = inputSource.JumpPressed;
|
|
}
|
|
else if (Input.GetButtonDown("Jump"))
|
|
{
|
|
jumpPressed = true;
|
|
}
|
|
|
|
if (jumpPressed)
|
|
{
|
|
lastJumpPressedTime = Time.time;
|
|
}
|
|
}
|
|
|
|
private void ApplyGravity()
|
|
{
|
|
float gravityMultiplier = verticalVelocity > 0f ? upwardGravityMultiplier : fallGravityMultiplier;
|
|
float currentGravity = gravity * gravityMultiplier;
|
|
|
|
if (isGrounded && verticalVelocity < 0f)
|
|
{
|
|
verticalVelocity = groundedGravity;
|
|
lastTimeGrounded = Time.time;
|
|
isJumping = false;
|
|
}
|
|
else
|
|
{
|
|
verticalVelocity += currentGravity * Time.deltaTime;
|
|
}
|
|
}
|
|
|
|
private void UpdateAnimator()
|
|
{
|
|
if (animator == null) return;
|
|
|
|
// Use actual CharacterController velocity for accurate animation
|
|
Vector3 actualVelocity = characterController.velocity;
|
|
Vector3 planarActualVelocity = new Vector3(actualVelocity.x, 0f, actualVelocity.z);
|
|
|
|
// Calculate movement in local space
|
|
Vector3 localVelocity = transform.InverseTransformDirection(planarActualVelocity);
|
|
float speed = planarActualVelocity.magnitude;
|
|
float denom = Mathf.Max(0.01f, moveSpeed);
|
|
float moveX = Mathf.Clamp(localVelocity.x / denom, -1f, 1f);
|
|
float moveZ = Mathf.Clamp(localVelocity.z / denom, -1f, 1f);
|
|
float normalizedSpeed = Mathf.Clamp01(speed / moveSpeed);
|
|
|
|
// Update animator parameters
|
|
animator.SetFloat(hashSpeed, speed);
|
|
animator.SetFloat(hashMoveSpeedNormalized, normalizedSpeed);
|
|
animator.SetFloat(hashMoveX, moveX, animationDamping, Time.deltaTime);
|
|
animator.SetFloat(hashMoveZ, moveZ, animationDamping, Time.deltaTime);
|
|
animator.SetBool(hashIsGrounded, isGrounded);
|
|
animator.SetBool(hashIsJumping, isJumping);
|
|
|
|
// Crouch input (currently only supports local input, can be extended via inputSource)
|
|
bool isCrouching = !isDead && Input.GetKey(crouchKey);
|
|
animator.SetBool(hashIsCrouching, isCrouching);
|
|
|
|
// Jump trigger - when leaving ground with upward velocity
|
|
animator.SetBool(hashIsDead, isDead);
|
|
lastGrounded = isGrounded;
|
|
}
|
|
|
|
public void SetDead(bool dead)
|
|
{
|
|
isDead = dead;
|
|
EnsureAnimatorReference();
|
|
InitializeAnimatorHashes();
|
|
if (animator != null)
|
|
{
|
|
animator.SetBool(hashIsDead, isDead);
|
|
}
|
|
}
|
|
|
|
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);
|
|
animationDamping = Mathf.Max(0f, animationDamping);
|
|
jumpBufferTime = Mathf.Clamp(jumpBufferTime, 0f, 0.3f);
|
|
coyoteTime = Mathf.Clamp(coyoteTime, 0f, 0.3f);
|
|
upwardGravityMultiplier = Mathf.Max(0.5f, upwardGravityMultiplier);
|
|
fallGravityMultiplier = Mathf.Max(1f, fallGravityMultiplier);
|
|
EnsureAnimatorReference();
|
|
InitializeAnimatorHashes();
|
|
}
|
|
|
|
private void EnsureAnimatorReference()
|
|
{
|
|
if (animator == null)
|
|
{
|
|
animator = GetComponent<Animator>();
|
|
animatorHashesInitialized = false;
|
|
}
|
|
}
|
|
|
|
private void InitializeAnimatorHashes()
|
|
{
|
|
if (animator == null || animatorHashesInitialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
hashMoveX = Animator.StringToHash("MoveX");
|
|
hashMoveZ = Animator.StringToHash("MoveZ");
|
|
hashSpeed = Animator.StringToHash("Speed");
|
|
hashIsGrounded = Animator.StringToHash("IsGrounded");
|
|
hashIsCrouching = Animator.StringToHash("IsCrouching");
|
|
hashIsDead = Animator.StringToHash("IsDead");
|
|
hashIsJumping = Animator.StringToHash("IsJumping");
|
|
hashMoveSpeedNormalized = Animator.StringToHash("MoveSpeedNormalized");
|
|
animatorHashesInitialized = true;
|
|
}
|
|
}
|
|
}
|