Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
thedivergentai avatar

Godot Camera Systems

  • 280 installs
  • 454 repo stars
  • Updated July 28, 2026
  • thedivergentai/gd-agentic-skills

Use godot-camera-systems for development tasks

About

godot-camera-systems: A skill for development. This provides functionality for development workflows.

  • godot-camera-systems

Godot Camera Systems by the numbers

  • 280 all-time installs (skills.sh)
  • +27 installs in the week ending Aug 5, 2026 (Skillselion tracking)
  • Ranked #1,402 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-camera-systems

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs280
repo stars454
Last updatedJuly 28, 2026
Repositorythedivergentai/gd-agentic-skills

What it does

Use godot-camera-systems for development tasks

Files

SKILL.mdMarkdownGitHub ↗

Camera Systems

Expert guidance for creating smooth, responsive cameras in 2D and 3D games.

NEVER Do

  • NEVER use `global_position = target.global_position` every frame — Instant position matching causes jittery movement. Use lerp() or position_smoothing_enabled = true [12].
  • NEVER use `offset` for permanent camera positioningoffset is for shake, sway, or temporary recoil effects only. Use position for permanent framing to avoid logic conflicts [14].
  • NEVER forget `limit_smoothed = true` for `Camera2D` — Hard boundaries cause jarring visual stops. Smoothing against limits ensures a professional feel [13].
  • NEVER enable multiple `Camera2D` nodes in the same viewport simultaneously — Only the last enabled camera takes precedence. Explicitly disable inactive cameras [15].
  • NEVER use `SpringArm3D` without a collision mask — It will clip through terrain and walls. Set it to the world/environment layer [16].
  • NEVER implement screen shake by randomizing `position` directly — This overwrites follow-logic. Use offset or a dedicated Trauma/Noise system to Layer shake over the follow-position [27, 28].
  • NEVER parent the Camera directly to a high-speed physics body — Physics stutter or parent rotation will cause motion sickness. Use RemoteTransform2D/3D with rotation sync disabled for a stable view [30].
  • NEVER use `look_at()` in 3D without a fallback for the 'Up' vector — If the target is directly above/below, the camera will flip wildly. Use guards or Quaternion math for vertical tracking.
  • NEVER rely on `SubViewport` defaults for Mini-maps — Viewports are expensive; explicitly set render_target_update_mode to UPDATE_WHEN_VISIBLE or a fixed lower framerate to save GPU [156].
  • NEVER use linear interpolation for Zoom — It feels 'robotic'. Use exponential lerp or a Tween with TRANS_CUBIC for a more natural tactical feel.

---

Available Scripts

MANDATORY: Read before implementing camera behaviors.

camera_shake_trauma_pro.gd

Advanced noise-based screenshake (Trauma system) for organic, non-jittery explosions and impacts.

cinematic_framing_logic.gd

Rule of Thirds and Lead Room management in code, ensuring high-quality cinematic composition.

camera_state_machine.gd

Managing transitions between 'Follow', 'Static', and 'Cinematic' camera states with Tweens.

minimap_viewport_manager.gd

SubViewport optimization for Mini-maps and UI overlays. Reduces render updates for better FPS.

split_screen_setup.gd

Dynamic split-screen architecture for local multiplayer, handling viewport stretching and audio listeners.

remote_transform_decoupling.gd

Decoupling camera position from player rotation/scale using RemoteTransform2D for high-speed stability.

zoom_damping_controller.gd

Non-linear, smooth zoom logic with tactical overview bounds and mouse-wheel support.

spring_lerp_camera_3d.gd

Physics-stable 3D follow camera using spring-mass interpolation to reduce follow-latency jitter.

first_person_sway.gd

Procedural 8-figure head bob and weapon sway logic for immersive First-Person systems.

deadzone_drag_margins.gd

Platformer-specific deadzone management using code to control follow-margins and drag-center behavior.

---

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

Expert Camera Architectures

1. Camera Framing Box (Multi-Target Framing)

To handle multiple targets in a single frame (e.g., Smash Bros or Local-Coop), calculate the AABB (Axis-Aligned Bounding Box) of all targets. Interpolate the camera's global_position to the center of the box and adjust the zoom (2D) or distance (3D) to encapsulate the entire box with a padding margin.

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
    
    # 1. Calculate the bounding box of all targets.
    var rect := Rect2(targets[0].global_position, Vector2.ZERO)
    for target in targets:
        rect = rect.expand(target.global_position)
    
    # 2. Add padding.
    rect = rect.grow(margin)
    
    # 3. Position the camera at the center.
    global_position = rect.get_center()
    
    # 4. Calculate required zoom level to fit the box.
    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

2. Camera Raycasting (SpringArm3D / RayCast3D)

To prevent the camera from clipping through terrain in 3D, use a SpringArm3D node. For custom camera logic, query the physics space directly using intersect_ray(). This allows you to smoothly interpolate the camera closer to the player when an obstruction (e.g., a wall) is detected between the camera's desired position and the target.

class_name OcclusionAwareCamera3D extends Camera3D
## Prevents camera clipping via manual physics space raycasting.

@export var target: Node3D
@export var ideal_distance: float = 5.0

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)
    
    # Query for obstructions between the target and the desired camera position.
    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():
        # Move the camera to the hit position (with a small offset to prevent clipping).
        global_position = result.position + result.normal * 0.2
    else:
        global_position = desired_pos
        
    look_at(target.global_position)

3. Screenshake Audit (Trauma Decay Profiler)

A "Screenshake Audit" involves visualizing the trauma-decay curve over time. By plotting the current_trauma value to a graph using the CanvasItem._draw() API, you can precisely tune the "punchiness" and "decay duration" of impacts to ensure they feel intentional rather than random noise.

class_name TraumaDebugger extends Node2D
## Visualizes the decay curve of a trauma-based shake system.

@export var camera: ProceduralScreenShake
var _history: PackedFloat32Array = []

func _process(_delta: float) -> void:
    if not camera: return
    
    _history.append(camera.get_current_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)

Reference

Related

  • Master Skill: godot-master

Related skills

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.