Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build game cameras that feel good — 2D follow with a deadzone, look-ahead, smoothing, and level-bounds clamping; 3D third-person orbit with collision and first-person look; plus multi-target framing and a shake hook. Engine-neutral techniques that pair with the engine's camera node and rigs like Unity Cinemachine or Godot Camera2D/PhantomCamera. Use when the user mentions camera follow, follow camera, deadzone, look-ahead, camera smoothing, camera bounds/ limits, third-person camera, orbit camer
.claude/skills/gamedev-skills-camera-systems/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-19 | ✗→✓ | ▲ Improved | 39% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 40% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 44% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 270% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 24% | 0% |
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.
player's motion, or ignore small movements (deadzone).
first-person look controller, or framing multiple targets at once.
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.
with deadzone), third-person (orbit + collision), first-person (look only). The genre sets the rules.
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.
target leaves a box/zone. Stops nausea in twitchy games.
motion or facing, eased in/out so it doesn't whip.
smoothing so it eases to a stop at the edge.
ray to pull the camera in when geometry blocks it; clamp pitch.
physics resolve) to avoid a one-frame lag jitter.
the level edges; confirm no jitter, no peeking past bounds, smooth stops. Report what you saw.
gdscript# 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
gdscript# 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.
gdscript# 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
gdscript# 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.
game-feel)gdscript# 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.
lerp(pos, target, const) per frame is frame-rate dependent — floatier at 30 FPS, snappierat 144. Use 1 - exp(-rate*dt) or SmoothDamp.
Follow in LateUpdate / after movement/physics resolve.
rect (account for the viewport half-size so the view, not the center, stays inside).
smoothing) or fast-ease. Don't let a huge SmoothDamp distance whip across the level.
Compose: smooth follow first, add shake offset last.
pick deliberately.
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.
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.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 21,776 | 15,308 | -30% | 1 | 1 | 0% | 4,334 | 5,210 | +20% | 0 | 0 | — |
case-02 | pass→pass | 13,191 | 7,217 | -45% | 1 | 1 | 0% | 2,625 | 3,672 | +40% | 0 | 0 | — |
case-03 | fail→fail | 13,018 | 8,282 | -36% | 1 | 1 | 0% | 2,152 | 3,525 | +64% | 0 | 0 | — |
case-04 | pass→pass | 13,683 | 7,521 | -45% | 1 | 1 | 0% | 2,476 | 3,557 | +44% | 0 | 0 | — |
case-05 | pass→pass | 4,179 | 3,197 | -23% | 1 | 1 | 0% | 748 | 2,769 | +270% | 0 | 0 | — |
case-06 | pass→pass | 14,181 | 5,293 | -63% | 1 | 1 | 0% | 2,498 | 3,089 | +24% | 0 | 0 | — |
case-07 | pass→pass | 12,474 | 10,065 | -19% | 1 | 1 | 0% | 2,176 | 3,809 | +75% | 0 | 0 | — |
case-08 | pass→pass | 9,744 | 7,770 | -20% | 1 | 1 | 0% | 1,870 | 3,655 | +95% | 0 | 0 | — |
case-09 | pass→pass | 14,147 | 7,817 | -45% | 1 | 1 | 0% | 2,773 | 3,679 | +33% | 0 | 0 | — |
case-10 | pass→pass | 16,979 | 10,519 | -38% | 1 | 1 | 0% | 2,824 | 3,935 | +39% | 0 | 0 | — |
case-11 | pass→pass | 4,919 | 3,552 | -28% | 1 | 1 | 0% | 829 | 2,814 | +239% | 0 | 0 | — |
case-12 | pass→pass | 10,246 | 6,500 | -37% | 1 | 1 | 0% | 2,019 | 3,347 | +66% | 0 | 0 | — |
case-13 | pass→pass | 10,270 | 6,537 | -36% | 1 | 1 | 0% | 1,681 | 3,179 | +89% | 0 | 0 | — |
case-14 | pass→pass | 10,570 | 4,999 | -53% | 1 | 1 | 0% | 1,983 | 3,048 | +54% | 0 | 0 | — |
case-15 | pass→pass | 12,838 | 8,082 | -37% | 1 | 1 | 0% | 2,199 | 3,421 | +56% | 0 | 0 | — |
case-16 | pass→pass | 15,835 | 11,443 | -28% | 1 | 1 | 0% | 2,869 | 4,263 | +49% | 0 | 0 | — |
case-17 | pass→pass | 13,628 | 10,130 | -26% | 1 | 1 | 0% | 2,784 | 4,200 | +51% | 0 | 0 | — |
case-18 | pass→pass | 10,538 | 6,332 | -40% | 1 | 1 | 0% | 1,870 | 3,319 | +77% | 0 | 0 | — |
case-19 | fail→pass | 20,467 | 16,264 | -21% | 1 | 1 | 0% | 3,684 | 5,113 | +39% | 0 | 0 | — |
case-20 | pass→pass | 14,055 | 11,480 | -18% | 1 | 1 | 0% | 2,678 | 4,351 | +62% | 0 | 0 | — |
case-21 | pass→pass | 15,494 | 14,584 | -6% | 1 | 1 | 0% | 3,027 | 5,327 | +76% | 0 | 0 | — |
case-22 | pass→pass | 17,600 | 14,406 | -18% | 1 | 1 | 0% | 2,914 | 4,521 | +55% | 0 | 0 | — |
case-23 | pass→pass | 12,844 | 11,236 | -13% | 1 | 1 | 0% | 2,475 | 4,319 | +75% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/2/2026 | -100% |
Other measured skills in the registry, with their headline benchmark lift.