Movement That Feels Right
Movement is one of those things that players feel immediately. Get it wrong and everything feels off. Get it right and it's invisible — the player just feels in control. Here's how we built ours.
Why CharacterController?
We chose CharacterController over Rigidbody. It's a trade-off: we lose physics simulation for the player body, but gain precise control over movement feel. For a game where tight controls matter, this was the right call.
The Basic Setup
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float sprintSpeed = 8f;
[SerializeField] private float rotationSpeed = 720f;
[Header("Physics")]
[SerializeField] private float gravity = -9.81f;
[SerializeField] private float jumpHeight = 1.5f;
private CharacterController controller;
private Vector3 velocity;
private bool isGrounded;
void Start()
{
controller = GetComponent();
}
void Update()
{
HandleGroundCheck();
HandleMovement();
HandleGravity();
}
void HandleGroundCheck()
{
// Small sphere cast for ground detection
isGrounded = Physics.CheckSphere(
transform.position - Vector3.up * 0.1f,
0.4f,
LayerMask.GetMask("Ground")
);
if (isGrounded && velocity.y < 0)
{
velocity.y = -2f; // Small downward force when grounded
}
}
void HandleMovement()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(horizontal, 0f, vertical).normalized;
if (movement.magnitude >= 0.1f)
{
// Calculate facing direction
float targetAngle = Mathf.Atan2(movement.x, movement.z) *
Mathf.Rad2Deg + Camera.main.transform.eulerAngles.y;
float angle = Mathf.SmoothDampAngle(
transform.eulerAngles.y,
targetAngle,
ref rotationSpeed,
0.1f
);
transform.rotation = Quaternion.Euler(0f, angle, 0f);
// Move the player
Vector3 moveDir = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward;
controller.Move(moveDir.normalized * moveSpeed * Time.deltaTime);
}
}
void HandleGravity()
{
velocity.y += gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime);
}
}
Smooth Acceleration
003cp>Instant acceleration feels robotic. We useMathf.SmoothDamp for velocity smoothing:
[SerializeField] private float acceleration = 10f;
private Vector3 currentVelocity;
void HandleMovement()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector3 targetMovement = new Vector3(horizontal, 0f, vertical).normalized;
// Smooth acceleration
currentVelocity = Vector3.SmoothDamp(
currentVelocity,
targetMovement * moveSpeed,
ref velocityRef,
1f / acceleration
);
controller.Move(currentVelocity * Time.deltaTime);
}
Camera-Relative Movement
The player should move relative to where they're looking, not the world. The camera angle adjustment in HandleMovement() handles this automatically.
Next Steps
This is the foundation. Coming up: sprinting, crouching, and raycasting for interaction.