Lesson 04

A third-person follow camera

The view is currently fixed. This lesson builds a camera rig that sits behind and above the player, follows smoothly, and turns with the mouse — the classic third-person feel.

▸ By the end of this lesson: A smooth third-person camera steered with the mouse, with the player facing the movement direction

4.1The camera rig pattern

The clean way to do third person: a pivot node at the player's position, with the camera parented to it at an offset. Rotating the pivot orbits the camera. Build it inside player.tscn so it follows automatically.

  1. Open player.tscn. Add a child Node3D to Player, rename it CameraPivot. Leave it at position (0,0,0) — it sits at the player's feet; we'll offset height in code or by raising it to y≈1.5.
  2. Under CameraPivot, add a Camera3D. Move it back and up: position roughly (0, 2, 5) (up 2, back 5 on +Z).
  3. Select the Camera3D and, in the viewport, rotate it slightly downward to look at the player — about -15° on X.

Why a pivot

Parenting the camera to a pivot means you rotate the pivot to look around, and the camera swings with it at a fixed distance — no trigonometry. The pivot lives on the player, so following is automatic.

4.2Mouse look

Add camera control to player.gd. We capture the mouse and turn the pivot horizontally, tilt it vertically within limits.

player.gd (append)gdscript
@onready var camera_pivot: Node3D = $CameraPivot

const MOUSE_SENS: float = 0.005
var _pitch: float = 0.0

func _ready() -> void:
	# Lock the mouse to the window for FPS-style look.
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseMotion:
		# Yaw: turn the whole player left/right.
		rotate_y(-event.relative.x * MOUSE_SENS)
		# Pitch: tilt only the camera pivot up/down, clamped.
		_pitch -= event.relative.y * MOUSE_SENS
		_pitch = clampf(_pitch, deg_to_rad(-60), deg_to_rad(30))
		camera_pivot.rotation.x = _pitch

Clamp the pitch

Without the clampf, looking up or down far enough flips the camera upside-down. Clamping pitch to a sane range (here −60° to +30°) is essential for every mouse-look camera.

4.3Move relative to where the camera faces

Because we now yaw the whole player with the mouse, "forward" should mean "the way the player faces." Update the movement in _physics_process to use the player's own basis:

player.gd (replace direction lines)gdscript
	# Replace the old direction line with camera-relative movement:
	var input_dir: Vector2 = Input.get_vector(
		"move_left", "move_right", "move_forward", "move_back")
	# transform.basis orients the local axes to the player's facing.
	var direction: Vector3 = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	velocity.x = direction.x * SPEED
	velocity.z = direction.z * SPEED

transform.basis

Multiplying a local direction by transform.basis rotates it into world space according to the player's orientation. Pressing forward now always moves in the facing direction — the core of third/first-person movement.

4.4Let Esc free the mouse

player.gd (append to _unhandled_input)gdscript
# add to _unhandled_input, so the cursor can be freed to click away or quit:
	if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
		Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
  1. Run with F6. Move the mouse to look around; the camera orbits behind the player.
  2. WASD now moves relative to facing direction. Esc frees the cursor.

Checkpoint — expected state