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?

CharacterControllerRigidbody
You decide exactly how it movesPhysics decides (forces, mass, bouncing)
Snappy, predictable platformer or FPS movementRolling balls, cars, ragdolls, things that get knocked around
No gravity unless you add itGravity 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

  1. Make a floor, a few boxes, a small step and a ramp.
  2. Create an empty Player at (0, 1, 0) and add a CharacterController (Height 2, Radius 0.5). Put your model underneath as a child.
  3. Make sure the Input System package is installed (see here).
  4. 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.

Read inputCamera-relative directionAdd gravity / jumpcontroller.Move()
One Move call per frame, with everything combined.

Why it works

Move vs SimpleMove

Move(motion)SimpleMove(speed)
You passDistance for this frame (multiply by deltaTime)Speed per second (don't multiply)
GravityYou add itAdded for you
JumpingYesNo, it ignores Y

Inspector settings

SettingWhat it doesTry
Height / Radius / CenterSize of the capsule. Fit it to the body, not to hats or swords.2 / 0.5 / (0,0,0)
Slope LimitSteepest ramp you can walk up45
Step OffsetTallest step you can walk onto0.3
Skin WidthA small safety gap that stops you getting stuck10% of the radius

Going further

Common Mistakes

See also