Camera systems
The camera is the player's window; bad camera work makes a good game feel awful. This skill
covers the engine-neutral camera techniques — smooth follow, deadzones, look-ahead, bounds
clamping, third-person orbit with collision, first-person look, and multi-target framing — and
maps them onto each engine's camera node or rig.
When to use
- Use when a 2D camera should follow the player smoothly, stay inside the level, lead the
player's motion, or ignore small movements (deadzone).
- Use when building a 3D third-person orbit camera (mouse/stick look, collision push-in) or a
first-person look controller, or framing multiple targets at once.
- Use to fix camera jitter, snapping, motion sickness, or a camera that shows past the level edge.
When not to use: for the magnitude and trigger of screen shake and impact juice, use
game-feel (this skill exposes the shake offset hook it drives). For the engine's concrete
camera node/component setup, use godot-3d-essentials (Camera3D, environment) or the engine
skill. For player movement itself use the engine movement skill (godot-2d-movement). For
performance of many cameras/render targets, see performance-optimization.
Core workflow
- Decide what the camera serves. Platformer (lead the jump, see hazards), top-down (center
with deadzone), third-person (orbit + collision), first-person (look only). The genre sets the
rules.
- Follow smoothly and frame-rate independently. Move the camera toward the target with
exponential smoothing or a spring (
SmoothDamp), not a fixed lerp(a, b, 0.1) — that 0.1 is
per-frame and changes with frame rate.
- Add a deadzone so tiny target movements don't nudge the camera; it only follows once the
target leaves a box/zone. Stops nausea in twitchy games.
- Lead the action with look-ahead by offsetting the camera target in the direction of
motion or facing, eased in/out so it doesn't whip.
- Clamp to level bounds so the camera never shows outside the playable area; combine with
smoothing so it eases to a stop at the edge.
- For 3D, separate look from collision. Orbit via yaw/pitch on a rig; use a spring arm /
ray to pull the camera in when geometry blocks it; clamp pitch.
- Update the camera after the target moves. Follow in the late/post step (after movement and
physics resolve) to avoid a one-frame lag jitter.
- Verify by moving the target at low and high frame rates, into corners and walls, and at
the level edges; confirm no jitter, no peeking past bounds, smooth stops. Report what you saw.
Patterns
1. Godot 2D built-in follow: smoothing + bounds (don't hand-roll first)
# Godot 4.7 Camera2D. Engine-provided smoothing + hard limits + drag margins.
@onready var cam := $Camera2D
func _ready() -> void:
cam.make_current()
cam.position_smoothing_enabled = true
cam.position_smoothing_speed = 6.0 # higher = snappier; lower = floatier
cam.limit_left = 0; cam.limit_top = 0 # clamp to the level rect (pixels)
cam.limit_right = level_width; cam.limit_bottom = level_height
cam.drag_horizontal_enabled = true # built-in deadzone via drag margins
2. Frame-rate-independent smooth follow (when you hand-roll it)
# RIGHT: exponential smoothing — same feel at any FPS. `rate` ~ 5..12.
func _follow(dt: float) -> void:
var t := 1.0 - exp(-rate * dt) # converges correctly regardless of dt
global_position = global_position.lerp(target.global_position, t)
# WRONG: global_position = global_position.lerp(target.global_position, 0.1)
# → faster smoothing at higher FPS; different feel on every machine.
# Unity 6.3 LTS: Vector3.SmoothDamp(transform.position, target.position, ref vel, smoothTime) in
# LateUpdate gives the same spring behavior with built-in frame-rate correction.
3. Deadzone + look-ahead (lead the player, ignore jitter)
# Camera only chases once the target leaves the deadzone box, then aims AHEAD of motion.
func _camera_target(dt: float) -> Vector2:
var to := target.global_position - _focus
var dz := deadzone_half_extents # e.g. Vector2(48, 32)
# Only move the focus by the overflow beyond the deadzone (per axis).
_focus.x += clampf(absf(to.x) - dz.x, 0, INF) * signf(to.x)
_focus.y += clampf(absf(to.y) - dz.y, 0, INF) * signf(to.y)
var lead := target.velocity.normalized() * look_ahead_dist # aim ahead of travel
return _focus + lead
4. 3D third-person orbit with collision push-in
# Godot 4.7. Yaw/pitch a pivot; a SpringArm3D auto-pulls the camera in when blocked.
func _unhandled_input(e):
if e is InputEventMouseMotion:
_yaw -= e.relative.x * sensitivity
_pitch = clampf(_pitch - e.relative.y * sensitivity, -1.2, 0.4) # clamp pitch!
func _process(_dt):
pivot.rotation = Vector3(_pitch, _yaw, 0)
# $SpringArm3D handles wall collision: set spring_length + collision_mask; the child
# Camera3D slides in automatically. RIGHT: spring arm. WRONG: camera clips through walls.
# Unity 6.3 LTS: a Cinemachine 3 CinemachineCamera (namespace Unity.Cinemachine) with an Orbital
# Follow + Cinemachine Deoccluder; the CinemachineBrain on the Camera blends automatically.
5. Screen shake hook (owned trigger lives in game-feel)
# Expose an additive offset the game-feel trauma model writes to; follow + shake compose.
var shake_offset := Vector2.ZERO # set each frame by game-feel (trauma^2 * noise)
func _apply(final_focus: Vector2) -> void:
global_position = final_focus + shake_offset # shake rides ON TOP of smooth follow
# Unity Cinemachine: add a CinemachineBasicMultiChannelPerlin and set amplitude from trauma.
Pitfalls
lerp(pos, target, const) per frame is frame-rate dependent — floatier at 30 FPS, snappier
at 144. Use 1 - exp(-rate*dt) or SmoothDamp.
- Following in the normal update before the target has moved yields a one-frame lag jitter.
Follow in
LateUpdate / after movement/physics resolve.
- No bounds clamp lets the camera show black past the level edge. Clamp focus to the level
rect (account for the viewport half-size so the view, not the center, stays inside).
- No deadzone in twitchy games makes the camera twitch with every micro-movement → nausea.
- Unclamped pitch in third/first-person flips the camera over the top. Clamp pitch to ~±80°.
- Camera clipping through walls in 3D — use a spring arm / occlusion ray to pull in.
- Snapping on teleport/respawn is jarring; either hard-cut intentionally (and reset
smoothing) or fast-ease. Don't let a huge
SmoothDamp distance whip across the level.
- Shake driving the follow target instead of an additive offset makes follow fight shake.
Compose: smooth follow first, add shake offset last.
- Per-axis vs radial deadzone confusion — a box deadzone feels different from a circular one;
pick deliberately.
References
- For the exponential-smoothing/spring derivation, a complete deadzone+look-ahead+bounds 2D rig,
3D spring-arm/orbit details, first-person look, multi-target/group framing and split-screen,
cinematic camera blends, and the Cinemachine 3 / Godot Camera2D / PhantomCamera mapping, read
references/follow-and-framing.md.
Related skills
game-feel — owns screen-shake trauma/triggers; this skill exposes the offset it writes.
godot-2d-movement, godot-3d-essentials — the player/world the camera frames; Camera3D setup.
physics-tuning — interpolate camera follow with the physics step to kill jitter.
platformer, fps-shooter — genres whose camera rules this skill implements.
performance-optimization — cost of extra cameras, render targets, and split-screen.
1---
2name: camera-systems
3description: >
4 Build game cameras that feel good — 2D follow with a deadzone, look-ahead, smoothing, and
5 level-bounds clamping; 3D third-person orbit with collision and first-person look; plus
6 multi-target framing and a shake hook. Engine-neutral techniques that pair with the engine's
7 camera node and rigs like Unity Cinemachine or Godot Camera2D/PhantomCamera. Use when the user
8 mentions camera follow, follow camera, deadzone, look-ahead, camera smoothing, camera bounds/
9 limits, third-person camera, orbit camera, first-person look, Cinemachine, or camera jitter.
10---
11
12# Camera systems
13
14The camera is the player's window; bad camera work makes a good game feel awful. This skill
15covers the engine-neutral camera techniques — smooth follow, deadzones, look-ahead, bounds
16clamping, third-person orbit with collision, first-person look, and multi-target framing — and
17maps them onto each engine's camera node or rig.
18
19## When to use
20
21- Use when a 2D camera should follow the player smoothly, stay inside the level, lead the
22 player's motion, or ignore small movements (deadzone).
23- Use when building a 3D third-person orbit camera (mouse/stick look, collision push-in) or a
24 first-person look controller, or framing multiple targets at once.
25- Use to fix camera jitter, snapping, motion sickness, or a camera that shows past the level edge.
26
27**When *not* to use:** for the *magnitude and trigger* of screen shake and impact juice, use
28`game-feel` (this skill exposes the shake offset hook it drives). For the engine's concrete
29camera node/component setup, use `godot-3d-essentials` (Camera3D, environment) or the engine
30skill. For player movement itself use the engine movement skill (`godot-2d-movement`). For
31performance of many cameras/render targets, see `performance-optimization`.
32
33## Core workflow
34
351. **Decide what the camera serves.** Platformer (lead the jump, see hazards), top-down (center
36 with deadzone), third-person (orbit + collision), first-person (look only). The genre sets the
37 rules.
382. **Follow smoothly and frame-rate independently.** Move the camera toward the target with
39 exponential smoothing or a spring (`SmoothDamp`), not a fixed `lerp(a, b, 0.1)` — that 0.1 is
40 per-frame and changes with frame rate.
413. **Add a deadzone** so tiny target movements don't nudge the camera; it only follows once the
42 target leaves a box/zone. Stops nausea in twitchy games.
434. **Lead the action with look-ahead** by offsetting the camera target in the direction of
44 motion or facing, eased in/out so it doesn't whip.
455. **Clamp to level bounds** so the camera never shows outside the playable area; combine with
46 smoothing so it eases to a stop at the edge.
476. **For 3D, separate look from collision.** Orbit via yaw/pitch on a rig; use a spring arm /
48 ray to pull the camera in when geometry blocks it; clamp pitch.
497. **Update the camera after the target moves.** Follow in the late/post step (after movement and
50 physics resolve) to avoid a one-frame lag jitter.
518. **Verify by moving the target at low and high frame rates**, into corners and walls, and at
52 the level edges; confirm no jitter, no peeking past bounds, smooth stops. Report what you saw.
53
54## Patterns
55
56### 1. Godot 2D built-in follow: smoothing + bounds (don't hand-roll first)
57
58```gdscript
59# Godot 4.7 Camera2D. Engine-provided smoothing + hard limits + drag margins.
60@onready var cam := $Camera2D
61func _ready() -> void:
62 cam.make_current()
63 cam.position_smoothing_enabled = true
64 cam.position_smoothing_speed = 6.0 # higher = snappier; lower = floatier
65 cam.limit_left = 0; cam.limit_top = 0 # clamp to the level rect (pixels)
66 cam.limit_right = level_width; cam.limit_bottom = level_height
67 cam.drag_horizontal_enabled = true # built-in deadzone via drag margins
68```
69
70### 2. Frame-rate-independent smooth follow (when you hand-roll it)
71
72```gdscript
73# RIGHT: exponential smoothing — same feel at any FPS. `rate` ~ 5..12.
74func _follow(dt: float) -> void:
75 var t := 1.0 - exp(-rate * dt) # converges correctly regardless of dt
76 global_position = global_position.lerp(target.global_position, t)
77# WRONG: global_position = global_position.lerp(target.global_position, 0.1)
78# → faster smoothing at higher FPS; different feel on every machine.
79# Unity 6.3 LTS: Vector3.SmoothDamp(transform.position, target.position, ref vel, smoothTime) in
80# LateUpdate gives the same spring behavior with built-in frame-rate correction.
81```
82
83### 3. Deadzone + look-ahead (lead the player, ignore jitter)
84
85```gdscript
86# Camera only chases once the target leaves the deadzone box, then aims AHEAD of motion.
87func _camera_target(dt: float) -> Vector2:
88 var to := target.global_position - _focus
89 var dz := deadzone_half_extents # e.g. Vector2(48, 32)
90 # Only move the focus by the overflow beyond the deadzone (per axis).
91 _focus.x += clampf(absf(to.x) - dz.x, 0, INF) * signf(to.x)
92 _focus.y += clampf(absf(to.y) - dz.y, 0, INF) * signf(to.y)
93 var lead := target.velocity.normalized() * look_ahead_dist # aim ahead of travel
94 return _focus + lead
95```
96
97### 4. 3D third-person orbit with collision push-in
98
99```gdscript
100# Godot 4.7. Yaw/pitch a pivot; a SpringArm3D auto-pulls the camera in when blocked.
101func _unhandled_input(e):
102 if e is InputEventMouseMotion:
103 _yaw -= e.relative.x * sensitivity
104 _pitch = clampf(_pitch - e.relative.y * sensitivity, -1.2, 0.4) # clamp pitch!
105func _process(_dt):
106 pivot.rotation = Vector3(_pitch, _yaw, 0)
107 # $SpringArm3D handles wall collision: set spring_length + collision_mask; the child
108 # Camera3D slides in automatically. RIGHT: spring arm. WRONG: camera clips through walls.
109# Unity 6.3 LTS: a Cinemachine 3 CinemachineCamera (namespace Unity.Cinemachine) with an Orbital
110# Follow + Cinemachine Deoccluder; the CinemachineBrain on the Camera blends automatically.
111```
112
113### 5. Screen shake hook (owned trigger lives in `game-feel`)
114
115```gdscript
116# Expose an additive offset the game-feel trauma model writes to; follow + shake compose.
117var shake_offset := Vector2.ZERO # set each frame by game-feel (trauma^2 * noise)
118func _apply(final_focus: Vector2) -> void:
119 global_position = final_focus + shake_offset # shake rides ON TOP of smooth follow
120# Unity Cinemachine: add a CinemachineBasicMultiChannelPerlin and set amplitude from trauma.
121```
122
123## Pitfalls
124
125- **`lerp(pos, target, const)` per frame** is frame-rate dependent — floatier at 30 FPS, snappier
126 at 144. Use `1 - exp(-rate*dt)` or `SmoothDamp`.
127- **Following in the normal update before the target has moved** yields a one-frame lag jitter.
128 Follow in `LateUpdate` / after movement/physics resolve.
129- **No bounds clamp** lets the camera show black past the level edge. Clamp focus to the level
130 rect (account for the viewport half-size so the *view*, not the center, stays inside).
131- **No deadzone in twitchy games** makes the camera twitch with every micro-movement → nausea.
132- **Unclamped pitch** in third/first-person flips the camera over the top. Clamp pitch to ~±80°.
133- **Camera clipping through walls** in 3D — use a spring arm / occlusion ray to pull in.
134- **Snapping on teleport/respawn** is jarring; either hard-cut intentionally (and reset
135 smoothing) or fast-ease. Don't let a huge `SmoothDamp` distance whip across the level.
136- **Shake driving the follow target** instead of an additive offset makes follow fight shake.
137 Compose: smooth follow first, add shake offset last.
138- **Per-axis vs radial deadzone confusion** — a box deadzone feels different from a circular one;
139 pick deliberately.
140
141## References
142
143- For the exponential-smoothing/spring derivation, a complete deadzone+look-ahead+bounds 2D rig,
144 3D spring-arm/orbit details, first-person look, multi-target/group framing and split-screen,
145 cinematic camera blends, and the Cinemachine 3 / Godot Camera2D / PhantomCamera mapping, read
146 `references/follow-and-framing.md`.
147
148## Related skills
149
150- `game-feel` — owns screen-shake trauma/triggers; this skill exposes the offset it writes.
151- `godot-2d-movement`, `godot-3d-essentials` — the player/world the camera frames; Camera3D setup.
152- `physics-tuning` — interpolate camera follow with the physics step to kill jitter.
153- `platformer`, `fps-shooter` — genres whose camera rules this skill implements.
154- `performance-optimization` — cost of extra cameras, render targets, and split-screen.