Lesson 03

The player: a CharacterBody3D that moves

Now we build the player as a proper physics character — a capsule that walks on the ground, respects gravity, and moves relative to input. This is the CharacterBody3D pattern you'll reuse in every 3D project.

▸ By the end of this lesson: A capsule player that walks around the floor and falls under gravity

3.1Build the Player scene

  1. New Scene → "Other Node" → CharacterBody3D as root. Rename it Player. Save as player.tscn.
  2. Add a child MeshInstance3D → New CapsuleMesh. Give it a bright material so you can see it.
  3. Add a child CollisionShape3D → New CapsuleShape3D. Match its height/radius to the mesh (defaults line up closely).

CharacterBody3D

The go-to node for player-controlled characters. Unlike a RigidBody, you drive it — you set velocity and call move_and_slide(), and it handles collision response (sliding along walls, stopping at floors). Predictable and easy to tune.

3.2The movement script

Attach a script player.gd to the root Player node:

player.gdgdscript
extends CharacterBody3D

const SPEED: float = 6.0
const GRAVITY: float = 20.0

func _physics_process(delta: float) -> void:
	# Apply gravity when airborne.
	if not is_on_floor():
		velocity.y -= GRAVITY * delta

	# Read movement input as a 2D vector (x = strafe, y = forward/back).
	var input_dir: Vector2 = Input.get_vector(
		"move_left", "move_right", "move_forward", "move_back")

	# Convert to a 3D direction on the XZ plane.
	# input_dir.y maps to -Z because forward is -Z.
	var direction: Vector3 = Vector3(input_dir.x, 0, input_dir.y)
	direction = direction.normalized()

	velocity.x = direction.x * SPEED
	velocity.z = direction.z * SPEED

	move_and_slide()

_physics_process, not _process

Physics and movement go in _physics_process(delta), which runs at a fixed rate. Using _process for physics gives inconsistent movement on different framerates. Rule of thumb: anything touching velocity or move_and_slide() goes in _physics_process.

3.3Set up the movement actions

  1. Project Settings → Input Map. Add actions move_forward (W/Up), move_back (S/Down), move_left (A/Left), move_right (D/Right), and jump (Space).

Input.get_vector

Input.get_vector(left, right, up, down) returns a Vector2 already normalized for diagonals, so moving diagonally isn't faster than moving straight — a subtle bug you'd otherwise have to fix by hand.

3.4Add a jump

Extend player.gd with a jump before the move_and_slide() call:

player.gd (insert)gdscript
const JUMP_VELOCITY: float = 8.0

# ...inside _physics_process, after gravity, before move_and_slide():
	if is_on_floor() and Input.is_action_just_pressed("jump"):
		velocity.y = JUMP_VELOCITY

3.5Put the player in the world

  1. Open main.tscn. Drag player.tscn from the FileSystem dock into the scene tree under Main (or Instance Child Scene).
  2. Move the player above the floor (y ≈ 1) so it drops onto the ground when the game starts.
  3. Run with F6. The capsule falls, lands, and you can walk it around with WASD and jump with Space.

Falling forever

If the player falls through the floor, your ground is missing its CollisionShape3D, or the player's collision capsule is misaligned. Both bodies need collision shapes that actually overlap.

Checkpoint — expected state