CharacterController won't move: seven checks, cheapest first
Why is my CharacterController not moving in Unity?
Almost every reason a character refuses to walk produces no error, so check in order: whether input reaches the script, whether the component and its capsule are really there, and whether the vector is zero. Then look at where Move() itself refuses — Step Offset, Slope Limit, a trigger used as a floor — and finally at deltaTime, Transform scale, and a Rigidbody claiming the same Transform.
You press W. The character stands there, the walk animation may even be playing, and the Console shows nothing. That is normal: almost every reason a Unity CharacterController refuses to move produces no error at all. Seven groups of causes, cheapest check first — the first three cover most cases.
1. No input ever reaches the script
Every movement script opens the same way: read a number, and if it is not zero, move. Before changing anything, prove that number arrives. Four ordinary things stop it, and none writes an error.
- 1
Play mode, active object. Scripts only run while the game runs, and a greyed-out name in the Hierarchy does nothing at all. The box beside the object name at the top of the Inspector is a separate switch from the one on each component.
- 2
The script compiled. Any red line in the Console, from any script in the project, and Unity is still running your previous code: the new Update is not in it. An unsaved file behaves the same way.
- 3
The right object carries the script. Movement code reads and writes one Transform. On the parent, on the model child, or on the camera it drives the wrong object, which almost always looks like nothing moving.
- 4
Update, not FixedUpdate, and named exactly. Unity calls callbacks by name, so
void update()with a small letter compiles and is never called. Input for movement belongs in Update; only physics work belongs in FixedUpdate.
using UnityEngine;
// Attach it to the SAME object as your movement script, press Play, hold W.
public class MoveProbe : MonoBehaviour
{
CharacterController cc;
void Awake()
{
cc = GetComponent<CharacterController>();
}
void Update()
{
Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
// A green ray out of the character's head, growing as you press.
// It draws in the Scene view, not the Game view.
if (input.sqrMagnitude > 0.001f)
Debug.DrawRay(transform.position + Vector3.up, input * 2f, Color.green);
// Once a second so the Console does not flood. Clicking a line selects
// the object that wrote it: trust this output, not your memory.
if (Time.frameCount % 60 == 0)
Debug.Log(name + " | input " + input
+ " | deltaTime " + Time.deltaTime
+ " | controller " + (cc != null)
+ " | grounded " + (cc != null && cc.isGrounded), this);
}
}Numbers in the Console means the input is fine and the problem is downstream. Nothing at all means the script is not running, and no amount of editing the movement maths will help.
2. The script is off, or a component is missing
The probe stayed silent on an active object. The rest of the search fits inside one Inspector, and while you are there, check which object actually holds the controller: select the character during Play and press F over the Scene view. A green capsule that arrives somewhere other than where the model stands is two different objects.
The checkbox on the component. An unchecked MonoBehaviour is never updated — no input read, no Move call — even though physics messages like OnTriggerEnter still reach it. One click is the entire fix, so look there first.
No CharacterController on this object, so the code holds null and does nothing. A CharacterController is itself a collider and never needs a second one, but it must sit on the same object as the script calling Move. Add [RequireComponent(typeof(CharacterController))] to the class and Unity attaches it for you, and refuses to remove it while the script is there.
The capsule has no volume. Fit it carelessly and Radius or Height ends up 0, or the whole capsule sits under the floor. Move then has nothing to push through the world, and Unity never mentions it. The values that fit a human-sized model in the course are Center 0, 1, 0 with Radius 0.3 and Height 2.
3. The movement vector works out as zero
Input arrives, the component is on, and Move runs every frame — with Vector3.zero. This is the most common cause and the least visible, because zero is a perfectly legal value that no warning fires about.
The speed field was never assigned. public float speed; starts at 0 — Unity gives a number no default of its own, and an Inspector showing 0 is not an error, so the script runs flawlessly at zero metres per second. This single case is most of the CharacterController-will-not-move questions people ask.
Vector3.forward is not transform.forward. The first is the constant (0, 0, 1): world north, forever, whichever way the character faces. The second is where this character actually points. Use world north after turning into a corridor and you press into the wall beside you, which is section 4 wearing a disguise.
Move wants a world-space direction, while transform.Translate defaults to local. Build the vector from the character's own axes with transform.TransformDirection(input); the same line means different things in the two functions, which is how a script copied between them stops making sense.
A guard that never opens: Vector3.zero left in from scaffolding, canMove still false because its animation event never fires, a level-start flag down, a stamina test against a field that is 0. Anything shaped if (!ready) return above the Move line stops movement with no evidence at all.
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerMove : MonoBehaviour
{
// A float with no value stays 0 forever, and nothing about that is red.
// [SerializeField] keeps it visible so you can watch it during Play.
[SerializeField] private float speed = 6f;
[SerializeField] private float gravity = -20f;
CharacterController cc;
float verticalSpeed;
void Awake()
{
cc = GetComponent<CharacterController>();
if (cc == null)
Debug.LogError(name + " has no CharacterController on this object", this);
}
void Update()
{
Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
if (input.sqrMagnitude > 1f) input.Normalize();
// Local input to world direction, along THIS character's facing.
// Vector3.forward here would be world north instead.
Vector3 planar = transform.TransformDirection(input) * speed;
// A CharacterController ignores physics, so gravity is manual.
if (cc.isGrounded && verticalSpeed < 0f) verticalSpeed = -2f;
verticalSpeed += gravity * Time.deltaTime;
// Move takes metres for THIS frame, so deltaTime is multiplied on.
// The return value is what stopped you: Sides is a wall, Below the floor.
CollisionFlags hit = cc.Move((planar + Vector3.up * verticalSpeed) * Time.deltaTime);
if (input.sqrMagnitude > 0.001f && (hit & CollisionFlags.Sides) != 0)
Debug.Log(name + " is pressing into something: " + hit, this);
}
}4. Move is called, and collision blocks it
A CharacterController only goes where its capsule fits. When it cannot, it returns the reason as CollisionFlags, and almost nobody reads that value. Capture it once and a lot of mystery goes away: Sides is a wall, Below is the floor, Above is a ceiling.
Step Offset, 0.3 by default. Anything taller is a wall, silently and completely — a 0.35 metre crate stops the character dead, with the Sides flag as the only sign. Raise it a little or add a ramp, and remember a step needs headroom: a low beam blocks the climb.
Slope Limit, 45 degrees by default. A hill steeper than that is refused outright: input still reads, Move still runs, nothing happens. A terrain edge painted too steep is the usual culprit.
Skin Width, 0.08 by default — a margin around the capsule. Too large and the character cannot enter a doorway narrower than about two skin widths, and hovers above the floor without touching it, which makes isGrounded flicker. Too small and it snags on the seams between meshes.
Standing on a trigger. A CharacterController walks straight through any collider with Is Trigger ticked, so a floor authored that way holds nothing: isGrounded is false forever and a script gated on if (!cc.isGrounded) return never calls Move at all.
Spawned inside geometry. If the capsule starts the game overlapping a wall, Move spends every frame pushing you out of the surface you are stuck in and the character wriggles against nothing. Look at the start position in the Scene view before it moves.
5. Time.deltaTime multiplied or divided wrong
Speed is metres per second; Move wants metres for this frame. deltaTime is the conversion between them, and the two directions of the mistake look nothing alike on screen.
// speed = 6, running at 60 fps, so Time.deltaTime is about 0.0166.
cc.Move(input * speed); // 6 metres EVERY frame: 360 m/s
cc.Move(input * speed / Time.deltaTime); // 360 metres per frame: gone by frame 2
cc.Move(input * speed * Time.deltaTime); // 0.1 m this frame, which is 6 m/s — correctA speed that is too small is not stuck, just slow — and at game scale the two are indistinguishable. speed = 0.001 is one millimetre per second, about a hundred minutes to cross a six-metre room. In Unity's units 2 is a walk and 6 is a run.
Dividing instead of multiplying launches the character at thousands of metres per second, usually with the camera tearing behind it. Dropping deltaTime entirely gives movement that depends on the frame rate: correct on your 60 Hz monitor, twice as fast on a 120 Hz phone.
Time.timeScale = 0 makes Time.deltaTime exactly zero, so every multiplication by it is zero too. A pause menu that sets timeScale and never restores it freezes the character while the animation keeps playing, because an Animator on unscaled time does not read timeScale.
6. The character turns but never goes forward
Rotation working is already an answer: the input arrives, the script runs, and only the forward step is missing. That combination points at the Transform's scale.
A CharacterController is not built to be scaled; Unity expects 1, 1, 1 on its Transform. Scale the root or any parent and the capsule no longer matches what the movement maths assumes — the character ends up welded to the floor or a wall, while rotation, which is unaffected, keeps working.
localScale with 0 on any axis, or a negative value from a mirrored model, leaves the capsule with no volume at all. This usually arrives through import — a centimetre-based file kept at Scale Factor 0.01, or a parent set to (-1, 1, 1) to flip the mesh. Set it on the Model tab of the importer instead, or keep the controller on an unscaled root and make the scaled mesh its child.
The parent is rotated and the character is a child with its own local rotation, so transform.forward returns the parent's facing and forward is a direction nobody looks at. Flatten the hierarchy: the object with the CharacterController and the object being rendered should share one rotation, not two multiplied together.
7. A Rigidbody, or a second controller, claiming the same Transform
A CharacterController does its own collision and its own motion. Anything else that writes to the same Transform competes with it, and the character ends up listening to neither.
Rigidbody and CharacterController on the same object. Physics pulls you into the floor on its own schedule while your script pushes with its own gravity in Update, and the character sinks, trembles, or stays welded however hard you press W. Keep one; for a player, the CharacterController.
A Kinematic Rigidbody beside the controller is a deliberate choice for shoving crates — and a two-owners problem the moment another script also drives it. If anything writes rb.velocity or AddForce to that body, or calls Rigidbody.MovePosition while Move is also running, the position has two authors again.
A second CharacterController does not collide with yours — they walk straight through each other — so an NPC cannot block you, but a static wall, a closed door, or a crate with a Rigidbody can, and the thing that stopped you may be behind the camera. Travel the Scene view to the character and see what the capsule is touching.
isGrounded never passes. A guard shaped if (!cc.isGrounded) return refuses every command without a trace: the ground is a trigger, the capsule bottom floats above the floor after an edited Center or Height, or the ground sits on a layer unticked against the character's in Edit > Project Settings > Physics. Log cc.isGrounded while you press W — one line, one answer.
The 90-second order
- ✓No red errors in the Console — otherwise Unity is still running your previous code.
- ✓Play mode running, the object active, the script component ticked.
- ✓MoveProbe prints numbers while you hold W. Silence means sections 1 and 2.
- ✓speed in the Inspector is a non-zero number you recognise, checked during Play.
- ✓The green capsule wraps the character and rests on the floor — not inside it, not floating.
- ✓Time.deltaTime is multiplied, never divided.
- ✓The object's scale and every parent's are (1, 1, 1) — no zero, no negative mirror.
- ✓Exactly one thing moves this Transform: the CharacterController, or a Rigidbody — not both.
- ✓cc.isGrounded logs true while standing still, and nothing taller than Step Offset or steeper than Slope Limit lies ahead.
When the character walks again, the next thing you notice is what happens on contact, and that is collisions and triggers. For the shape movement itself should have — camera-relative input, gravity, a smooth turn — see how to move a character.
the lesson that builds this
Movement, jumping and landing on blocks
This post is a standalone recipe. In the course, the same thing is built as part of the one project that runs through all five tabs — Character & Combat, lesson 02.