Godot 4.x 3D Character Controller Tutorial
Build a Godot 4 3D body that walks across a floor, turns to face where it moves, and falls with gravity, and nothing more.
A character controller can grow into a huge system. Jumping, crouching, climbing, combat, and networking all pile on fast. This guide does none of that.
You will build one movement slice: a body that walks on a floor, turns to face its direction, and falls when there is no ground under it. When it does those three things well, stop. A clean slice is easier to extend than a messy all-in-one controller.
This uses CharacterBody3D and move_and_slide(), both documented in the current Godot 4 class reference. Godot 4 still changes between minor versions, so if a method below is missing in your editor, check the reference for your exact version.
Know what the body does
The Godot docs describe CharacterBody3D as a physics body meant to be moved by script. It is not pushed around by physics the way a rigid body is. Instead, you set its velocity, and move_and_slide() moves it, sliding along walls and floors instead of stopping dead.
That gives you control. Physics will not tip your character over or bounce it off a wall. You decide every frame how it should move.
Two details from the docs matter right away:
- Call
move_and_slide()from_physics_process(). It uses the physics step automatically. - Set
velocityin units per second. Do not multiply it bydeltayourself before callingmove_and_slide(). The docs call that out as a common mistake.
Build a test floor
You need something to walk on. Create a new 3D scene with a Node3D root named TestLevel.
Add a StaticBody3D named Floor. Give it a MeshInstance3D with a wide, thin BoxMesh, and a CollisionShape3D with a matching BoxShape3D. The mesh is what you see. The shape is what the body stands on.
Add a DirectionalLight3D angled down so you can see shadows. Shadows help you judge whether the character is touching the ground.
Place a few boxes around the floor as obstacles. They give you something to slide along and make it easy to see turning.
Build the character scene
Create a new scene with a CharacterBody3D root. Name it Player.
Add these children:
- A
CollisionShape3Dwith aCapsuleShape3D. A capsule slides over small bumps better than a box. - A
Node3DnamedVisual. This holds everything you see. - Under
Visual, aMeshInstance3Dwith aCapsuleMeshsized to match the collision shape. - Also under
Visual, a small box mesh on the front so you can tell which way it faces.
Godot treats negative z as forward. Put the small box on the negative z side of the capsule.
Raise the whole capsule so its bottom sits at the body’s origin, or place the body slightly above the floor in the level. Either works. Just make sure it does not start inside the floor.
Save it as player.tscn and drop an instance into TestLevel.
Set up input actions
Open Project Settings and find the input map. Add four actions:
move_forwardmove_backmove_leftmove_right
Assign keys and, if you like, a gamepad stick to each.
Input-map names differ between projects and tutorials. Some use ui_up and similar built-in actions, others use custom names. Whatever you choose, the strings in your script must match exactly. The menu location can also move between versions, so check the current manual if you cannot find it.
Add a camera that follows
Add a Camera3D as a child of Player. Move it up and back, then tilt it down so the capsule sits near the center of the view.
Because the camera is a child of the body, it follows wherever the body goes. And because you will rotate only Visual, not the body itself, the camera will not spin when the character turns. That keeps the view steady while you test.
Write the movement script
Attach a script to Player:
extends CharacterBody3D
@export var speed := 4.0
@export var turn_speed := 10.0
@onready var visual: Node3D = $Visual
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity += get_gravity() * delta
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var direction := Vector3(input.x, 0.0, input.y)
if direction.length() > 0.0:
velocity.x = direction.x * speed
velocity.z = direction.z * speed
var target_yaw := atan2(-direction.x, -direction.z)
visual.rotation.y = lerp_angle(visual.rotation.y, target_yaw, turn_speed * delta)
else:
velocity.x = move_toward(velocity.x, 0.0, speed)
velocity.z = move_toward(velocity.z, 0.0, speed)
move_and_slide()
Here is what each part does.
Gravity
The first block adds gravity when the body is not on the floor. is_on_floor() reports whether the last move_and_slide() call touched a floor surface.
get_gravity() returns the gravity acting on the body, including the project default and any Area3D overrides. It appears in recent Godot 4 versions. If your version does not have it, read the default 3D gravity from project settings instead, as older tutorials do.
Gravity only touches the y part of velocity. Walking only touches x and z. Keeping them apart stops one from overwriting the other.
Walking
Input.get_vector() takes four action names and returns a 2D direction. It already limits the length, so diagonal movement is not faster than straight movement.
You map that 2D input onto the floor plane: input x becomes world x, and input y becomes world z. Pressing forward gives a negative y, which becomes negative z, which is forward in Godot.
When there is no input, move_toward() brings horizontal speed back to zero. With the values above, the stop is nearly instant. Lower the step if you want a short slide.
Turning
atan2(-direction.x, -direction.z) turns the movement direction into an angle around the y axis. That angle points the negative z face of Visual toward where you are going.
lerp_angle() blends from the current angle to the target and handles the wrap-around at a full turn, so the character never spins the long way. Raise turn_speed for snappier turns, lower it for heavier ones.
Test the slice
Run TestLevel. Check each behavior:
- The character falls and lands on the floor at the start.
- Holding a direction walks it smoothly.
- The nose box turns to face the direction of travel.
- Releasing input stops it.
- Walking into a box slides along it instead of sticking.
- Walking off the edge of the floor makes it fall.
If the body falls through the floor, check that both the floor and the body have collision shapes. If it moves the wrong way, check that your action names match the strings in the script. If it faces backward, move the nose box to the other side.
Stop here
You now have a walking, turning, falling body. That is the slice. Commit it.
When you are ready, add one thing at a time on top: camera-relative movement, slopes, or a simple animation. Test each addition on this same flat floor before you take it anywhere else.