Game Feel · Blog
Manette de jeu en gros plan sur fond sombre
Photo: Furkan Salihoğlu / Pexels
Sommaire7
  1. Pourquoi l'approche naïve ne marche pas
  2. Pourquoi ça marche
  3. La recette
  4. Le code
  5. Conseils de réglage et erreurs courantes
  6. Exemples dans des jeux connus
  7. FAQ

Le screen shake est la modification la plus rapide pour donner de la vie à un jeu. Sans lui, les balles font l'effet de pichenettes, les explosions ressemblent à des pétards, et les morts à un écran de pause. Avec lui, chaque impact a du poids. Voici comment implémenter la méthode trauma dans Godot 4 — l'approche la plus flexible, celle que l'on retrouve sous le capot de nombreux jeux indés.

trauma²pourquoi les petits coups restent discrets
0,8 / sdécroissance de départ
80 × 60 pxdécalage max à trauma 1
15 minpour l'ajouter à votre jeu
Mouvement de la caméra : trauma linéaire ou au carré (% du max)
LinéaireAu carré (trauma²)
Trauma 0,2
20 %
4 %
Trauma 0,4
40 %
16 %
Trauma 0,6
60 %
36 %
Trauma 0,8
80 %
64 %
Trauma 1,0
100 %
100 %
courbe trauma² utilisée dans le code ci-dessous
Valeur add_trauma() de départ par événement
Clic sur un bouton
0,05
Coup léger / balle
0,25
Coup lourd / mêlée
0,4
Explosion proche
0,7
Énorme explosion / mort
1
valeurs de départ du tableau ci-dessous (haut de chaque fourchette)
Borne d'arcade illuminée dans une salle sombre
Borne d'arcade illuminée dans une salle sombre — Photo: Dan Butler / Pexels

Pourquoi l'approche naïve ne marche pas

Le premier screen shake que l'on écrit ressemble à ceci : à l'impact, on choisit un offset aléatoire, on l'applique quelques frames, terminé. Le résultat est un clignotement saccadé qui ressemble à un bug graphique plutôt qu'à un impact. Deux problèmes :

  1. Des valeurs aléatoires frame par frame créent du jitter, pas du mouvement. La caméra se téléporte au lieu de balancer.
  2. Une amplitude fixe donne la même sensation à chaque coup. Un poing et une grenade reçoivent le même coup d'épaule.

La méthode trauma résout les deux.

Pourquoi ça marche

Le modèle trauma, popularisé dans la communauté indé via le talk de Jan Willem Nijman (Vlambeer) en 2013 intitulé "The Art of Screenshake" aux INDIGO Classes (YouTube, 2013), repose sur trois idées :

  • Accumulation. Le trauma est une valeur de 0 à 1 qui s'additionne. Trois tirs consécutifs font monter le shake ; il ne se réinitialise pas à chaque impact.
  • Mise au carré. Le déplacement réel est trauma² (ou trauma³). À 0,3 de trauma, la caméra bouge à peine (0,09 du maximum) ; à 1,0 elle atteint toute sa force. Les petits chocs restent discrets ; les gros sont violents.
  • Bruit lissé. Au lieu d'un randf() à chaque frame, la caméra échantillonne une texture FastNoiseLite le long d'un axe en mouvement. Les frames consécutives sont corrélées — la caméra balance, elle ne tressaille pas.

Le résultat s'adapte naturellement : une balle ajoute 0,2 de trauma, une explosion 0,7, une bombe plein écran 1,0. Chaque événement mérite son propre drame.

La recette

Tableau des paramètres

ParamètreValeur de départPlage utileCe que ça change
decay0.80,5 – 2,5Trauma perdu par seconde ; plus élevé = fondu plus rapide
max_offsetVector2(80, 60)30 – 150 pxDéplacement maximal en pixels à trauma complet
max_roll0.08 rad (~4,5°)0,0 – 0,15Rotation à trauma complet ; à garder bas pour éviter la nausée
trauma_power22 – 3Exposant ; 3 rend les petits chocs quasi invisibles
noise.frequency0.50,3 – 1,5Plus bas = oscillation plus lente et molle ; plus haut = énergie nerveuse

Quelle valeur de trauma par événement

ÉvénementValeur pour add_trauma()
Pression d'un bouton UI0,05
Impact léger / balle0,15 – 0,25
Impact lourd / mêlée0,3 – 0,4
Explosion proche0,5 – 0,7
Grosse explosion / mort0,8 – 1,0

Le code

Attachez ce script à votre nœud Camera2D. Il a besoin d'une ressource FastNoiseLite (créée au démarrage) et de rien d'autre — aucun nœud supplémentaire nécessaire.

# TraumaCamera2D.gd
# À attacher à un nœud Camera2D.
# Appelez add_trauma(valeur) depuis n'importe quel endroit à l'impact.

extends Camera2D

## Vitesse de décroissance du trauma par seconde (0 = jamais, 2.5 = très rapide).
@export var decay: float = 0.8

## Déplacement maximal en pixels à trauma complet.
@export var max_offset: Vector2 = Vector2(80.0, 60.0)

## Rotation maximale à trauma complet (~4,5 degrés). À garder sous 0,15.
@export var max_roll: float = 0.08

## Exposant appliqué au trauma avant le déplacement. 2 = quadratique, 3 = cubique.
@export_range(2, 3, 1) var trauma_power: int = 2

# Valeur de trauma actuelle, de 0.0 (calme) à 1.0 (shake complet).
var trauma: float = 0.0

# La position de bruit avance à chaque frame pour obtenir une oscillation continue.
var _noise_t: float = 0.0

@onready var _noise: FastNoiseLite = _make_noise()


func _make_noise() -> FastNoiseLite:
    var n := FastNoiseLite.new()
    n.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
    n.seed = randi()
    n.frequency = 0.5
    return n


## À appeler à chaque impact. La valeur est comprise entre 0.0 et 1.0.
## Les valeurs s'accumulent : deux appels à 0.3 donnent 0.6 de trauma (plafonné à 1.0).
func add_trauma(amount: float) -> void:
    trauma = minf(trauma + amount, 1.0)


func _process(delta: float) -> void:
    if trauma <= 0.0:
        return

    # Décroissance linéaire du trauma dans le temps.
    trauma = maxf(trauma - decay * delta, 0.0)

    # Mise à la puissance : les petits traumas restent discrets.
    var t: float = pow(trauma, trauma_power)

    # Avancement de la position de bruit (50 est une bonne cadence).
    _noise_t += delta * 50.0

    # Échantillonnage de trois canaux de bruit indépendants pour x, y et la rotation.
    # Les décalages de seed assurent que les canaux ne sont pas corrélés.
    rotation    = max_roll     * t * _noise.get_noise_2d(0.0,    _noise_t)
    offset.x    = max_offset.x * t * _noise.get_noise_2d(1000.0, _noise_t)
    offset.y    = max_offset.y * t * _noise.get_noise_2d(2000.0, _noise_t)

    # Réinitialisation quand le trauma est épuisé.
    if trauma <= 0.0:
        offset   = Vector2.ZERO
        rotation = 0.0

Où le placer

  1. Sélectionnez votre nœud Camera2D dans l'arbre de scène.
  2. Cliquez sur Attacher un script et collez le code ci-dessus (ou sauvegardez-le sous TraumaCamera2D.gd).
  3. C'est tout — pas d'autoload, pas de singleton.

Déclencher le shake depuis d'autres scripts

# Option A : référence directe (la plus simple)
@onready var camera: TraumaCamera2D = $Camera2D

func _on_enemy_hit() -> void:
    camera.add_trauma(0.25)

# Option B : via un groupe (utile quand la caméra est dans une scène différente)
# Dans _ready() : add_to_group("trauma_camera")
# Depuis n'importe où :
func _on_explosion() -> void:
    for cam in get_tree().get_nodes_in_group("trauma_camera"):
        cam.add_trauma(0.7)
Développeur de jeu codant devant un écran avec du code affiché
Développeur de jeu codant devant un écran avec du code affiché — Photo: Lee Campbell / Pexels

Conseils de réglage et erreurs courantes

Commencez plus petit que vous ne le pensez. Testez avec add_trauma(0.2) pour un impact standard avant de monter à 0,5. Les joueurs assis près de leur écran sont plus sensibles au mouvement que vous ne l'êtes en développement.

Ajustez decay, pas max_offset. La durée du shake compte plus que la distance. Un impact à 0,3 de trauma qui dure 0,8 seconde semble plus lourd qu'un qui saute à 50 px et disparaît en 2 frames.

Plafonnez la rotation aggressivement. max_roll = 0.08 (environ 4,5°) est le bon compromis pour la plupart des jeux 2D. Au-delà de 0,15, les joueurs commencent à ressentir le mal des transports, notamment sur mobile.

Ne pas appeler add_trauma à chaque frame pour les effets continus. Un lance-flammes appelant add_trauma(0.3) à 60 FPS verrouille la caméra à 1,0 indéfiniment. Utilisez add_trauma(0.3 * delta) pour les effets soutenus.

Réinitialisez l'offset quand le trauma atteint zéro. Le code ci-dessus le fait déjà. L'oublier laisse la caméra définitivement décalée par la valeur de bruit sur laquelle elle s'est arrêtée.

Les caméras avec interpolation physique demandent une attention particulière. Si votre projet utilise l'interpolation physique, appliquez le shake après que l'interpolation s'est exécutée. Consultez le fil de discussion sur le forum Godot pour les détails.

Exemples dans des jeux connus

Nuclear Throne (Vlambeer, 2015) — Chaque arme a sa propre signature de shake : le pistolet nudge, le fusil à pompe donne un coup, le laser vibre. Dans son talk de 2013 aux INDIGO Classes (YouTube), Nijman montre le moment précis où le screen shake a été ajouté au prototype, et la réaction immédiate qu'il a provoquée. L'idée centrale — l'intensité proportionnelle au poids de l'arme — se traduit directement par le tableau de trauma ci-dessus.

Celeste (Maddy Thorson & Noel Berry, 2018) — Utilise un shake extrêmement retenu. Le dash à peine bouge la caméra ; la mort est la seule action qui déclench un shake important. Cette sobriété volontaire garde le jeu lisible à grande vitesse — quand le shake arrive, il signifie quelque chose. Le talk GDC 2017 sur le level design (GDC Vault) aborde la façon dont chaque feedback a été maintenu minimal au service de la platforme de précision.

Hollow Knight (Team Cherry, 2017) — Les affrontements et les parades utilisent un shake bref et vif couplé à un freeze frame (hit-stop). La combinaison est ce qui fait atterrir les coups : le freeze retient la frame au moment de l'impact, puis le shake annonce le résultat de la collision. Si vous construisez un jeu d'action, screen shake et hit-stop vont ensemble.

Jeu en pixel art avec personnages colorés en side-scrolling
Jeu en pixel art avec personnages colorés en side-scrolling — Photo: Forse Jorre / Pexels

FAQ

Le screen shake nuit-il à l'accessibilité ?

Oui, pour certains joueurs — les troubles vestibulaires rendent le mouvement de caméra réellement inconfortable, voire douloureux. Ajoutez un curseur Screen Shake (0–100 %) dans vos options et multipliez la valeur d'entrée de add_trauma() par ce facteur. Dix lignes de code pour couvrir tous vos joueurs.

Dois-je utiliser Camera2D.offset ou Camera2D.position ?

Utilisez offset. Modifier position entre en conflit avec toute logique de suivi ou de lissage sur la même caméra. offset s'ajoute au-dessus de la position calculée, sans interférer.

Mon shake a l'air robotique / trop régulier. Que se passe-t-il ?

La fréquence du bruit est probablement trop élevée. Essayez n.frequency = 0.3 dans _make_noise(). Vérifiez aussi que vous incrémentez _noise_t dans _process() et non dans _physics_process() — la physique tourne à 60 Hz fixe, ce qui crée un rythme métronomique.

Est-ce que ça marche en 3D ?

Oui, mais rotation devient trois axes et offset une translation 3D. Étendez Camera3D à la place de Camera2D, remplacez offset par h_offset / v_offset (ou surchargez la base de la caméra), et le reste de la logique reste identique.

Comment combiner ça avec un hit-stop ?

Déclenchez add_trauma() immédiatement à l'impact. Pour le hit-stop, passez Engine.time_scale = 0.05 pendant 60 à 100 ms, puis restaurez-le à 1.0. Faites tourner le timer avec process_callback = Timer.TIMER_PROCESS_PHYSICS et assurez-vous que votre caméra de shake utilise _process() (pas _physics_process()), pour que le shake continue pendant le freeze.


Le screen shake est peu coûteux, rapide à mettre en place, et l'un des meilleurs investissements pour le game feel. Collez le script ci-dessus, appelez add_trauma(0.25) sur votre prochain impact de balle, et vous ne reviendrez pas en arrière. Une fois que vous ressentez la différence, vous voudrez la boîte à outils complète — explorez le Game Feel Cookbook pour le hit-stop, le squash & stretch, et tout ce qui fait dire aux joueurs "ça a l'air vraiment bon."