A simple rounded clay figure mid-stride
| |

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 velocity in units per second. Do not multiply it by delta yourself before calling move_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 CollisionShape3D with a CapsuleShape3D. A capsule slides over small bumps better than a box.
  • A Node3D named Visual. This holds everything you see.
  • Under Visual, a MeshInstance3D with a CapsuleMesh sized 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_forward
  • move_back
  • move_left
  • move_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.

Similar Posts

Leave a Reply

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