Working with CharacterController
Make a player walk, jump and bump into walls, with full control over how the movement feels.
Imagine your character is inside an invisible capsule-shaped bumper. You say "move this way" and the bumper slides along walls instead of going through them. But the bumper has no gravity of its own: if you want it to fall or jump, you have to say so.
CharacterController or Rigidbody?
| CharacterController | Rigidbody |
|---|---|
| You decide exactly how it moves | Physics decides (forces, mass, bouncing) |
| Snappy, predictable platformer or FPS movement | Rolling balls, cars, ragdolls, things that get knocked around |
| No gravity unless you add it | Gravity built in |
Pick one. Don't put a CharacterController, a Rigidbody and a NavMeshAgent on the same object; they'll fight over who moves it.
Set up
- Make a floor, a few boxes, a small step and a ramp.
- Create an empty Player at (0, 1, 0) and add a CharacterController (Height 2, Radius 0.5). Put your model underneath as a child.
- Make sure the Input System package is installed (see here).
- Add the script below to Player and drag your camera into View.
The movement script
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{
[SerializeField] private Transform view; // the camera
[SerializeField] private float speed = 5f; // metres per second
[SerializeField] private float jumpHeight = 1.5f; // metres
[SerializeField] private float gravity = -20f; // negative = pulls down
private CharacterController controller;
private float verticalSpeed; // how fast we're rising/falling
void Awake() => controller = GetComponent<CharacterController>();
void Update()
{
var keyboard = Keyboard.current;
if (keyboard == null) return;
// 1. Read WASD as a 2D input
Vector2 input = Vector2.zero;
if (keyboard.wKey.isPressed) input.y += 1;
if (keyboard.sKey.isPressed) input.y -= 1;
if (keyboard.dKey.isPressed) input.x += 1;
if (keyboard.aKey.isPressed) input.x -= 1;
input = Vector2.ClampMagnitude(input, 1f); // diagonals aren't faster
// 2. Turn it into a direction relative to the camera, flat on the ground
Vector3 forward = view.forward; forward.y = 0; forward.Normalize();
Vector3 right = view.right; right.y = 0; right.Normalize();
Vector3 move = (forward * input.y + right * input.x) * speed;
// 3. Gravity and jumping
if (controller.isGrounded && verticalSpeed < 0)
verticalSpeed = -2f; // keep pressed onto the floor
if (controller.isGrounded && keyboard.spaceKey.wasPressedThisFrame)
verticalSpeed = Mathf.Sqrt(jumpHeight * -2f * gravity);
verticalSpeed += gravity * Time.deltaTime;
move.y = verticalSpeed;
// 4. Move once per frame. The controller handles walls and floors.
controller.Move(move * Time.deltaTime);
}
}
To support gamepads and rebinding, swap step 1 for an InputAction. The rest stays the same.
Why it works
Time.deltaTimeturns "metres per second" into "metres this frame", so speed is the same at 30 FPS and 144 FPS.verticalSpeedis remembered between frames. Gravity makes it more negative every frame, so you fall faster and faster, like real falling.- The jump formula
Sqrt(height * -2 * gravity)is the launch speed needed to reach exactlyjumpHeight. -2fwhen grounded keeps pushing gently down soisGroundedstays true while walking down ramps.
Move vs SimpleMove
Move(motion) | SimpleMove(speed) | |
|---|---|---|
| You pass | Distance for this frame (multiply by deltaTime) | Speed per second (don't multiply) |
| Gravity | You add it | Added for you |
| Jumping | Yes | No, it ignores Y |
Inspector settings
| Setting | What it does | Try |
|---|---|---|
| Height / Radius / Center | Size of the capsule. Fit it to the body, not to hats or swords. | 2 / 0.5 / (0,0,0) |
| Slope Limit | Steepest ramp you can walk up | 45 |
| Step Offset | Tallest step you can walk onto | 0.3 |
| Skin Width | A small safety gap that stops you getting stuck | 10% of the radius |
Going further
- Coyote time: remember when you were last on the ground and allow a jump for ~0.1 s after walking off a ledge. It makes jumping feel much fairer.
- Pushing boxes:
OnControllerColliderHit(ControllerColliderHit hit)runs when you bump into something. Ifhit.rigidbodyisn't null, push it withAddForce. - Moving platforms: the controller doesn't ride along automatically. Add the platform's movement for this frame to your own
Move.
Common Mistakes
- Setting
transform.positiondirectly. That skips collision and you walk through walls. UseMove. - Calling
Movetwice per frame (once for walking, once for gravity).isGroundedonly reflects the last call, so combine them into one. - Forgetting
Time.deltaTimewithMove, or adding it toSimpleMove. - Using
.normalizedon stick input. Slow walking then becomes impossible. UseClampMagnitude.
See also
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official documentation.