Lesson 04
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.
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.
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.CameraPivot, add a Camera3D. Move it back and up: position roughly (0, 2, 5) (up 2, back 5 on +Z).-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.
Add camera control to player.gd. We capture the mouse and turn the pivot horizontally, tilt it vertically within limits.
@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 = _pitchClamp 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.
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:
# 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 * SPEEDtransform.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.
# 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_VISIBLECheckpoint — expected state
transform.basis.