---
description: Configure 2D camera — smooth follow, zoom, area limits, cinematic mode
user-invocable: false
---
# /godot-camera — Caméra 2D : suivi, zoom, limites, cinematic

> **Synonymes** : camera, caméra, camera2d, follow, suivi, zoom, limit, limites, dead zone, cinematic

Tu es un expert caméra pour Godot 4.x. Ton rôle : implémenter des comportements de caméra 2D satisfaisants — smooth follow, dead zones, limites par zone, zoom dynamique, multi-target, mode cinematic.

---

## Sommaire

1. Camera2D — Fondamentaux
2. Smooth Follow
3. Limites par zone
4. Zoom dynamique
5. Multi-target (optionnel)
6. Cinematic Mode
7. Intégration avec /godot-juice
8. Checklist Review
9. Pièges courants

---

## Philosophie

- **Invisible quand ça marche** — le joueur ne doit pas penser à la caméra
- **Smooth, pas laggy** — le suivi doit être fluide, pas en retard
- **Au service du gameplay** — la caméra montre ce qui compte
- **Prévisible** — pas de mouvements brusques sauf intention (shake)

---

## 1. Camera2D — Fondamentaux

### Properties essentielles

| Propriété | Description | Défaut |
|-----------|------------|--------|
| `enabled` | Active cette caméra | true |
| `anchor_mode` | FIXED_TOP_LEFT ou DRAG_CENTER | DRAG_CENTER |
| `position_smoothing_enabled` | Smooth follow built-in | false |
| `position_smoothing_speed` | Vitesse du smoothing (plus haut = plus réactif) | 5.0 |
| `drag_horizontal_enabled` | Dead zone horizontale | false |
| `drag_vertical_enabled` | Dead zone verticale | false |
| `limit_left/right/top/bottom` | Limites de la caméra | -10000000 |
| `limit_smoothed` | Smooth transition aux limites | false |
| `zoom` | Zoom (Vector2, > 1 = zoom in, < 1 = zoom out) | Vector2(1,1) |

### Presets par type de jeu

| Preset | Smoothing | Dead zone | Limites | Usage |
|--------|-----------|-----------|---------|-------|
| Tight follow | Off | Off | Off | Action rapide, precision platformer |
| Smooth follow | On, speed 5-8 | Off | On | Platformer standard |
| Lazy camera | On, speed 2-3 | On | On | Exploration, RPG |
| Fixed | Off | Off | On | Puzzle, single-screen |
| Cinematic | On, speed 1-2 | Off | Off | Cutscenes |

---

## 2. Smooth Follow

### Built-in smoothing vs manual lerp

**Built-in** : simple, convient dans la plupart des cas
```gdscript
position_smoothing_enabled = true
position_smoothing_speed = 5.0
```

**Manual lerp** : plus de contrôle, nécessaire pour look-ahead, multi-target

### Pattern : Smooth follow with offset

```gdscript
## camera_follow.gd — Attach to Camera2D
extends Camera2D

@export var target_path: NodePath
@export var offset: Vector2 = Vector2.ZERO
@export var smoothing: float = 5.0

var target: Node2D

func _ready():
	target = get_node(target_path)
	position_smoothing_enabled = false  # We handle it manually

func _process(delta: float):
	if not target:
		return
	var target_pos = target.global_position + offset
	global_position = global_position.lerp(target_pos, 1.0 - exp(-smoothing * delta))
```

**Note** : utiliser `1.0 - exp(-speed * delta)` au lieu de `delta * speed` pour un lerp frame-rate independent.

---

### Dead zone (drag margins)

```gdscript
drag_horizontal_enabled = true
drag_left_margin = 0.2
drag_right_margin = 0.2
drag_vertical_enabled = true
drag_top_margin = 0.2
drag_bottom_margin = 0.2
```

La caméra ne bouge que quand le target sort de la dead zone. Idéal pour l'exploration où les petits mouvements ne doivent pas déclencher de mouvement caméra.

---

### Pattern : Look-ahead

Offset vers la direction du mouvement pour anticiper.

```gdscript
@export var look_ahead_factor: float = 50.0
@export var look_ahead_smoothing: float = 3.0

var _look_ahead_offset: Vector2 = Vector2.ZERO

func _process(delta: float):
	if not target:
		return

	# Look ahead based on target velocity
	var velocity = target.velocity if "velocity" in target else Vector2.ZERO
	var target_look = velocity.normalized() * look_ahead_factor
	_look_ahead_offset = _look_ahead_offset.lerp(target_look, 1.0 - exp(-look_ahead_smoothing * delta))

	var target_pos = target.global_position + offset + _look_ahead_offset
	global_position = global_position.lerp(target_pos, 1.0 - exp(-smoothing * delta))
```

---

## 3. Limites par zone

### Hard boundaries

```gdscript
camera.limit_left = -500
camera.limit_right = 500
camera.limit_top = -300
camera.limit_bottom = 300
camera.limit_smoothed = true  # Smooth stop at limits (otherwise snaps)
```

---

### Pattern : Area2D zone boundaries

Area2D qui reconfigure les limites de la caméra à l'entrée.

```gdscript
## camera_zone.gd — Attach to Area2D with CollisionShape2D
extends Area2D

@export var camera_limits: Rect2 = Rect2(-500, -300, 1000, 600)

func _ready():
	body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node2D):
	var camera = body.get_viewport().get_camera_2d()
	if not camera:
		return
	camera.limit_left = int(camera_limits.position.x)
	camera.limit_top = int(camera_limits.position.y)
	camera.limit_right = int(camera_limits.position.x + camera_limits.size.x)
	camera.limit_bottom = int(camera_limits.position.y + camera_limits.size.y)
```

---

### Smooth transition entre zones

```gdscript
func _on_body_entered(body: Node2D):
	var camera = body.get_viewport().get_camera_2d()
	if not camera:
		return

	var tween = create_tween().set_parallel(true)
	tween.tween_property(camera, "limit_left", int(camera_limits.position.x), 0.5)
	tween.tween_property(camera, "limit_top", int(camera_limits.position.y), 0.5)
	tween.tween_property(camera, "limit_right", int(camera_limits.end.x), 0.5)
	tween.tween_property(camera, "limit_bottom", int(camera_limits.end.y), 0.5)
```

---

## 4. Zoom dynamique

### Tween sur camera.zoom

```gdscript
func zoom_to(target_zoom: float, duration: float = 0.3):
	var tween = create_tween()
	tween.tween_property(self, "zoom", Vector2(target_zoom, target_zoom), duration) \
		.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
```

---

### Pattern : Zoom contextuel

Zoom out en combat, zoom in pour dialogue.

```gdscript
func _on_combat_started():
	zoom_to(0.8, 0.5)  # Zoom out to see more

func _on_dialogue_started():
	zoom_to(1.5, 0.3)  # Zoom in on characters

func _on_event_ended():
	zoom_to(1.0, 0.5)  # Return to default
```

---

### Pattern : Zoom progressif basé sur la vitesse

```gdscript
@export var min_zoom: float = 0.7
@export var max_zoom: float = 1.2
@export var speed_threshold: float = 400.0
@export var zoom_smoothing: float = 2.0

func _process(delta: float):
	if not target or not "velocity" in target:
		return

	var speed_ratio = clampf(target.velocity.length() / speed_threshold, 0.0, 1.0)
	var target_zoom = lerpf(max_zoom, min_zoom, speed_ratio)
	var current = zoom.x
	var new_zoom = lerpf(current, target_zoom, 1.0 - exp(-zoom_smoothing * delta))
	zoom = Vector2(new_zoom, new_zoom)
```

---

## 5. Multi-target (optional)

Centre entre 2+ targets (co-op, boss + player), auto-zoom pour garder tout visible.

```gdscript
## multi_target_camera.gd
extends Camera2D

@export var targets: Array[NodePath] = []
@export var smoothing: float = 5.0
@export var min_zoom: float = 0.5
@export var max_zoom: float = 1.5
@export var margin: float = 100.0

var _target_nodes: Array[Node2D] = []

func _ready():
	for path in targets:
		var node = get_node_or_null(path)
		if node is Node2D:
			_target_nodes.append(node)

func _process(delta: float):
	if _target_nodes.is_empty():
		return

	# Calculate bounding box of all targets
	var min_pos = _target_nodes[0].global_position
	var max_pos = min_pos

	for target in _target_nodes:
		min_pos.x = minf(min_pos.x, target.global_position.x)
		min_pos.y = minf(min_pos.y, target.global_position.y)
		max_pos.x = maxf(max_pos.x, target.global_position.x)
		max_pos.y = maxf(max_pos.y, target.global_position.y)

	# Center point
	var center = (min_pos + max_pos) / 2.0
	global_position = global_position.lerp(center, 1.0 - exp(-smoothing * delta))

	# Zoom to fit all targets
	var viewport_size = get_viewport_rect().size
	var spread = max_pos - min_pos + Vector2(margin * 2, margin * 2)
	var zoom_x = viewport_size.x / maxf(spread.x, 1.0)
	var zoom_y = viewport_size.y / maxf(spread.y, 1.0)
	var target_zoom = clampf(minf(zoom_x, zoom_y), min_zoom, max_zoom)
	var current_zoom = zoom.x
	var new_zoom = lerpf(current_zoom, target_zoom, 1.0 - exp(-smoothing * delta))
	zoom = Vector2(new_zoom, new_zoom)
```

---

## 6. Cinematic Mode

Déconnecte la caméra du joueur temporairement. Pan vers un point d'intérêt, pause, retour.

```gdscript
## cinematic_camera.gd — Extend the follow camera
extends Camera2D

signal cinematic_finished

var _cinematic_active := false
var _return_target: Node2D

func pan_to(world_pos: Vector2, duration: float = 1.0, hold: float = 1.0):
	_cinematic_active = true
	_return_target = null  # Will be set by the follow script

	var tween = create_tween()
	# Pan to target
	tween.tween_property(self, "global_position", world_pos, duration) \
		.set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)
	# Hold
	tween.tween_interval(hold)
	# Signal done
	tween.tween_callback(_end_cinematic)

func pan_to_and_return(world_pos: Vector2, return_target: Node2D, duration: float = 1.0, hold: float = 1.0):
	_return_target = return_target
	_cinematic_active = true

	var tween = create_tween()
	tween.tween_property(self, "global_position", world_pos, duration) \
		.set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)
	tween.tween_interval(hold)
	tween.tween_property(self, "global_position", return_target.global_position, duration) \
		.set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)
	tween.tween_callback(_end_cinematic)

func _end_cinematic():
	_cinematic_active = false
	cinematic_finished.emit()

func is_in_cinematic() -> bool:
	return _cinematic_active
```

---

### Usage

```gdscript
# In level script
func _on_door_unlocked():
	var camera = get_viewport().get_camera_2d()
	camera.pan_to_and_return($Door.global_position, $Player, 1.0, 2.0)
	await camera.cinematic_finished
	# Resume gameplay
```

---

## 7. Intégration avec /godot-juice

Le shake est dans `/godot-juice` (ne pas dupliquer). Voici comment combiner shake + smooth follow + limits.

### Architecture des propriétés

```
Position (follow) + Offset (shake + look_ahead) + Limits (boundaries) + Zoom (dynamic)
```

Chaque système possède sa propriété — pas de conflit :
- `camera.offset` pour shake (géré par `/godot-juice` camera_shake.gd)
- `camera.global_position` pour follow (géré par follow script)
- `camera.limit_*` pour boundaries
- `camera.zoom` pour dynamic zoom

---

### Désactiver le shake pendant les cinématiques

```gdscript
func add_trauma(amount: float):
	if is_in_cinematic():
		return
	trauma = minf(1.0, trauma + amount)
```

---

## 8. Checklist Review

| Check | Description |
|-------|-------------|
| Camera uses `offset` for shake (not `position`)? | Sinon le shake déplace la caméra définitivement |
| Smoothing is frame-rate independent (exp-based lerp)? | Utiliser `1.0 - exp(-speed * delta)` |
| Limits set for all level boundaries? | Évite que la caméra ne sorte du niveau |
| `limit_smoothed` enabled for smooth stops? | Transition douce aux bords |
| Dead zone configured for the game type? | Tight pour action, lazy pour exploration |
| Zoom values clamped (min/max)? | Évite les zooms extrêmes |
| Cinematic mode disables player input? | Le joueur ne doit pas bouger pendant une cinematic |
| Camera child of scene root (not player)? | Sinon le shake déplace le joueur |

---

## 9. Pièges courants

| Piège | Symptôme | Solution |
|-------|----------|----------|
| Camera enfant du joueur | Shake déplace le joueur | Camera en sibling, follow via script |
| Shake sur position au lieu de offset | Caméra se décale définitivement | Toujours utiliser `offset` |
| Lerp avec delta brut | Comportement différent selon FPS | Utiliser `1.0 - exp(-speed * delta)` |
| Zoom Vector2(2,2) | Zoom IN (pas out) | Plus grand = plus zoomé, plus petit = dézoomé |
| Limites en float | Jitter sub-pixel | Cast en int (`int(limit)`) |
| Deux Camera2D actives | Une seule gagne, comportement imprévisible | Vérifier `enabled` / `make_current()` |
| Zoom 1.0 sur pixel art | Sprites microscopiques (16px = 0.8% de 1920px) | Pixel art 16px base → zoom ~3.0 (visible area ~640x360). Formule : `zoom = viewport_width / target_visible_width` |
| Camera local position ≠ (0,0) | Drift au démarrage, pan visible | Camera locale à (0,0) dans la source scene. Le script smooth follow gère `global_position` |
| Viewport origin confusion | Player spawne en haut à gauche | (0,0) = top-left en Godot 2D. Centre viewport 1920x1080 = (960, 540). Positionner le jeu autour du centre |

---

## See also

| Skill | Quand l'utiliser |
|-------|-----------------|
| `/godot-juice` | Screenshake (Section 2), le shake utilise camera.offset |
| `/godot-scene` | Construction de scène, placement de la caméra |
| `/godot-vfx` | Parallax backgrounds (profondeur visuelle) |

| Recette | Contenu marinade |
|---------|-----------------|
| `recipes/hit_feedback.md` | Screenshake values par impact weight, directional vs random, decay curves |
| `recipes/turn_combat_feel.md` | Combat camera choreography, zoom per action phase, close-up technique |
