CharacterController vs Rigidbody in Unity: Build a Player Controller

Illustration of two capsule characters comparing physics and controller movement

Every Unity project with a walking character hits the same fork in the road on day one: use the built-in CharacterController component, or put a Rigidbody on the player and move it with physics? Both work, both are used in shipped games, and picking the wrong one means fighting the engine for the rest of the project. This guide explains how each one actually moves, gives you a clear way to choose, and then builds a complete, working controller with each approach in Unity 6, including jumping, gravity, slopes and the bugs that show up in the first week.

Watch it in action: CharacterController vs Rigidbody side by side on steps, slopes, crates and explosions (3:51).


The Core Difference

A CharacterController is a capsule that you move. You call Move() with how far to go this frame, and it slides along walls, climbs steps and stops at obstacles. It is not a physics body: gravity, forces and momentum don’t exist unless you write them. That makes it predictable and precise.

A Rigidbody is moved by the physics engine. You give it velocity or forces, and PhysX integrates them along with gravity, friction, collisions and every other body in the scene. That makes it interact naturally with the world, but you’re steering a simulation rather than placing a character.

In short: CharacterController is kinematic movement with collision. Rigidbody is simulated movement.

Side by side Unity scene: an explosion pushes a Rigidbody player away while a CharacterController player next to it does not move.
Left: a Rigidbody is moved by physics, so the explosion throws it. Right: a CharacterController ignores forces unless you code them.

Side-by-Side Comparison

Feature CharacterController Rigidbody
Gravity You write it Built in
Steps and stairs Built in (Step Offset) You handle it (ramps, step-up code)
Slopes Slope Limit, stops cleanly Slides down unless you counter it
Pushed by explosions, moving objects No (you fake it) Yes, naturally
Pushes other Rigidbodies Only via OnControllerColliderHit Yes, naturally
Moving platforms You add platform velocity Friction helps, still needs work
Responsiveness Instant, snappy Can feel floaty without tuning
Where to move it Update FixedUpdate
Collision shape Upright capsule only Any collider
Best fit FPS, third-person action, platformers with tight control Physics games, ragdolls, vehicles, rolling balls, sandbox
Side by side Unity scene: a CharacterController player at the top of a staircase and a Rigidbody player stuck at the first step.
Same input: the CharacterController climbs with Step Offset, the Rigidbody stops at the first step.
Side by side Unity scene: a CharacterController player standing still on a ramp and a Rigidbody player sliding down the same ramp.
On a slope the CharacterController stands still, while the zero-friction Rigidbody slides downhill.

Which One Should You Use?

A practical rule of thumb:

  • Use CharacterController if the player is a person who walks, runs and jumps, and you want the controls to feel exact. Most first-person and third-person games fall here. You’ll write a little gravity code, and in return you never fight sliding, bouncing or jitter.
  • Use Rigidbody if physics is the gameplay: the player gets knocked around, rides physics vehicles, swings on ropes, is a ball or a ragdoll, or must push and stack crates realistically.
  • Use neither for top-down or grid games that only need trigger checks. A kinematic Rigidbody moved with MovePosition, or plain transform movement with overlap checks, is simpler.

Don’t put both on the same object. A CharacterController plus a non-kinematic Rigidbody fight each other every frame and produce the jitter and teleporting that fill forum threads.

Build a CharacterController Player

Add a CharacterController to your player (it replaces the Capsule Collider; remove that). Set Height 2, Radius 0.5, Center Y 1 if the pivot is at the feet, Step Offset 0.3, Slope Limit 45, and Skin Width around 10% of the radius (0.05).

This script uses the Input System with a PlayerInput-free approach (input actions referenced directly) so it works in a fresh Unity 6 project:

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(CharacterController))]
public class CCPlayer : MonoBehaviour
{
    [SerializeField] InputActionReference move;
    [SerializeField] InputActionReference jump;
    [SerializeField] float speed = 6f;
    [SerializeField] float jumpHeight = 1.2f;
    [SerializeField] float gravity = -20f;

    CharacterController cc;
    Vector3 velocity; // only Y is used for gravity and jumping

    void Awake() => cc = GetComponent<CharacterController>();
    void OnEnable() { move.action.Enable(); jump.action.Enable(); }
    void OnDisable() { move.action.Disable(); jump.action.Disable(); }

    void Update()
    {
        bool grounded = cc.isGrounded;
        if (grounded && velocity.y < 0f)
            velocity.y = -2f; // keep a little downward push so isGrounded stays true

        Vector2 input = move.action.ReadValue<Vector2>();
        Vector3 horizontal = (transform.right * input.x + transform.forward * input.y) * speed;

        if (grounded && jump.action.WasPressedThisFrame())
            velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);

        velocity.y += gravity * Time.deltaTime;

        // one Move call per frame: horizontal + vertical together
        cc.Move((horizontal + velocity) * Time.deltaTime);
    }
}

Three details make this work:

  • One Move call per frame. isGrounded reports the result of the last Move. Calling Move twice (once for horizontal, once for gravity) makes it flicker between true and false.
  • The small -2 downward velocity on the ground keeps the capsule pressed into the floor, so isGrounded doesn’t drop out on bumps and downhill slopes.
  • The jump formula sqrt(h * -2 * g) gives the exact launch speed to reach jumpHeight metres, so you tune in metres rather than magic numbers.

Don’t call SimpleMove in the same script. It applies its own gravity, ignores the Y component you pass and makes jumping impossible. It’s only useful for the simplest NPC wandering.

Build a Rigidbody Player

Add a Rigidbody and a Capsule Collider. On the Rigidbody, set Interpolate to Interpolate, Collision Detection to Continuous (or Continuous Dynamic for fast players), and under Constraints freeze rotation X, Y and Z so the capsule doesn’t tip over. Create a Physics Material with Dynamic and Static Friction 0 and Friction Combine Minimum, and put it on the collider so the player doesn’t stick to walls.

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody))]
public class RBPlayer : MonoBehaviour
{
    [SerializeField] InputActionReference move;
    [SerializeField] InputActionReference jump;
    [SerializeField] float speed = 6f;
    [SerializeField] float acceleration = 50f;
    [SerializeField] float jumpHeight = 1.2f;
    [SerializeField] LayerMask groundMask;

    Rigidbody rb;
    Vector2 input;
    bool jumpQueued;

    void Awake() => rb = GetComponent<Rigidbody>();
    void OnEnable() { move.action.Enable(); jump.action.Enable(); }
    void OnDisable() { move.action.Disable(); jump.action.Disable(); }

    void Update()
    {
        // read input every frame so no presses are missed
        input = move.action.ReadValue<Vector2>();
        if (jump.action.WasPressedThisFrame()) jumpQueued = true;
    }

    void FixedUpdate()
    {
        bool grounded = Physics.SphereCast(rb.position + Vector3.up * 0.5f, 0.45f,
                                           Vector3.down, out _, 0.1f, groundMask,
                                           QueryTriggerInteraction.Ignore);

        Vector3 target = (transform.right * input.x + transform.forward * input.y) * speed;
        Vector3 current = rb.linearVelocity;
        Vector3 change = new Vector3(target.x - current.x, 0f, target.z - current.z);
        change = Vector3.ClampMagnitude(change, acceleration * Time.fixedDeltaTime);
        rb.AddForce(change, ForceMode.VelocityChange);

        if (jumpQueued && grounded)
        {
            float jumpSpeed = Mathf.Sqrt(jumpHeight * -2f * Physics.gravity.y);
            rb.linearVelocity = new Vector3(rb.linearVelocity.x, jumpSpeed, rb.linearVelocity.z);
        }
        jumpQueued = false;
    }
}

Notes on this version:

  • In Unity 6, Rigidbody.velocity was renamed to linearVelocity (and drag to linearDamping). Old tutorials still use velocity, which now gives an obsolete warning.
  • Input is read in Update and applied in FixedUpdate. Reading WasPressedThisFrame inside FixedUpdate misses presses, because FixedUpdate doesn’t run every frame.
  • Steering velocity toward a target with a capped VelocityChange gives snappy control while still letting explosions and collisions push the player, unlike setting linearVelocity directly every step.
  • The ground check is a short SphereCast, which is more forgiving on edges than a single ray. Our raycast guide explains masks and shape casts in detail.

If the camera follows this player and the image stutters, it’s almost always interpolation or the camera updating at the wrong time. See jittery Rigidbody movement for the full fix.

Unity Inspector for a Rigidbody player with Interpolate, Continuous Dynamic collision detection and Freeze Rotation X, Y and Z set.
The Rigidbody player: Interpolate, Continuous collision detection and rotation frozen on X, Y and Z.

Camera-Relative Movement

Both scripts move relative to the player’s own facing (transform.forward), which suits first-person. For third-person, “up” on the stick should mean “away from the camera”. Flatten the camera’s vectors onto the ground plane:

Vector3 camForward = Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up).normalized;
Vector3 camRight = Vector3.ProjectOnPlane(cam.transform.right, Vector3.up).normalized;
Vector3 moveDir = camRight * input.x + camForward * input.y;

if (moveDir.sqrMagnitude > 0.01f)
    transform.rotation = Quaternion.Slerp(transform.rotation,
        Quaternion.LookRotation(moveDir), 12f * Time.deltaTime);

Use moveDir * speed in place of the transform.right/forward line. This pairs well with a Cinemachine orbit camera; our Cinemachine 3 third-person camera guide sets one up.

📚 Want to understand game feel properly?

Acceleration curves, coyote time and jump buffering are covered brilliantly in a couple of the game design books on our list.

See the best books for game developers →

Pushing Objects and Being Pushed

A CharacterController walks into Rigidbody crates and simply stops, because it isn’t part of the simulation. Push them yourself in OnControllerColliderHit:

[SerializeField] float pushPower = 2f;

void OnControllerColliderHit(ControllerColliderHit hit)
{
    Rigidbody body = hit.collider.attachedRigidbody;
    if (body == null || body.isKinematic) return;
    if (hit.moveDirection.y < -0.3f) return; // don't push things we're standing on

    Vector3 push = new Vector3(hit.moveDirection.x, 0f, hit.moveDirection.z);
    body.linearVelocity = push * pushPower;
}

Being pushed (knockback, explosions, wind) is the reverse: add an externalVelocity vector to the controller, include it in Move, and decay it toward zero each frame. With a Rigidbody player, AddForce or AddExplosionForce just works, which is a big reason physics-heavy games pick Rigidbody.

Side by side Unity scene: a CharacterController player stopped against a crate stack, and the same player pushing the crates with OnControllerColliderHit.
Left: a CharacterController just stops at a crate. Right: OnControllerColliderHit pushes it.

Troubleshooting

CharacterController isGrounded keeps flickering

You’re calling Move more than once per frame, or you reset vertical velocity to exactly 0 on the ground. Combine movement into one Move call and keep a small negative Y velocity (around -2) while grounded.

CharacterController falls through the floor or teleports

Something else is setting transform.position (an Animator with Apply Root Motion, a respawn script, a parent). Setting the position directly while a CharacterController is enabled gets overwritten. Disable the controller, set the position, then re-enable it.

The player gets stuck on the edges of steps or walls

For CharacterController, raise Step Offset slightly and make sure Skin Width isn’t tiny. For Rigidbody, add the zero-friction Physics Material described above.

Rigidbody player slides down slopes

Gravity pulls it along the slope. When grounded and not giving input, set the horizontal velocity to zero, or project movement onto the ground normal and temporarily disable gravity (rb.useGravity = false) while standing on a walkable slope.

Rigidbody player bounces when landing or hitting walls

The collider’s Physics Material has bounciness above 0, or the player hits walls at high speed with Discrete collision. Set Bounciness to 0 and Collision Detection to Continuous.

Rigidbody player tips over or spins

Rotation constraints aren’t frozen. Freeze rotation X, Y and Z on the Rigidbody, then rotate the player yourself with rb.MoveRotation.

FAQ

Is CharacterController better than Rigidbody?

Neither is better overall. CharacterController gives tight, predictable control for walking characters. Rigidbody gives natural physical interaction. Pick based on whether physics is part of your gameplay.

Can I use CharacterController and Rigidbody together?

Not on the same object with a non-kinematic Rigidbody; they conflict. A kinematic Rigidbody alongside a CharacterController is allowed and only helps if you need trigger callbacks with certain setups.

Does CharacterController use gravity?

Only SimpleMove applies gravity. With Move you add gravity yourself by accumulating a vertical velocity each frame, as in the script above.

Why use FixedUpdate for Rigidbody movement?

Physics runs on a fixed timestep. Applying forces in FixedUpdate keeps movement consistent at any frame rate. Read input in Update and apply it in FixedUpdate.

Does a CharacterController trigger OnTriggerEnter?

Yes. A CharacterController counts as a collider for trigger events, so trigger zones work without adding a Rigidbody to the player.

What did Unity 6 change for Rigidbody movement?

The velocity and drag properties were renamed to linearVelocity and linearDamping (angularDrag became angularDamping). Behaviour is the same; old names show obsolete warnings.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *