Camera Systems
Expert guidance for creating smooth, responsive cameras in 2D and 3D games.
NEVER Do
- NEVER use
global_position = target.global_positionevery frame — Instant position matching causes jittery movement. Uselerp()orposition_smoothing_enabled = true. - NEVER forget
limit_smoothed = truefor Camera2D — Hard limits cause sudden stops at edges. Smoothing prevents jarring halts. - NEVER use offset for permanent camera positioning —
offsetis for shake/sway effects only. Usepositionfor permanent camera placement. - NEVER enable multiple Camera2D nodes simultaneously — Only one camera can be active. Others must have
enabled = false. - NEVER use SpringArm3D without collision mask — SpringArm clips through walls if
collision_maskis empty. Set to world layer
Available Scripts
MANDATORY: Read before implementing camera behaviors.
camera_follow_2d.gd
Smooth camera following with look-ahead prediction, deadzones, and boundary limits.
camera_shake_trauma.gd
Trauma-based camera shake using Perlin noise - industry-standard screenshake implementation. Uses FastNoiseLite for smooth camera shake (squared trauma for feel) with automatic decay.
phantom_decoupling.gd
Phantom camera pattern: separates "where we look" from "what we follow". Camera follows phantom node, enabling cinematic offsets and area bounds.
Camera2D Basics
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
)
Position Smoothing
extends Camera2D
func _ready() -> void:
# Built-in smoothing
position_smoothing_enabled = true
position_smoothing_speed = 5.0
Camera Limits
extends Camera2D
func _ready() -> void:
# Constrain camera to level bounds
limit_left = 0
limit_top = 0
limit_right = 1920
limit_bottom = 1080
# Smooth against limits
limit_smoothed = true
Camera Shake
extends Camera2D
var shake_amount := 0.0
var shake_decay := 5.0
func _process(delta: float) -> void:
if shake_amount > 0:
shake_amount = max(shake_amount - shake_decay * delta, 0)
offset = Vector2(
randf_range(-shake_amount, shake_amount),
randf_range(-shake_amount, shake_amount)
)
else:
offset = Vector2.ZERO
func shake(intensity: float) -> void:
shake_amount = intensity
# Usage:
$Camera2D.shake(10.0) # Screen shake on explosion
Zoom Controls
extends Camera2D
@export var zoom_speed := 0.1
@export var min_zoom := 0.5
@export var max_zoom := 2.0
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
zoom_in()
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
zoom_out()
func zoom_in() -> void:
zoom = zoom.move_toward(
Vector2.ONE * max_zoom,
zoom_speed
)
func zoom_out() -> void:
zoom = zoom.move_toward(
Vector2.ONE * min_zoom,
zoom_speed
)
Look-Ahead Camera
extends Camera2D
@export var look_ahead_distance := 50.0
@export var target: CharacterBody2D
func _process(delta: float) -> void:
if target:
var look_ahead := target.velocity.normalized() * look_ahead_distance
global_position = target.global_position + look_ahead
Split-Screen (Multiple Cameras)
# Player 1 Camera
@onready var cam1: Camera2D = $Player1/Camera2D
# Player 2 Camera
@onready var cam2: Camera2D = $Player2/Camera2D
func _ready() -> void:
# Split viewport
cam1.anchor_mode = Camera2D.ANCHOR_MODE_DRAG_CENTER
cam2.anchor_mode = Camera2D.ANCHOR_MODE_DRAG_CENTER
Camera3D Patterns
Third-Person Camera
extends Camera3D
@export var target: Node3D
@export var distance := 5.0
@export var height := 2.0
@export var rotation_speed := 3.0
var rotation_angle := 0.0
func _process(delta: float) -> void:
if not target:
return
# Rotate around target
rotation_angle += Input.get_axis("camera_left", "camera_right") * rotation_speed * delta
# Calculate position
var offset := Vector3(
sin(rotation_angle) * distance,
height,
cos(rotation_angle) * distance
)
global_position = target.global_position + offset
look_at(target.global_position, Vector3.UP)
First-Person Camera
extends Camera3D
@export var mouse_sensitivity := 0.002
@export var max_pitch := deg_to_rad(80)
var pitch := 0.0
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
# Yaw (horizontal)
get_parent().rotate_y(-event.relative.x * mouse_sensitivity)
# Pitch (vertical)
pitch -= event.relative.y * mouse_sensitivity
pitch = clamp(pitch, -max_pitch, max_pitch)
rotation.x = pitch
Camera Transitions
# Smooth camera position change
func move_to_position(target_pos: Vector2, duration: float = 1.0) -> void:
var tween := create_tween()
tween.tween_property(self, "global_position", target_pos, duration)
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_CUBIC)
Cinematic Cameras
# Camera path following
extends Path2D
@onready var path_follow: PathFollow2D = $PathFollow2D
@onready var camera: Camera2D = $PathFollow2D/Camera2D
func play_cutscene(duration: float) -> void:
var tween := create_tween()
tween.tween_property(path_follow, "progress_ratio", 1.0, duration)
await tween.finished
Best Practices
1. One Active Camera
# Only one Camera2D should be enabled at a time
# Others should have enabled = false
2. Parent Camera to Player
# Scene structure:
# Player (CharacterBody2D)
# └─ Camera2D
3. Use Anchors for UI
# Camera doesn't affect UI positioned with anchors
# UI stays in screen space
4. Deadzone for Platformers
extends Camera2D
func _ready() -> void:
drag_horizontal_enabled = true
drag_vertical_enabled = true
drag_left_margin = 0.3
drag_right_margin = 0.3
Reference
Related
- Master Skill: godot-master