Le Game Feel
Cookbook
20 petites astuces qui rendent n'importe quel jeu génial à jouer — ce que fait chacune, pourquoi les joueurs la sentent, les valeurs de départ exactes, et du code Godot 4 court à coller dès aujourd'hui.
▶ Voir la vidéo complète · les 20 astuces en détail
Chapitre 1 · commente PUNCH sous le Short
Rendre les hits percutants
Un hit qui ne fait que retirer des PV ne se sent pas. Ces cinq couches se déclenchent toutes sur la même frame d'impact — ensemble, elles donnent du poids au coup.
▶ Voir le Short PUNCHÉTAPE 1
Flash
Passez l'ennemi en blanc pur pendant quelques frames à l'impact.
Pourquoi ça marche — L'œil capte le contraste instantanément : le joueur sait que le coup a touché avant même de lire un chiffre.
- Durée : 50–80 ms (3–5 frames à 60 fps)
- Flash = 100 % blanc, sans fondu entrant
- Rendez le matériau local à la scène, sinon tous les ennemis flashent en même temps
GDScript · Godot 4
# hit_flash.gdshader
shader_type canvas_item;
uniform float flash : hint_range(0.0, 1.0) = 0.0;
void fragment() {
COLOR.rgb = mix(COLOR.rgb, vec3(1.0), flash);
}
# enemy.gd
@onready var sprite: Sprite2D = $Sprite2D
func flash() -> void:
var mat := sprite.material as ShaderMaterial
mat.set_shader_parameter("flash", 1.0)
await get_tree().create_timer(0.06).timeout
mat.set_shader_parameter("flash", 0.0)ÉTAPE 2
Son
Un son d'impact court et sec joué sur la frame exacte du contact.
Pourquoi ça marche — L'audio vend le poids mieux que le visuel. Un son en retard de 50 ms rend le hit mou.
- Attaque ≤ 5 ms, durée totale 80–200 ms
- Pitch aléatoire 0,9–1,1 pour que 10 coups d'affilée ne sonnent pas robotiques
- Superposez un grave sourd + un aigu claquant
GDScript · Godot 4
@onready var hit_sfx: AudioStreamPlayer2D = $HitSfx
func play_hit_sound() -> void:
hit_sfx.pitch_scale = randf_range(0.9, 1.1)
hit_sfx.play()ÉTAPE 3
Particules
Une gerbe d'étincelles projetée dans la direction du hit.
Pourquoi ça marche — Les débris directionnels indiquent au cerveau où la force est allée — le hit a un vecteur, pas seulement un flash.
- 8–16 particules, one_shot, explosiveness 1.0
- Spread 20–30°, durée de vie 0,2–0,35 s
- Vitesse initiale 250–450 px/s avec amortissement
GDScript · Godot 4
const SPARKS := preload("res://fx/sparks.tscn") # CPUParticles2D, one_shot
func spawn_sparks(pos: Vector2, hit_dir: Vector2) -> void:
var p := SPARKS.instantiate() as CPUParticles2D
p.global_position = pos
p.rotation = hit_dir.angle() # les particules émettent le long de +X
get_tree().current_scene.add_child(p)
p.emitting = true
p.finished.connect(p.queue_free)ÉTAPE 4
Knockback
Repoussez l'ennemi loin du hit, et donnez une légère impulsion en arrière à l'attaquant.
Pourquoi ça marche — Le mouvement est la preuve de la force. Le petit recul du joueur donne l'impression que deux corps se sont vraiment percutés.
- Ennemi : 350–500 px/s, décroît à 0 en ~0,15 s
- Recul joueur : 20–30 % du knockback ennemi
- Arme plus grande = knockback plus fort, pas plus de chiffres de dégâts
GDScript · Godot 4
# enemy.gd (CharacterBody2D)
var walk_velocity := Vector2.ZERO
var knockback := Vector2.ZERO
func take_hit(hit_dir: Vector2) -> void:
knockback = hit_dir.normalized() * 450.0
func _physics_process(delta: float) -> void:
knockback = knockback.move_toward(Vector2.ZERO, 3000.0 * delta)
velocity = walk_velocity + knockback
move_and_slide()ÉTAPE 5
Shake + freeze
Gelez tout le jeu quelques frames à l'impact, puis un léger screen shake.
Pourquoi ça marche — Le freeze (hit stop) laisse au cerveau le temps d'enregistrer le contact ; le shake propage l'énergie vers l'extérieur.
- Hit stop : 40–90 ms (coup léger 40, lourd 90)
- Shake : 6–12 px, disparu en 0,2–0,3 s
- Shake basé sur trauma² pour que les petits hits restent discrets
GDScript · Godot 4
# Hit stop : le timer ignore time_scale (4e argument)
func hit_stop(duration := 0.06) -> void:
Engine.time_scale = 0.0
await get_tree().create_timer(duration, true, false, true).timeout
Engine.time_scale = 1.0
# camera_shake.gd (sur la Camera2D)
extends Camera2D
var trauma := 0.0
func add_trauma(amount: float) -> void:
trauma = minf(trauma + amount, 1.0)
func _process(delta: float) -> void:
trauma = maxf(trauma - 3.0 * delta, 0.0)
var s := trauma * trauma
offset = Vector2(randf_range(-1.0, 1.0), randf_range(-1.0, 1.0)) * 10.0 * sChapitre 2 · commente JUMP sous le Short
Rendre les sauts agréables
La plupart des sauts « flottants » ne sont pas un problème de physique, mais un problème d'indulgence. Quatre de ces cinq astuces sont invisibles — les joueurs ne les remarquent que quand elles sont absentes.
▶ Voir le Short JUMPÉTAPE 1
Coyote time
Laissez le joueur sauter encore quelques frames après avoir marché au bord d'une plateforme.
Pourquoi ça marche — Les joueurs appuient sur saut quand ils voient le bord, avec un léger retard. Sans cela, le jeu avale les inputs et paraît injuste.
- 0,08–0,12 s (5–7 frames à 60 fps)
- Seulement après avoir marché dans le vide, jamais après un saut
- Remettez à 0 dès que le saut se déclenche
GDScript · Godot 4
const COYOTE_TIME := 0.10
var coyote_timer := 0.0
func _physics_process(delta: float) -> void:
if is_on_floor():
coyote_timer = COYOTE_TIME
else:
coyote_timer -= delta
# can_jump: coyote_timer > 0.0 (mettre à 0.0 au moment du saut)ÉTAPE 2
Jump buffer
Mémorisez un appui saut juste avant l'atterrissage, et déclenchez le saut sur la frame d'atterrissage.
Pourquoi ça marche — Le timing du joueur était juste dans sa tête. L'honorer rend les commandes réactives plutôt que pointilleuses.
- Fenêtre de buffer : 0,10–0,15 s
- Fonctionne avec le coyote time : saut si buffer > 0 et coyote > 0
- Videz les deux timers dès que le saut se déclenche
GDScript · Godot 4
const JUMP_BUFFER := 0.12
const JUMP_VELOCITY := -420.0
var buffer_timer := 0.0
func handle_jump(delta: float) -> void:
if Input.is_action_just_pressed("jump"):
buffer_timer = JUMP_BUFFER
else:
buffer_timer -= delta
if buffer_timer > 0.0 and coyote_timer > 0.0:
velocity.y = JUMP_VELOCITY
buffer_timer = 0.0
coyote_timer = 0.0ÉTAPE 3
Maintenir pour sauter plus haut
Tapotez pour un petit saut, maintenez pour un saut complet.
Pourquoi ça marche — Le joueur obtient un contrôle analogique depuis un bouton numérique — de la précision sans inputs supplémentaires.
- Coupez la vélocité vers le haut de 50 % au relâchement
- Ne coupez que si l'on monte encore (velocity.y < 0)
- Petit saut minimum ≈ 35–45 % de la hauteur maximale
GDScript · Godot 4
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_released("jump") and velocity.y < 0.0:
velocity.y *= 0.5ÉTAPE 4
Chuter plus vite
Utilisez une gravité plus forte en descente qu'en montée.
Pourquoi ça marche — Une parabole symétrique donne une impression de flottement. Une chute rapide se sent vive et redonne le contrôle au joueur plus tôt.
- Gravité en chute ×1,8–2,5 (×2 est un bon point de départ)
- Vitesse de chute maximale : 800–1 000 px/s
- Gravité en montée ~1 200 px/s² pour un saut à 420 px/s
GDScript · Godot 4
const GRAVITY := 1200.0
const FALL_MULT := 2.0
const MAX_FALL := 900.0
func apply_gravity(delta: float) -> void:
var g: float = GRAVITY * (FALL_MULT if velocity.y > 0.0 else 1.0)
velocity.y = minf(velocity.y + g * delta, MAX_FALL)ÉTAPE 5
Squash + stretch
Étirez le sprite au décollage, écrasez-le à l'atterrissage, soufflez un peu de poussière.
Pourquoi ça marche — Cela donne à un sprite rigide l'impression d'avoir de la masse et des muscles — le saut a une élan et un impact.
- Décollage : scale (0,8, 1,2)
- Atterrissage : scale (1,25, 0,8)
- Retour à (1, 1) en 0,10–0,15 s, TRANS_BACK ease out
GDScript · Godot 4
@onready var visual: Node2D = $Visual # scalez le visuel, jamais le body
@onready var dust: CPUParticles2D = $Dust
var was_on_floor := true
func squash(s: Vector2) -> void:
visual.scale = s
create_tween().tween_property(visual, "scale", Vector2.ONE, 0.12) \
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
# après move_and_slide() :
func check_landing() -> void:
if is_on_floor() and not was_on_floor:
squash(Vector2(1.25, 0.8))
dust.restart()
was_on_floor = is_on_floor()
# au saut : squash(Vector2(0.8, 1.2))Chapitre 3 · commente COIN sous le Short
Rendre les ramassages de pièces agréables
Les pickups sont la boucle de récompense de votre jeu. Faites de chaque pièce une petite célébration et les joueurs les chasseront sans qu'on le leur demande.
▶ Voir le Short COINÉTAPE 1
Rotation + flottement
Chaque pièce tourne et flotte pour donner envie d'être ramassée.
Pourquoi ça marche — Le mouvement capte la vision périphérique : les pièces au repos deviennent une piste de miettes de pain qui guide le joueur.
- Flottement : 4–6 px, période ~1,2 s
- Rotation : ~1 tour par seconde
- Phase aléatoire par pièce, sinon elles bougent toutes en cadence
GDScript · Godot 4
extends Area2D
@onready var sprite: Sprite2D = $Sprite2D
var t := randf() * TAU # phase aléatoire
var base_y := 0.0
func _ready() -> void:
base_y = sprite.position.y
func _process(delta: float) -> void:
t += delta
sprite.position.y = base_y + sin(t * 5.2) * 5.0
sprite.scale.x = cos(t * TAU) # fausse rotation 3DÉTAPE 2
Aimant
Attirez les pièces vers le joueur dès qu'elles sont à portée.
Pourquoi ça marche — Les quasi-ratés se transforment quand même en gain. Le joueur se sent chanceux et puissant plutôt que frustré.
- Rayon : 60–100 px (upgradeable = progression gratuite)
- Accélérez, ne téléportez pas : 1 800 px/s², max 600 px/s
- Une fois attirée, ne lâchez plus jamais
GDScript · Godot 4
var player: Node2D
var vel := Vector2.ZERO
var pulled := false
func _physics_process(delta: float) -> void:
if not pulled and global_position.distance_to(player.global_position) < 80.0:
pulled = true
if pulled:
var dir := global_position.direction_to(player.global_position)
vel = vel.move_toward(dir * 600.0, 1800.0 * delta)
global_position += vel * deltaÉTAPE 3
Pop + scintillement
Écrasez la pièce vers le haut, puis faites-la exploser en petites étoiles.
Pourquoi ça marche — Une pièce qui disparaît simplement donne l'impression d'être supprimée. Un pop offre au ramassage une fin que l'œil peut apprécier.
- Scale à 1,4 en 60 ms, puis à 0 en 100 ms
- 6–12 particules étoiles, durée de vie 0,3 s
- Désactivez la collision dès la première frame
GDScript · Godot 4
func collect() -> void:
$CollisionShape2D.set_deferred("disabled", true)
$Sparkles.emitting = true # CPUParticles2D, one_shot
var tw := create_tween()
tw.tween_property(sprite, "scale", Vector2(1.4, 1.4), 0.06)
tw.tween_property(sprite, "scale", Vector2.ZERO, 0.10)
await get_tree().create_timer(0.4).timeout # laisser les sparkles se terminer
queue_free()ÉTAPE 4
Pitch montant
Chaque pièce d'une série rapide joue une note légèrement plus haute.
Pourquoi ça marche — La gamme ascendante transforme une rangée de pièces en petite mélodie — le cerveau veut la finir.
- +1 demi-ton par pièce : pitch = 2^(n/12)
- Combo se réinitialise après 0,6–0,8 s sans pièce
- Plafond à +12 (une octave), puis boucle
GDScript · Godot 4
# Sur le joueur (ou un autoload) : un compteur partagé
@onready var coin_sfx: AudioStreamPlayer = $CoinSfx
var combo := 0
var last_pickup := -10.0
func play_coin_sound() -> void:
var now := Time.get_ticks_msec() / 1000.0
combo = combo + 1 if now - last_pickup < 0.7 else 0
last_pickup = now
coin_sfx.pitch_scale = pow(2.0, float(combo % 13) / 12.0)
coin_sfx.play()ÉTAPE 5
Vol vers le compteur
Envoyez chaque icône de pièce vers le score, et faites rebondir le chiffre à l'arrivée.
Pourquoi ça marche — Cela relie l'action à la progression. Le joueur voit sa richesse augmenter au lieu de la lire.
- Vol : 0,35–0,5 s, ease-in (accélère vers le compteur)
- Rebond du compteur : scale 1,3 → 1,0 en 0,15 s
- Ajoutez la valeur quand la pièce arrive, pas quand elle est ramassée
GDScript · Godot 4
# hud.gd (CanvasLayer)
@onready var counter: Label = $Counter
const ICON := preload("res://ui/coin_icon.tscn")
var coins := 0
func fly_coin(screen_pos: Vector2) -> void:
var icon := ICON.instantiate() as Control
add_child(icon)
icon.position = screen_pos
var tw := create_tween()
tw.tween_property(icon, "position", counter.position, 0.4) \
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
tw.tween_callback(func():
icon.queue_free()
coins += 1
counter.text = str(coins)
counter.pivot_offset = counter.size / 2.0
counter.scale = Vector2(1.3, 1.3)
create_tween().tween_property(counter, "scale", Vector2.ONE, 0.15))
# screen_pos = coin.get_global_transform_with_canvas().originChapitre 4 · commente DASH sous le Short
Rendre les dashs agréables
Un dash est une explosion de vitesse qui dure une fraction de seconde. Tout ici existe pour rendre cette fraction explosive — et lisible.
▶ Voir le Short DASHÉTAPE 1
Anticipation
Écrasez le joueur quelques frames, puis étirez-le dans la direction du dash.
Pourquoi ça marche — L'armement charge le ressort : le cerveau anticipe le relâchement, ce qui rend l'explosion plus forte.
- Squash : 3–4 frames (≈ 50 ms) à (1,2, 0,8)
- Stretch dans la direction du dash : (1,35, 0,75)
- Gardez ça sous 80 ms, sinon le dash paraît lent
GDScript · Godot 4
func start_dash(dir: Vector2) -> void:
visual.scale = Vector2(1.2, 0.8)
await get_tree().create_timer(0.05).timeout
visual.rotation = dir.angle()
visual.scale = Vector2(1.35, 0.75)
create_tween().tween_property(visual, "scale", Vector2.ONE, 0.2)
dashing = true
dash_dir = dir
dash_time = DASH_DURATIONÉTAPE 2
Afterimages
Laissez une traînée de copies fantômes qui s'estompent derrière le joueur.
Pourquoi ça marche — Les fantômes dessinent le chemin parcouru — la vitesse devient visible même sur une image arrêtée.
- Un fantôme toutes les 25–35 ms pendant le dash
- Alpha de départ 0,5–0,6, fondu vers 0 en 0,2 s
- Teintez les fantômes d'une couleur d'accent
GDScript · Godot 4
func spawn_ghost() -> void:
var g := Sprite2D.new()
g.texture = sprite.texture
get_tree().current_scene.add_child(g)
g.global_transform = sprite.global_transform
g.modulate = Color(0.93, 0.42, 0.2, 0.55) # teinte d'accent
var tw := g.create_tween()
tw.tween_property(g, "modulate:a", 0.0, 0.2)
tw.tween_callback(g.queue_free)
# pendant le dash : appelez depuis un Timer avec wait_time = 0.03ÉTAPE 3
Lignes de vitesse + coup de FOV
Ajoutez des traînées et élargissez la caméra pendant un instant.
Pourquoi ça marche — Élargir le champ de vision imite le tunnel visuel de la vraie vitesse — le monde défile.
- 3D : FOV +12–18° (75 → 90), retour en 0,25 s
- 2D : zoom ×0,92, retour en 0,25 s
- Particules de traînée en sens inverse de la direction du dash
GDScript · Godot 4
@onready var cam: Camera3D = $Camera3D
const BASE_FOV := 75.0
func fov_kick() -> void:
cam.fov = BASE_FOV + 15.0
create_tween().tween_property(cam, "fov", BASE_FOV, 0.25) \
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)ÉTAPE 4
Hit stop
Si le dash touche quelque chose, gelez à l'impact, puis faites trembler.
Pourquoi ça marche — Le freeze ponctue la fin du mouvement — le dash obtient un vrai arrêt plutôt qu'un flou.
- Freeze 40–70 ms
- Shake 8 px pendant 0,2 s
- Même hit_stop() que le chapitre PUNCH — réutilisez-le
GDScript · Godot 4
func _on_dash_hit(body: Node) -> void:
if body.has_method("take_hit"):
body.take_hit(dash_dir)
await hit_stop(0.05) # voir PUNCH › Shake + freeze
camera.add_trauma(0.5)ÉTAPE 5
Poussière + récupération
Soufflez de la poussière à la fin et décélérez progressivement au lieu de stopper net.
Pourquoi ça marche — Un arrêt instantané donne l'impression de toucher un mur. Un court skid montre que l'élan est absorbé.
- Dash : 0,12–0,18 s à 800–1 000 px/s
- Skid : décélération jusqu'à la vitesse de marche sur ~0,12 s
- Burst de 6–10 puffs de poussière aux pieds
GDScript · Godot 4
const DASH_SPEED := 900.0
const DASH_DURATION := 0.15
const SKID_DECEL := 6000.0
func _physics_process(delta: float) -> void:
if dashing:
dash_time -= delta
velocity = dash_dir * DASH_SPEED
if dash_time <= 0.0:
dashing = false
dust.restart()
else:
velocity.x = move_toward(velocity.x, input_x * WALK_SPEED, SKID_DECEL * delta)
move_and_slide()La checklist
Imprimez-la, collez-la à côté de votre écran, cochez chaque case avant de livrer.
PUNCH
- Flash blanc 50–80 ms à l'impact
- Son d'impact sur la frame de contact, pitch 0,9–1,1
- Étincelles directionnelles (8–16, spread 25°)
- Knockback 350–500 px/s + petit recul joueur
- Hit stop 40–90 ms, puis shake basé sur trauma
JUMP
- Coyote time 0,10 s
- Jump buffer 0,12 s
- Relâchement = velocity.y × 0,5
- Gravité en chute ×2, vitesse de chute maximale plafonnée
- Stretch au décollage, squash à l'atterrissage, poussière
COIN
- Rotation + flottement avec phase aléatoire
- Rayon d'aimant 60–100 px, attraction accélérée
- Pop à 1,4× puis explosion en scintillements
- +1 demi-ton par pièce d'affilée (2^(n/12))
- Vol vers le compteur + rebond du compteur
DASH
- Squash d'anticipation ≤ 80 ms, stretch dans la direction du dash
- Afterimages toutes les 30 ms, fondu en 0,2 s
- Coup de FOV +15° (ou zoom 0,92) et lignes de vitesse
- Hit stop + shake au contact
- Skid jusqu'à l'arrêt + poussière plutôt qu'un arrêt brutal
Fait par Make Money With AI — les scènes de ces clips ont été codées par Claude. Extraits de code : MIT, utilisez-les partout.


