Unity Character Controller

Summary

The Character Controller is Unity’s built-in component for moving a first- or third-person character through a 3D level. It gives the character “a simple, capsule-shaped collider that is always upright”, and its movement is “intentionally not physically realistic”: the character can start, stop and turn almost instantly, without momentum (Unity Technologies, Unity 6.3 Manual, “Introduction to character control”). Your script decides where the capsule should go each frame and calls Move; the controller then slides it along walls, up steps and slopes, and stops it at obstacles. It needs no Rigidbody and is not pushed around by physics.

Key ideas

  • Script-driven, collision-aware. Move takes a displacement for this frame; “a collision constrains the Move from taking place” (Unity Technologies, Scripting API, “CharacterController.Move”). The controller resolves collisions, but nothing moves it except your code.
  • No forces, no automatic gravity. “The Controller does not react to forces on its own”, and it “can not be affected by objects through physics” (Unity Technologies, Unity 6.3 Manual, “Character Controller component reference”). Move “does not use gravity”, so falling and jumping are your job.
  • Always upright. The capsule never tips over, which is what you want for a walking character and why no rotation constraints are needed.
  • Grounding is a by-product of moving. isGrounded answers “was the CharacterController touching the ground during the last move?” and is updated only by Move or SimpleMove (Unity Technologies, Scripting API, “CharacterController.isGrounded”).
  • Shape and tolerance settings matter. Slope Limit, Step Offset and Skin Width decide whether a level built at a sensible scale (3d-blocking-and-greyboxing) is actually walkable.

In practice

Setup

  1. Create a capsule (GameObject > 3D Object > Capsule), remove its Capsule Collider, and add Character Controller. The controller has its own capsule collider.
  2. Do not add a Rigidbody. A Rigidbody would hand the object to the physics engine, which is the opposite of what a Character Controller is for.
  3. Add a movement script. It runs in Update, scales by Time.deltaTime (monobehaviour-lifecycle) and calls Move once.

Move or SimpleMove

Move(Vector3 motion)SimpleMove(Vector3 speed)
Argumentdisplacement this frame (“absolute movement delta values”), so multiply velocity by Time.deltaTimespeed in units per second; Unity handles the frame time
Vertical axisused: you add gravity and jump yourself”Velocity along the y-axis is ignored”
Gravity”does not use gravity""Gravity is automatically applied”
ReturnsCollisionFlags (None, Sides, Above, Below)true if grounded
Use foranything that jumps, falls in a controlled way or needs custom gravitythe simplest walking-only character

Sources for the table: Unity Technologies, Scripting API, “CharacterController.Move” and “CharacterController.SimpleMove”. The same pages recommend making “only one call to Move or SimpleMove per frame”. Calling both, or calling Move twice (once for walking and once for gravity), makes isGrounded describe only the last call.

Camera-relative movement with the Input System

New Unity 6 projects use the Input System (unity-input-system; the older Input.GetAxis API taught in unity-input is contrasted in unity-input-manager-vs-input-system). The project-wide actions asset includes default Move and Jump actions, reached in code with InputSystem.actions.FindAction("Move"); if the project has none, create it in Edit > Project Settings > Input System Package (Unity Technologies, Input System manual, “Project-wide actions”).

For a third-person camera, “up” on the stick should mean “away from the camera”, not “world north”. Flatten the camera’s forward and right vectors onto the floor and combine them with the input:

using UnityEngine;
using UnityEngine.InputSystem;
 
[RequireComponent(typeof(CharacterController))]
public class CameraRelativeMover : MonoBehaviour
{
    [SerializeField] float moveSpeed = 4f, gravity = -20f, jumpHeight = 1.2f;
    CharacterController controller;
    InputAction move, jump;
    float verticalVelocity;
 
    void Start()
    {
        controller = GetComponent<CharacterController>();
        move = InputSystem.actions.FindAction("Move");
        jump = InputSystem.actions.FindAction("Jump");
    }
 
    void Update()
    {
        Vector2 input = move.ReadValue<Vector2>();
        Transform cam = Camera.main.transform;
        Vector3 forward = Vector3.ProjectOnPlane(cam.forward, Vector3.up).normalized;
        Vector3 right = Vector3.ProjectOnPlane(cam.right, Vector3.up).normalized;
        Vector3 motion = (forward * input.y + right * input.x) * moveSpeed;
 
        if (controller.isGrounded && verticalVelocity < 0f) verticalVelocity = -2f;     // keep touching the floor
        if (controller.isGrounded && jump.WasPressedThisFrame())
            verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);              // v = sqrt(2gh)
        verticalVelocity += gravity * Time.deltaTime;
 
        motion.y = verticalVelocity;
        controller.Move(motion * Time.deltaTime);                                     // one Move per frame
    }
}

Unity’s own CharacterController.Move example follows the same pattern: a small downward velocity while grounded, the jump velocity from the square root of height × −2 × gravity, and an Input System action as the input source (Unity Technologies, Scripting API, “CharacterController.Move”). A full version that also turns the character to face its movement, falls back to Camera.main safely, accepts InputActionReferences and adds a short coyote-time grace is in code/unity-csharp/3d/ (SimplePlayerController.cs, with scene setup in the folder’s README).

isGrounded quirks

  • It describes the previous move. It is updated after each Move, based on whether that move hit anything beneath the capsule (Unity Technologies, Scripting API, “CharacterController.isGrounded”). Read it before this frame’s Move, and remember that it is one frame old.
  • No downward motion, no ground contact. If vertical velocity is reset to exactly 0 on landing, the next Move may not touch the floor at all, and isGrounded turns false. Resetting to a small negative value (−2 in the snippet, as in Unity’s example) keeps the capsule pressed to the floor.
  • Walking down slopes and off steps. Moving horizontally faster than gravity pulls down can briefly lift the capsule off a downward slope, so isGrounded flickers. This is arguably the main reason games add coyote time (a short grace period in which a jump still counts); the example code includes one. This explanation is an inference from the documented behaviour, not a statement in Unity’s documentation.

Tuning the component

PropertyWhat it does (Unity’s description)Starting value for a 2 m human
Slope Limitonly climbs slopes “less steep (in degrees) than the indicated value”the manual says 90° “often works best”; lower it if players should be stopped by steep ramps
Step Offsetsteps up a stair “only if it is closer to the ground than the indicated value”; must not exceed the controller’s height0.1–0.4
Skin Widthhow far two colliders may penetrate; larger values reduce jitter, too small causes stickingat least 0.01 and about 10% of Radius
Min Move Distancemoves smaller than this are ignored0
Center, Radius, Heightthe capsule’s shape; Center offsets the capsule without changing the pivotHeight about 2, Radius about 0.5

All descriptions and recommended values are from Unity Technologies, Unity 6.3 Manual, “Character Controller component reference”, which also advises using about 2 m for a human-like character and notes that “if your character gets stuck it is most likely because your Skin Width is too small”. Stair risers in a greybox should therefore be lower than Step Offset; ProBuilder’s Stairs shape (unity-probuilder) makes this easy to test.

Collisions: OnControllerColliderHit

Because the controller is not part of the physics simulation, it does not push Rigidbodies by itself; the component reference says to “apply forces to any object that it collides with via the OnControllerColliderHit() function”. Unity calls OnControllerColliderHit(ControllerColliderHit hit) on scripts attached to the controller’s GameObject whenever a Move touches a collider (Unity Technologies, Scripting API, “CharacterController.OnControllerColliderHit”). The Scripting API example gets hit.collider.attachedRigidbody, skips kinematic bodies and things below the character, and sets the body’s velocity in the horizontal part of hit.moveDirection. ControllerRigidbodyPusher.cs does the same with Unity 6’s linearVelocity and divides the push by mass. The controller does take part in trigger events: the component reference warns that changing its properties in the Inspector recreates it, so “any existing Trigger contacts will get lost”. Pickups and goal zones can therefore use trigger colliders and OnTriggerEnter, the 3D form of the pattern in unity-collider2d-and-triggers.

Note that Unity’s introductory page says the controller “can push rigidbody objects aside while moving”, while the component reference says it “does not automatically push Rigidbodies away”. In practice, write the push yourself in OnControllerColliderHit if you need it.

CharacterController or Rigidbody?

CharacterControllerRigidbody-based controller
Who moves ityour script, via Move / SimpleMovethe physics engine, via velocity, forces or MovePosition
Where code runsUpdateFixedUpdate for physics changes (see unity-rigidbody2d for the 2D version of this pattern)
Gravityyou write it (or SimpleMove)built in
Steps and slopesStep Offset and Slope Limit built inyou handle them (step-up logic, slope projection)
Uprightalwaysfreeze rotation on the Rigidbody
Pushed by explosions, moving platforms, other bodiesno, unless you script ityes
Feelcrisp, arcade, instant stopsweightier, momentum-based; needs damping or drag to stop cleanly

Unity’s component reference states the trade-off directly: “if you want your player character to be affected by physics then you might be better off using a Rigidbody instead of the Character Controller” (Unity Technologies, Unity 6.3 Manual, “Character Controller component reference”). For a first 3D project with walking, jumping and stairs, the Character Controller is usually the faster route to a controllable player. A Rigidbody fits ragdoll-adjacent, vehicle-like or physics-puzzle characters. transform-vs-rigidbody2d-movement makes the parallel argument for 2D.

Unity’s Starter Assets

Unity Technologies publishes free ready-made controllers as First Person + Third Person | Character Controllers on the Asset Store. At the time of writing the listing shows version 2.0.1 (18 September 2026) and marks it URP-compatible for Unity 6000.3 (Unity Technologies, Asset Store listing, accessed 2026-10-25). The older Starter Assets: Character Controllers | URP listing is marked as deprecated, so do not start new projects from it (Asset Store listing, accessed 2026-10-25).

Unity’s launch post describes the Starter Assets as controllers built “with the built-in CharacterController component” that give “a solid base to practice using Cinemachine and the Input System” (Vig, Say hello to the new Starter Asset packages, Unity blog, 2021). Import them from My Assets in the Package Manager, open the included sample scene first, then bring the player prefab into your own scene.

Use Starter Assets when the goal is a working player in minutes (a greybox test or a design prototype). Write your own when the goal is learning how movement works, or when you need behaviour the package does not have. Reading the package’s controller script after writing your own is a good comparison exercise. Whether release 2.x has moved to Cinemachine 3 was not verified for this page; check which Cinemachine version the package installs, and do not add a second camera rig on top of the one it brings.

Camera: Cinemachine 3

A Character Controller only moves the body. For the camera, Cinemachine 3’s GameObject > Cinemachine > Targeted Cameras > Follow Camera creates a Cinemachine Camera with Cinemachine Follow (position) and Cinemachine Rotation Composer (rotation); set its Tracking Target to an empty child of the player at head height, and check that the Main Camera has a Cinemachine Brain (Unity Technologies, Cinemachine 3.1 manual, “Set up a follow camera”). With camera-relative movement, choose the World Space or Lazy Follow binding mode on Cinemachine Follow: in those modes the camera does not swing round when the target rotates (Unity Technologies, Cinemachine 3.1 manual, “Cinemachine Follow”). A camera locked to the target’s rotation turns with the player, which changes the “forward” the player is moving along, and the character tends to circle. cinemachine-overview covers Cinemachine more generally (it still uses the Cinemachine 2 term “virtual camera”).

Other engines

  • Godot: CharacterBody3D is the equivalent. It is “not affected by physics at all” but affects other bodies, moves with move_and_slide(), and reports grounding with is_on_floor() based on the last move_and_slide() call, with floor_max_angle playing the role of Slope Limit (Godot Engine, CharacterBody3D class reference).
  • Unreal Engine: a Character uses a vertically oriented capsule and a CharacterMovementComponent that “allows avatars not using rigid body physics to move”, with walking, jumping, falling, swimming and flying modes built in (Epic Games, Characters in Unreal Engine).

Evidence

  • Unity describes the Character Controller as an upright capsule whose acceleration and movement “are intentionally not physically realistic” and which needs no Rigidbody (Unity Technologies, Unity 6.3 Manual, “Introduction to character control”).
  • The component reference gives the tuning guidance: about 2 m for a human, Step Offset 0.1–0.4, Skin Width at least 10% of Radius, Min Move Distance 0, and a Rigidbody instead if the character must be affected by physics (Unity Technologies, Unity 6.3 Manual, “Character Controller component reference”).
  • The Scripting API pages state that Move does not apply gravity and returns CollisionFlags, that SimpleMove ignores y and applies gravity, that isGrounded reflects the last move, and that only one Move or SimpleMove call should be made per frame (Unity Technologies, Scripting API, “CharacterController.Move”, “SimpleMove”, “isGrounded”).
  • Godot and Unreal both provide a script-driven, capsule-based character body alongside their rigid body physics (Godot Engine, CharacterBody3D; Epic Games, Characters in Unreal Engine). This suggests that kinematic character movement is the conventional default for player characters across engines, not a Unity quirk.

Implications

  • Level metrics and controller settings are one design decision. Step heights, ramp angles and doorway widths in the greybox must match Step Offset, Slope Limit and Radius.
  • Jump feel comes from your numbers, not from physics. Gravity stronger than −9.81 and a chosen jump height are design parameters (game-feel).
  • Anything physical the player does to the world (pushing crates, being knocked back) must be written explicitly, which is extra code but also full control.
  • Starter Assets save time but hide the decisions. Students who only use them should still be able to explain Move, isGrounded and camera-relative input.

Open questions

  • At what point in a course should students switch from a Character Controller to a Rigidbody player, if ever? Physics-driven characters feel different and suit different genres.
  • Should a beginner controller include coyote time and jump buffering from the start, or should students add them after feeling the problem?

Practice

Build a camera-relative third-person player on a Character Controller and test it in a small greybox.

  • a ProBuilder room with stairs (0.25 m risers), a 30° ramp and a 50° ramp
  • a capsule player with a Character Controller (Height 2, Radius 0.5, Skin Width 0.05, Step Offset 0.3) and no Rigidbody
  • a script that reads Move and Jump with the Input System, moves relative to the Main Camera, applies gravity and jumps with Mathf.Sqrt(jumpHeight * -2f * gravity)
  • a Cinemachine Follow Camera with Binding Mode set to World Space

Success test:

  • the player climbs the stairs and the 30° ramp, and is stopped by the 50° ramp when Slope Limit is 45
  • pushing “up” always moves the player away from the camera, wherever the camera is
  • the player can jump repeatedly while standing still on flat floor (no missed jumps)
  • the script contains exactly one controller.Move call per frame
  • the GameObject has no Rigidbody component

Extension:

  • add OnControllerColliderHit to push Rigidbody crates, then try the same scene with Unity’s Starter Assets and write three differences you notice in feel or setup

Self-test

  1. Does CharacterController.Move apply gravity? What about SimpleMove?
  2. Why is vertical velocity reset to a small negative number rather than 0 when grounded?
  3. What does isGrounded actually report?
  4. A character will not climb your stairs. Which property do you check first, and what range does Unity suggest for a 2 m human?
  5. How do you push a Rigidbody crate with a Character Controller?
  6. When does Unity’s documentation suggest a Rigidbody instead of a Character Controller?

Answers

  1. Move does not apply gravity; you add it yourself. SimpleMove applies gravity automatically and ignores the y component of the speed you pass in.
  2. isGrounded depends on the last Move hitting something below. A small downward velocity keeps the capsule pressed to the floor, so the contact (and isGrounded) stays reliable.
  3. Whether the controller touched the ground during the last Move or SimpleMove call.
  4. Step Offset; Unity suggests 0.1–0.4 for a 2 m human. Also check that the stair risers are lower than that value.
  5. In OnControllerColliderHit, get hit.collider.attachedRigidbody and, if it is not kinematic, set or add velocity in the horizontal direction of hit.moveDirection.
  6. When the player character must be affected by physics, such as forces, knockback or other bodies pushing it.