references/camera-expert-patterns.md
# Camera Expert Patterns (load on demand)
> **MANDATORY** when implementing multi-target framing, custom 3D occlusion rigs, or trauma decay tuning. Do not paste these into scenes from memory.
## Multi-target framing (WHY)
Smash-style or local-coop cameras must fit an AABB of all targets — not chase a single `global_position`. Juice shake stays on `offset` only so follow math is not overwritten.
Use [framing_box_camera_2d.gd](../scripts/framing_box_camera_2d.gd) — lerp center, clamp zoom to fit margin.
## Occlusion without SpringArm
When SpringArm3D is insufficient, raycast between target and ideal camera position. Exclude the target RID; offset along hit normal to prevent wall clipping.
Use [occlusion_aware_camera_3d.gd](../scripts/occlusion_aware_camera_3d.gd). For SpringArm-first rigs see [spring_lerp_camera_3d.gd](../scripts/spring_lerp_camera_3d.gd).
## Trauma decay audit
> **CAUTION:** Plot `get_trauma()` over time while tuning [camera_shake_trauma_pro.gd](../scripts/camera_shake_trauma_pro.gd). Raw `randf` offset demos hide whether decay feels intentional.
[trauma_debugger.gd](../scripts/trauma_debugger.gd) draws the last 200 samples via `_draw()`.
## 2D follow recipes (beginner → expert)
### Lerp follow
```gdscript
extends Camera2D
@export var target: Node2D
@export var follow_speed := 5.0
func _process(delta: float) -> void:
if target:
global_position = global_position.lerp(target.global_position, follow_speed * delta)
```
Prefer `position_smoothing_enabled` when built-in smoothing suffices.
### Look-ahead
Offset camera by `target.velocity.normalized() * look_ahead_distance` — requires velocity from [godot-characterbody-2d](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-characterbody-2d/SKILL.md).
### Deadzone / drag
Use [deadzone_drag_margins.gd](../scripts/deadzone_drag_margins.gd) — enable `drag_horizontal_enabled` / margins instead of manual lerp every frame.
## 3D patterns
### Third-person orbit
Orbit with `sin/cos` on horizontal angle + `look_at(target, Vector3.UP)`. Guard vertical targets — see NEVER on `look_at` up-vector flip in SKILL.md.
### First-person mouse look
Yaw on parent, pitch on camera child with clamped pitch. Use [first_person_sway.gd](../scripts/first_person_sway.gd) for bob on `offset`.
## Transitions & cinematics
Tween `global_position` with `TRANS_CUBIC` / `EASE_IN_OUT`. PathFollow2D `progress_ratio` for cutscenes — see [cinematic_framing_logic.gd](../scripts/cinematic_framing_logic.gd) and [camera_state_machine.gd](../scripts/camera_state_machine.gd).
## Zoom
**WHY exponential zoom:** Linear zoom feels robotic. Use [zoom_damping_controller.gd](../scripts/zoom_damping_controller.gd) or wheel-driven exponential lerp.
## Minimap / split-screen
SubViewport `render_target_update_mode` must not default to always-on updates — [minimap_viewport_manager.gd](../scripts/minimap_viewport_manager.gd), [split_screen_setup.gd](../scripts/split_screen_setup.gd).
references/migration-notes.md
# Migration notes: godot-camera-systems
Incremental upgrade for topics this skill covers. Apply **one hop**, stabilize/test, then next. Never skip hops.
If the project is **< 4.0**, follow [godot-version-migration](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-version-migration/SKILL.md) era bridges (legacy → 3→4) until 4.0, then these hops. Official 3→4: [Upgrading from Godot 3 to Godot 4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.html).
## 3.x → 4.0
Official: [Upgrading from Godot 3 to Godot 4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.html)
- Camera2D: `zoom` inverted; `rotating` → inverted `ignore_rotation`; drag offset renames.
- Camera3D: `znear`/`zfar` → `near`/`far`.
- `ClippedCamera`/`InterpolatedCamera` removed — rebuild with Camera2D/3D.
- ARVR cameras → XR* nodes.
## 4.0 → 4.1
Official: [Upgrading to Godot 4.1](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.1.html)
- `Node3D.look_at` / `look_at_from_position` gain `use_model_front`.
- `PathFollow2D.lookahead` removed.
- `MeshInstance3D.create_multiple_convex_collisions` optional `settings`.
- `PathFollow2D.lookahead` removed (2D paths); 3D look_at `use_model_front`.
## 4.1 → 4.2
Official: [Upgrading to Godot 4.2](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.2.html)
*No skill-relevant breaking changes for this hop.*
## 4.2 → 4.3
Official: [Upgrading to Godot 4.3](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.3.html)
- `Skeleton3D.add_bone` returns `int32`; pose update signal rename.
## 4.3 → 4.4
Official: [Upgrading to Godot 4.4](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.4.html)
- CSG uses Manifold — **non-manifold** meshes unsupported; use MeshInstance3D for quads/planes.
## 4.4 → 4.5
Official: [Upgrading to Godot 4.5](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.5.html)
- GLTF/BLEND/FBX naming version for non-joint nodes in skeletons — set Import dock Naming Version for old assets.
## 4.5 → 4.6
Official: [Upgrading to Godot 4.6](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.6.html)
- `MeshInstance3D.skeleton` default empty; SpringBone enums moved to SkeletonModifier3D.
## 4.6 → 4.7
Official: [Upgrading to Godot 4.7](https://docs.godotengine.org/en/stable/tutorials/migrating/upgrading_to_godot_4.7.html)
- Path3D snap-to-colliders / 3D vertex snapping editor workflows; AreaLight3D for soft rect lights.
scripts/camera_follow_2d.gd
extends Camera2D
## Expert Camera2D follow script with look-ahead prediction and deadzones.
## Attachment: Add as child of the level OR parent to player and set top_level = true.
@export_group("Targeting")
@export var target: Node2D
@export var look_ahead_enabled: bool = true
@export var look_ahead_distance: float = 120.0
@export var look_ahead_speed: float = 2.0
@export_group("Smoothing")
@export var follow_smoothing: float = 5.0
@export var velocity_smoothing: float = 2.0
var _target_velocity: Vector2 = Vector2.ZERO
var _last_target_pos: Vector2 = Vector2.ZERO
func _ready() -> void:
if not target:
push_warning("CameraFollow2D: No target assigned.")
# Enable built-in smoothing as base
position_smoothing_enabled = true
position_smoothing_speed = follow_smoothing
func _process(delta: float) -> void:
if not target:
return
var target_pos = target.global_position
if look_ahead_enabled:
# Calculate target velocity if it's not a CharacterBody
var current_velocity = (target_pos - _last_target_pos) / delta if delta > 0 else Vector2.ZERO
if target is CharacterBody2D:
current_velocity = target.get_real_velocity()
_target_velocity = _target_velocity.lerp(current_velocity, velocity_smoothing * delta)
_last_target_pos = target_pos
# Apply look-ahead offset
var offset_vec = _target_velocity.normalized() * look_ahead_distance
target_pos += offset_vec
global_position = target_pos
## Helper to set camera limits from a ColorRect or reference shape
func set_limits_from_rect(rect: Rect2) -> void:
limit_left = int(rect.position.x)
limit_top = int(rect.position.y)
limit_right = int(rect.end.x)
limit_bottom = int(rect.end.y)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# - https://docs.godotengine.org/en/stable/tutorials/math/interpolation.html
# - https://docs.godotengine.org/en/stable/classes/class_characterbody2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-characterbody-2d/SKILL.md — velocity look-ahead from real_velocity
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-tweening/SKILL.md — optional soft handoff when retargeting
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md — level limit rects / stretch context
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/camera_shake_trauma_pro.gd
# camera_shake_trauma_pro.gd
# Advanced trauma-based screenshake using noise [27]
extends Camera2D
# EXPERT NOTE: Noise-based shake is superior to random offsets as
# it prevents high-frequency jitter and feels more organic.
@export var trauma_reduction_rate: float = 1.0
@export var max_offset: Vector2 = Vector2(100, 75)
@export var max_roll: float = 0.1
var trauma: float = 0.0 # 0.0 to 1.0
var noise: FastNoiseLite = FastNoiseLite.new()
var noise_y: int = 0
func _ready() -> void:
noise.seed = randi()
noise.frequency = 0.5
func add_trauma(amount: float) -> void:
trauma = clamp(trauma + amount, 0.0, 1.0)
func get_trauma() -> float:
return trauma
func _process(delta: float) -> void:
if trauma > 0:
trauma = max(trauma - trauma_reduction_rate * delta, 0)
_execute_shake()
func _execute_shake() -> void:
# Using squared trauma makes the shake feel more explosive [28]
var shake = trauma * trauma
noise_y += 1
rotation = max_roll * shake * noise.get_noise_2d(noise.seed, noise_y)
offset.x = max_offset.x * shake * noise.get_noise_2d(noise.seed * 2, noise_y)
offset.y = max_offset.y * shake * noise.get_noise_2d(noise.seed * 3, noise_y)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# - https://docs.godotengine.org/en/stable/classes/class_fastnoiselite.html
# - https://docs.godotengine.org/en/stable/tutorials/math/interpolation.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — layer shake over follow via signals
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md — trauma decay vs perceived impact
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — keep noise cheap vs per-frame rand spam
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/camera_shake_trauma.gd
# skills/camera-systems/scripts/camera_shake_trauma.gd
extends Camera2D
## Trauma-Based Camera Shake Expert Pattern
## Perlin-noise powered shake that degrades naturally over time.
class_name CameraShakeTrauma
@export var max_offset := 100.0
@export var max_rotation := 10.0 # degrees
@export var trauma_power := 2.0
@export var trauma_decay := 1.0
var trauma := 0.0
var _noise := FastNoiseLite.new()
var _noise_seed := randi()
func _ready() -> void:
_noise.seed = _noise_seed
_noise.frequency = 4.0
func _process(delta: float) -> void:
if trauma > 0:
trauma = max(trauma - trauma_decay * delta, 0.0)
_apply_shake()
else:
offset = Vector2.ZERO
rotation = 0.0
func add_trauma(amount: float) -> void:
trauma = min(trauma + amount, 1.0)
func _apply_shake() -> void:
var shake_amount := pow(trauma, trauma_power)
# Use time-based noise for smooth shake
var time := Time.get_ticks_msec() / 1000.0
offset.x = max_offset * shake_amount * _noise.get_noise_2d(_noise_seed, time)
offset.y = max_offset * shake_amount * _noise.get_noise_2d(_noise_seed + 1, time)
rotation_degrees = max_rotation * shake_amount * _noise.get_noise_2d(_noise_seed + 2, time)
## EXPERT USAGE:
## Extend Camera2D with this script, or:
## var cam: CameraShakeTrauma = $Camera2D
##
## # On explosion:
## cam.add_trauma(0.5)
##
## # On heavy hit:
## cam.add_trauma(0.8)
##
## # Trauma decays naturally over ~1 second
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# - https://docs.godotengine.org/en/stable/classes/class_fastnoiselite.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — add_trauma from combat/VFX events
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md — cap trauma so readability survives spam
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — process-time noise sampling patterns
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/camera_state_machine.gd
# camera_state_machine.gd
# Managing transitions between multiple camera states
extends Node
# This pattern uses a central manager to handle transitions
# between 'Follow', 'Static', and 'Cinematic' camera states.
enum State { FOLLOW, STATIC, CINEMATIC }
var current_state: State = State.FOLLOW
@onready var main_camera: Camera2D = get_viewport().get_camera_2d()
func transition_to_static(pos: Vector2, duration: float = 1.0) -> void:
current_state = State.STATIC
var tween = create_tween()
# Disable smoothing during manual transition to take full control
main_camera.position_smoothing_enabled = false
tween.tween_property(main_camera, "global_position", pos, duration)\
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT)
await tween.finished
# Re-enable if needed
func set_follow_target(node: Node2D) -> void:
current_state = State.FOLLOW
# Use RemoteTransform2D on target to drive camera position
# for perfectly decoupled logic.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# - https://docs.godotengine.org/en/stable/classes/class_tween.html
# - https://docs.godotengine.org/en/stable/classes/class_remotetransform2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-state-machine-advanced/SKILL.md — expand Follow/Static/Cinematic ownership
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-tweening/SKILL.md — TRANS_CUBIC camera handoffs
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — request transitions without hard refs
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/cinematic_framing_logic.gd
# cinematic_framing_logic.gd
# Implementing Rule of Thirds and Lead Room in code [31]
extends Camera2D
@export var target: Node2D
@export var look_ahead_factor: float = 0.2
@export var vertical_offset_ratio: float = -0.1 # Move up for Rule of Thirds
func _process(delta: float) -> void:
if not target: return
# Rule of Thirds: Offset the target slightly above center
var v_offset = get_viewport_rect().size.y * vertical_offset_ratio
# Lead Room: Shift camera in direction of target velocity
var velocity = Vector2.ZERO
if "velocity" in target:
velocity = target.velocity
var lead_offset = velocity * look_ahead_factor
var goal_pos = target.global_position + lead_offset + Vector2(0, v_offset)
# Smoothly interpolate to the framed goal
global_position = global_position.lerp(goal_pos, 5.0 * delta)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# - https://docs.godotengine.org/en/stable/tutorials/math/interpolation.html
# - https://docs.godotengine.org/en/stable/classes/class_pathfollow2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-characterbody-2d/SKILL.md — lead room from target.velocity
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-tweening/SKILL.md — cutscene framing blends
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-adapt-single-to-multiplayer/SKILL.md — framing fairness when multiple actors share a view
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/deadzone_drag_margins.gd
# deadzone_drag_margins.gd
# Platformer-style deadzone management in code [267]
extends Camera2D
func _ready() -> void:
# Enables the 'drag' margins that define a central deadzone
drag_horizontal_enabled = true
drag_vertical_enabled = true
# Margin 0.2 means the player must move 20% from center
# before the camera starts following.
drag_left_margin = 0.2
drag_right_margin = 0.2
drag_top_margin = 0.4
drag_bottom_margin = 0.1
# Visualizes the deadzone in the editor
editor_draw_drag_margin = true
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-characterbody-2d/SKILL.md — platformer motion that fills drag margins
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md — window aspect affects perceived deadzone
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md — editor_draw_drag_margin tuning loop
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/first_person_sway.gd
# first_person_sway.gd
# Procedural head-bob and weapon sway for FPS games [212]
extends Camera3D
@export var bob_freq: float = 2.0
@export var bob_amp: float = 0.08
var _time: float = 0.0
func _process(delta: float) -> void:
var velocity = get_parent().velocity if get_parent() is CharacterBody3D else Vector3.ZERO
var horizontal_vel = Vector2(velocity.x, velocity.z).length()
if horizontal_vel > 0.1:
_time += delta * horizontal_vel
# 8-figure head bob
var bob = Vector3.ZERO
bob.y = sin(_time * bob_freq) * bob_amp
bob.x = cos(_time * bob_freq * 0.5) * bob_amp
transform.origin = bob
else:
_time = 0
transform.origin = transform.origin.lerp(Vector3.ZERO, delta * 5.0)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera3d.html
# - https://docs.godotengine.org/en/stable/classes/class_characterbody3d.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md — parent CharacterBody3D velocity source
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md — mouse capture / look axes for FPS
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — local-origin bob without fighting look_at
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/framing_box_camera_2d.gd
# framing_box_camera_2d.gd
class_name FramingBoxCamera2D
extends Camera2D
## Dynamically zooms and pans to frame multiple targets.
@export var targets: Array[Node2D] = []
@export var margin: float = 100.0
@export var min_zoom: float = 0.5
@export var max_zoom: float = 2.0
func _physics_process(_delta: float) -> void:
if targets.is_empty():
return
var rect := Rect2(targets[0].global_position, Vector2.ZERO)
for target in targets:
rect = rect.expand(target.global_position)
rect = rect.grow(margin)
global_position = rect.get_center()
var screen_size := get_viewport_rect().size
var zoom_x := screen_size.x / rect.size.x
var zoom_y := screen_size.y / rect.size.y
var target_zoom := clampf(min(zoom_x, zoom_y), min_zoom, max_zoom)
zoom = Vector2.ONE * target_zoom
scripts/juice_camera.gd
# skills/camera-systems/code/juice_camera.gd
extends Camera2D
## Juice Camera Expert Pattern
## Combines Trauma-based Simplex Noise shake with Velocity Lead Room.
@export_group("Trauma Settings")
@export var decay: float = 0.8 # How quickly trauma drops
@export var max_offset: Vector2 = Vector2(100, 75)
@export var max_roll: float = 0.1
@export var noise: FastNoiseLite = FastNoiseLite.new()
@export_group("Lead Room Settings")
@export var lead_distance: float = 200.0
@export var lead_speed: float = 5.0
var trauma: float = 0.0 # Current "stress" level (0 to 1)
var trauma_power: int = 2 # Trauma is squared for feel
var _noise_y: int = 0
func _ready() -> void:
randomize()
noise.seed = randi()
noise.frequency = 0.5
func _process(delta: float) -> void:
# 1. Decay trauma
trauma = max(trauma - decay * delta, 0.0)
# 2. Apply shake
if trauma > 0:
_apply_shake()
# 3. Handle Lead Room (Logic should usually be in a separate controller,
# but integrated here for reference)
var target_vel = Vector2.ZERO # In practice, get from Player.velocity
var target_offset = target_vel.normalized() * lead_distance
offset = offset.lerp(target_offset, lead_speed * delta)
func add_trauma(amount: float) -> void:
trauma = min(trauma + amount, 1.0)
func _apply_shake() -> void:
var amount = pow(trauma, trauma_power)
_noise_y += 1
rotation = max_roll * amount * noise.get_noise_2d(noise.seed, _noise_y)
offset.x = max_offset.x * amount * noise.get_noise_2d(noise.seed * 2, _noise_y)
offset.y = max_offset.y * amount * noise.get_noise_2d(noise.seed * 3, _noise_y)
## EXPERT NOTE:
## Noise shake is superior to Random shake because it produces 'smooth' jitter
## that replicates handheld camera weight.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# - https://docs.godotengine.org/en/stable/classes/class_fastnoiselite.html
# - https://docs.godotengine.org/en/stable/tutorials/math/interpolation.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — trauma pulses from hits/explosions
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-characterbody-2d/SKILL.md — lead-room velocity source
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md — juice vs competitive clarity
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/minimap_viewport_manager.gd
# minimap_viewport_manager.gd
# Setting up 2D/3D Mini-maps using SubViewports [156]
extends SubViewportContainer
# EXPERT NOTE: SubViewports are expensive. Use a low render_target_update_mode
# for UI elements that don't need 60FPS updates (like world maps).
@onready var minimap_cam: Camera2D = $SubViewport/Camera2D
@export var player: Node2D
func _ready() -> void:
# Optimization: Only update the minimap if the player moves significantly
$SubViewport.render_target_update_mode = SubViewport.UPDATE_WHEN_VISIBLE
func _process(_delta: float) -> void:
if player:
# Mini-map follows player but ignores rotation
minimap_cam.global_position = player.global_position
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/rendering/viewports.html
# - https://docs.godotengine.org/en/stable/classes/class_subviewport.html
# - https://docs.godotengine.org/en/stable/classes/class_subviewportcontainer.html
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — UPDATE_WHEN_VISIBLE / lower update rates
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-ui-containers/SKILL.md — SubViewportContainer layout in HUD
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-scene-management/SKILL.md — world/minimap camera ownership across scenes
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/occlusion_aware_camera_3d.gd
# occlusion_aware_camera_3d.gd
class_name OcclusionAwareCamera3D
extends Camera3D
## Prevents camera clipping via manual physics space raycasting.
@export var target: Node3D
@export var ideal_distance: float = 5.0
@export var wall_offset: float = 0.2
func _physics_process(_delta: float) -> void:
if not target:
return
var space_state := get_world_3d().direct_space_state
var desired_pos := target.global_position + (Vector3.BACK * ideal_distance)
var query := PhysicsRayQueryParameters3D.create(target.global_position, desired_pos)
query.exclude = [target.get_rid()]
var result: Dictionary = space_state.intersect_ray(query)
if not result.is_empty():
global_position = result.position + result.normal * wall_offset
else:
global_position = desired_pos
look_at(target.global_position, Vector3.UP)
scripts/phantom_decoupling.gd
# skills/camera-systems/code/phantom_decoupling.gd
extends Node2D
## Phantom Camera Decoupling Pattern
## Separates 'Where we look' from 'What we follow'.
@export var target_node: Node2D
@export var smoothing: float = 0.1 # Weight (0 to 1)
var _logical_position: Vector2
func _physics_process(_delta: float) -> void:
if not target_node: return
# 1. Update Logical Position
# This position can be influenced by secondary 'weight' sources (enemies, mouse, interest points)
_logical_position = target_node.global_position
# 2. Apply Logical Position to Camera (indirectly)
# The actual Camera2D should follow this node, not the Player directly.
global_position = global_position.lerp(_logical_position, smoothing)
## WHY THIS WAY?
## By following a 'Phantom' node instead of the Player, you can perform
## cinematic offsets, lock the camera to an Area2D bounds, or shift focus
## to an explosion without detaching the player's controls from their node.
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# - https://docs.godotengine.org/en/stable/tutorials/math/interpolation.html
# - https://docs.godotengine.org/en/stable/classes/class_remotetransform2d.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md — interest-point weights via events
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-tweening/SKILL.md — cinematic offset blends on the phantom
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-state-machine-advanced/SKILL.md — lock phantom during cutscenes
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/remote_transform_decoupling.gd
# remote_transform_decoupling.gd
# Decoupling Camera from Player hierarchy using RemoteTransform2D [30]
extends Node2D
# EXPERT NOTE: Avoid parenting the Camera directly to the Player.
# Using RemoteTransform2D prevents player rotation/scale from
# affecting the camera while keeping position sync.
@onready var remote: RemoteTransform2D = RemoteTransform2D.new()
@export var camera: Camera2D
func _ready() -> void:
add_child(remote)
remote.remote_path = camera.get_path()
# Configure what to sync
remote.update_position = true
remote.update_rotation = false # Camera stays upright
remote.update_scale = false # Camera stays at 1:1
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_remotetransform2d.html
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# - https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/physics_interpolation_introduction.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-characterbody-2d/SKILL.md — avoid parenting camera under physics body
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md — diagnose rotation/scale bleed into view
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md — remote_path setup and update flags
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/split_screen_setup.gd
# split_screen_setup.gd
# Managing dynamic split-screen viewports efficiently [146]
extends HBoxContainer
# Scene Structure:
# HBoxContainer
# ├─ SubViewportContainer (Player 1)
# │ └─ SubViewport
# │ └─ Camera2D
# └─ SubViewportContainer (Player 2)
# └─ SubViewport
# └─ Camera2D
func set_split_ratio(ratio: float) -> void:
# Custom weight management for asymmetric split-screen
var p1 = get_child(0) as Control
var p2 = get_child(1) as Control
p1.size_flags_stretch_ratio = ratio
p2.size_flags_stretch_ratio = 1.0 - ratio
func _ready() -> void:
# Ensure audio listeners are balanced
get_child(0).get_node("SubViewport").audio_listener_enable_2d = true
get_child(1).get_node("SubViewport").audio_listener_enable_2d = false
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/tutorials/rendering/viewports.html
# - https://docs.godotengine.org/en/stable/classes/class_subviewport.html
# - https://docs.godotengine.org/en/stable/classes/class_subviewportcontainer.html
# - https://docs.godotengine.org/en/stable/classes/class_hsplitcontainer.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-adapt-single-to-multiplayer/SKILL.md — local coop camera ownership
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-ui-containers/SKILL.md — stretch ratios for asymmetric splits
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md — dual SubViewport cost budgets
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-audio-systems/SKILL.md — which viewport owns the 2D audio listener
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/spring_lerp_camera_3d.gd
# spring_lerp_camera_3d.gd
# Advanced 3D camera follow using Spring interpolation [169]
extends Camera3D
@export var target: Node3D
@export var offset: Vector3 = Vector3(0, 5, 10)
@export var spring_stiffness: float = 15.0
func _physics_process(delta: float) -> void:
if not target: return
var target_pos = target.global_position + offset
# Spring-based follow prevents the 'elastic' feel of simple lerp
# and reduces visual stutter at high speeds.
global_position = global_position.lerp(target_pos, delta * spring_stiffness)
look_at(target.global_position)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera3d.html
# - https://docs.godotengine.org/en/stable/tutorials/3d/spring_arm.html
# - https://docs.godotengine.org/en/stable/classes/class_springarm3d.html
# - https://docs.godotengine.org/en/stable/tutorials/math/interpolation.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md — follow CharacterBody3D / collision context
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-raycasting-queries/SKILL.md — custom occlusion if not using SpringArm
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md — prove follow jitter vs physics tick
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
scripts/trauma_debugger.gd
# trauma_debugger.gd
class_name TraumaDebugger
extends Node2D
## Visualizes the decay curve of a trauma-based shake system.
@export var camera: Node
var _history: PackedFloat32Array = []
func _process(_delta: float) -> void:
if not camera or not camera.has_method(&"get_trauma"):
return
_history.append(camera.call(&"get_trauma"))
if _history.size() > 200:
_history.remove_at(0)
queue_redraw()
func _draw() -> void:
var width := 400.0
var height := 100.0
var step := width / 200.0
for i in range(1, _history.size()):
var p1 := Vector2(i * step, height - (_history[i - 1] * height))
var p2 := Vector2((i + 1) * step, height - (_history[i] * height))
draw_line(p1, p2, Color.YELLOW, 2.0)
scripts/zoom_damping_controller.gd
# zoom_damping_controller.gd
# Smooth, non-linear zoom control for tactical overview
extends Camera2D
@export var min_zoom: float = 0.5
@export var max_zoom: float = 2.0
@export var zoom_speed: float = 10.0
var target_zoom: Vector2 = Vector2.ONE
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
target_zoom = (target_zoom - Vector2(0.1, 0.1)).max(Vector2(min_zoom, min_zoom))
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
target_zoom = (target_zoom + Vector2(0.1, 0.1)).min(Vector2(max_zoom, max_zoom))
func _process(delta: float) -> void:
# Exponential lerp for zoom feels smoother than linear
zoom = zoom.lerp(target_zoom, zoom_speed * delta)
# =============================================================================
# GDSkills research links (agents) — does not affect runtime
# Official docs:
# - https://docs.godotengine.org/en/stable/classes/class_camera2d.html
# - https://docs.godotengine.org/en/stable/tutorials/inputs/mouse_and_input_coordinates.html
# - https://docs.godotengine.org/en/stable/tutorials/math/interpolation.html
# Related skills:
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md — wheel / magnify gesture routing
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-tweening/SKILL.md — alternative cubic zoom Tweens
# - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-ui-containers/SKILL.md — keep HUD crisp when zoom changes world scale
# Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-camera-systems/SKILL.md
# =============================================================================
SKILL.md
---
name: godot-camera-systems
description: "Expert patterns for 2D/3D camera control including smooth following (lerp, position_smoothing), camera shake (trauma system), screen shake with frequency parameters, deadzone/drag for platformers, look-ahead prediction, and camera transitions. Use for player cameras, cinematic sequences, or multi-camera systems. Trigger keywords: Camera2D, Camera3D, SpringArm3D, position_smoothing, camera_shake, trauma_system, look_ahead, drag_margin, camera_limits, camera_transition."
---
## NEVER Do
- **NEVER use `global_position = target.global_position` every frame** — Instant position matching causes jittery movement. Use `lerp()` or `position_smoothing_enabled = true`.
- **NEVER use `offset` for permanent camera positioning** — `offset` is for shake, sway, or temporary recoil effects only. Use `position` for permanent framing.
- **NEVER forget `limit_smoothed = true` for `Camera2D`** — Hard boundaries cause jarring visual stops.
- **NEVER enable multiple `Camera2D` nodes in the same viewport simultaneously** — Only the last enabled camera takes precedence. Explicitly disable inactive cameras.
- **NEVER use `SpringArm3D` without a collision mask** — It will clip through terrain and walls. Set it to the world/environment layer.
- **NEVER implement screen shake by randomizing `position` (or `randf` on `offset` as the whole system)** — Use a dedicated Trauma/Noise system layered on follow ([camera_shake_trauma_pro.gd](scripts/camera_shake_trauma_pro.gd)).
- **NEVER parent the Camera directly to a high-speed physics body as the default rig** — Physics stutter or parent rotation causes motion sickness. Prefer `RemoteTransform2D/3D` / phantom decoupling with rotation sync disabled ([remote_transform_decoupling.gd](scripts/remote_transform_decoupling.gd), [phantom_decoupling.gd](scripts/phantom_decoupling.gd)).
- **NEVER use `look_at()` in 3D without a fallback for the 'Up' vector** — Targets directly above/below flip the camera; use guards or Quaternion math.
- **NEVER rely on `SubViewport` defaults for Mini-maps** — Set `render_target_update_mode` to `UPDATE_WHEN_VISIBLE` or a lower fixed rate.
- **NEVER use linear interpolation for Zoom** — Prefer exponential lerp or Tween `TRANS_CUBIC`.
---
## Parenting / Decoupling (resolved)
| Rig | When | Script |
|-----|------|--------|
| **Default:** RemoteTransform / phantom | Player is CharacterBody / high-speed / rotates | [remote_transform_decoupling.gd](scripts/remote_transform_decoupling.gd), [phantom_decoupling.gd](scripts/phantom_decoupling.gd) |
| Camera as child of player | Slow top-down / locked rotation / prototype only | Explicit caveat: disable if motion sickness or physics jitter appears; never combine with position-overwrite shake |
| SpringArm3D + Camera3D | Third-person occlusion | [spring_lerp_camera_3d.gd](scripts/spring_lerp_camera_3d.gd) — mask required |
## Available Scripts
> **MANDATORY**: Read before implementing the matching behavior. No `randf` shake samples in project code.
- [camera_shake_trauma_pro.gd](scripts/camera_shake_trauma_pro.gd) — **MANDATORY** for any screen shake / impact juice.
- [camera_shake_trauma.gd](scripts/camera_shake_trauma.gd) — Lighter trauma variant.
- [remote_transform_decoupling.gd](scripts/remote_transform_decoupling.gd) — **MANDATORY** default for physics-body follow.
- [phantom_decoupling.gd](scripts/phantom_decoupling.gd) — Alternate stable follow phantom.
- [spring_lerp_camera_3d.gd](scripts/spring_lerp_camera_3d.gd) — **MANDATORY** before custom 3D follow springs.
- [deadzone_drag_margins.gd](scripts/deadzone_drag_margins.gd) — Platformer drag/deadzone.
- [camera_follow_2d.gd](scripts/camera_follow_2d.gd) — Smooth 2D follow helpers.
- [zoom_damping_controller.gd](scripts/zoom_damping_controller.gd) — Non-linear zoom.
- [camera_state_machine.gd](scripts/camera_state_machine.gd) — Follow / Static / Cinematic transitions.
- [cinematic_framing_logic.gd](scripts/cinematic_framing_logic.gd) — Rule of thirds / lead room.
- [minimap_viewport_manager.gd](scripts/minimap_viewport_manager.gd) — SubViewport update modes.
- [split_screen_setup.gd](scripts/split_screen_setup.gd) — Local multi camera viewports.
- [first_person_sway.gd](scripts/first_person_sway.gd) — FPS bob/sway on offset.
- [juice_camera.gd](scripts/juice_camera.gd) — Combined juice helpers.
- [framing_box_camera_2d.gd](scripts/framing_box_camera_2d.gd) — Multi-target AABB framing + zoom fit.
- [occlusion_aware_camera_3d.gd](scripts/occlusion_aware_camera_3d.gd) — Raycast occlusion when SpringArm is insufficient.
- [trauma_debugger.gd](scripts/trauma_debugger.gd) — On-screen trauma decay curve (debug builds).
## Expert Camera Architectures
### 1. Multi-target framing
Compute AABB of targets → lerp camera to center → zoom/distance to fit with margin. Keep juice shake on `offset` only. **MANDATORY:** [framing_box_camera_2d.gd](scripts/framing_box_camera_2d.gd).
### 2. Occlusion (3D)
Prefer `SpringArm3D` with world collision mask; custom rigs use `intersect_ray` between ideal camera pos and target — [occlusion_aware_camera_3d.gd](scripts/occlusion_aware_camera_3d.gd) (peer `godot-raycasting-queries`).
### 3. Trauma audit
Plot trauma decay (debug draw) while tuning [camera_shake_trauma_pro.gd](scripts/camera_shake_trauma_pro.gd) — wire [trauma_debugger.gd](scripts/trauma_debugger.gd) to `get_trauma()`. Never validate feel with raw `randf` offset demos.
> **MANDATORY** for multi-target framing, custom occlusion rigs, 2D/3D follow recipes, and cinematic transitions: [camera-expert-patterns.md](references/camera-expert-patterns.md). **Do NOT Load** when golden-path scripts already cover your rig.
## Reference
> Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
### Official Documentation
- [Camera2D](https://docs.godotengine.org/en/stable/classes/class_camera2d.html) — Position/drag margins, `limit_*` / `limit_smoothed`, and `position_smoothing_*` that underpin 2D follow, deadzones, and level bounds.
- [Camera3D](https://docs.godotengine.org/en/stable/classes/class_camera3d.html) — Projection, `look_at`, current-camera rules, and environment overrides used by third-person, FPS, and cinematic 3D rigs.
- [Third-person camera with spring arm](https://docs.godotengine.org/en/stable/tutorials/3d/spring_arm.html) — Why parenting a Camera3D alone clips geometry and how SpringArm3D length/shape keep the view clear.
- [SpringArm3D](https://docs.godotengine.org/en/stable/classes/class_springarm3d.html) — Collision mask, margin, and spring length API required before third-person occlusion pulls feel trustworthy.
- [Using Viewports](https://docs.godotengine.org/en/stable/tutorials/rendering/viewports.html) — Multiple cameras, SubViewport architecture, and when split-screen / minimap views share or isolate worlds.
- [SubViewport](https://docs.godotengine.org/en/stable/classes/class_subviewport.html) — `render_target_update_mode` and audio-listener flags that decide minimap and local-coop GPU/audio cost.
- [RemoteTransform2D](https://docs.godotengine.org/en/stable/classes/class_remotetransform2d.html) — Decouple camera position from player rotation/scale without parenting the camera under a physics body.
- [Interpolation](https://docs.godotengine.org/en/stable/tutorials/math/interpolation.html) — Lerp / exponential follow and zoom damping math so custom cameras do not feel robotic or jittery.
- [Physics interpolation (introduction)](https://docs.godotengine.org/en/stable/tutorials/physics/interpolation/physics_interpolation_introduction.html) — Why cameras following CharacterBody motion stutter when render and physics ticks disagree.
- [FastNoiseLite](https://docs.godotengine.org/en/stable/classes/class_fastnoiselite.html) — Coherent noise for trauma/offset shake instead of raw `randf` position thrashing.
- [PathFollow2D](https://docs.godotengine.org/en/stable/classes/class_pathfollow2d.html) — Progress-ratio driven cinematic paths when Tweening a camera along a Path2D.
- [Mouse and input coordinates](https://docs.godotengine.org/en/stable/tutorials/inputs/mouse_and_input_coordinates.html) — Wheel zoom and mouse-look coordinate spaces so FPS pitch/yaw and tactical zoom stay consistent across viewports.
### Related Skills
#### Prerequisites
- [godot-project-foundations](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md) — Stretch mode, default viewport, and input map setup decide how Camera2D limits and SubViewport sizes behave before any follow script runs.
- [godot-gdscript-mastery](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md) — Typed nodes, `_physics_process` vs `_process`, and Tween/await patterns used by state machines and spring follow.
- [godot-input-handling](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-input-handling/SKILL.md) — Captured mouse, look axes, and mouse-wheel events feed FPS look, zoom damping, and camera orbit controls.
#### Complements
- [godot-tweening](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-tweening/SKILL.md) — Camera transitions between Follow/Static/Cinematic should use Tweens (ease/trans), not hard snaps or linear zoom.
- [godot-characterbody-2d](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-characterbody-2d/SKILL.md) — Look-ahead and deadzone cameras need real velocity / floor state from the platformer body they frame.
- [godot-physics-3d](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-physics-3d/SKILL.md) — SpringArm collision layers and CharacterBody3D motion are the 3D counterparts to stable third-person and FPS sway parents.
- [godot-raycasting-queries](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-raycasting-queries/SKILL.md) — Custom occlusion-aware cameras that do not use SpringArm still need correct `intersect_ray` masks and excludes.
- [godot-state-machine-advanced](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-state-machine-advanced/SKILL.md) — Formalize Follow/Static/Cinematic (and cutscene ownership) when camera_state_machine outgrows a simple enum.
- [godot-signal-architecture](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-signal-architecture/SKILL.md) — Trauma add, cutscene handoff, and multi-target framing should be signal-driven so gameplay never reaches into camera internals.
#### Downstream / consumers
- [godot-performance-optimization](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md) — Escalate when SubViewport minimaps, split-screen, or always-on secondary cameras still dominate frame time after update-mode tuning.
- [godot-monte-carlo-balancer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md) — Simulate shake intensity, zoom fairness, and multi-target framing so camera juice never hides hitboxes or competitive information.
- [godot-adapt-single-to-multiplayer](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-adapt-single-to-multiplayer/SKILL.md) — Consumes split-screen SubViewport patterns when local coop needs per-player cameras and listener ownership.
- [godot-debugging-profiling](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-debugging-profiling/SKILL.md) — Use monitors and visualizers to prove camera jitter sources (physics tick, RemoteTransform, trauma) before rewriting follow math.
#### Master
- [godot-master](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-master/SKILL.md) — Library router and mirrored module entry; open when discovering which Domain Skill owns a cross-cutting camera concern.