Introduction
Godot 4 représente en 2026 le choix privilégié des développeurs indie pour sa gratuité, sa légèreté et ses performances natives en 2D/3D. Ce tutoriel intermédiaire vous guide pas à pas pour créer un jeu de plateforme 2D complet : un joueur qui saute et se déplace fluidement, des ennemis patrouilleurs, un système de score, une caméra suiveuse et des collisions précises. Contrairement aux tutoriels basiques, nous implémentons des mécaniques avancées comme l'accélération réaliste (analogue à un skateboard qui freine progressivement) et une IA simple pour les ennemis.
Pourquoi ce projet ? Il couvre 80 % des besoins d'un prototype commercialisable, optimisé SEO pour les moteurs de recherche Godot. À la fin, vous aurez un jeu jouable en 30 minutes, extensible vers Steam. Nous utilisons GDScript 2.0 pour sa simplicité Python-like, l'éditeur visuel pour les scènes, et des bonnes pratiques pour scaler vers Vulkan rendering. Prêt à jumper ? (128 mots)
Prérequis
- Godot 4.3+ installé (téléchargez depuis godotengine.org)
- Connaissances de base en GDScript (variables, signaux, _physics_process)
- Compréhension des nœuds 2D (CharacterBody2D, TileMap, CollisionShape2D)
- Éditeur de texte optionnel (VS Code avec extension Godot Tools)
- Temps estimé : 45 minutes pour un prototype jouable
Script du joueur : mouvement et saut fluide
extends CharacterBody2D
@export var speed: float = 300.0
@export var jump_velocity: float = -450.0
@export var acceleration: float = 1500.0
var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")
func _physics_process(delta: float) -> void:
# Gravité progressive
if not is_on_floor():
velocity.y += gravity * delta
# Saut double si en l'air (mécanique intermédiaire)
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = jump_velocity
elif Input.is_action_just_pressed("ui_accept") and velocity.y > 0 and get_slide_collision_count() > 0:
velocity.y = jump_velocity * 0.7
# Mouvement horizontal avec accélération (réaliste)
var direction: float = Input.get_axis("ui_left", "ui_right")
if direction != 0:
velocity.x = move_toward(velocity.x, direction * speed, acceleration * delta)
else:
velocity.x = move_toward(velocity.x, 0, acceleration * delta)
move_and_slide()
# Animation flip (à attacher à AnimatedSprite2D)
if direction > 0:
scale.x = 1
elif direction < 0:
scale.x = -1Ce script complet gère gravité synchrone avec les settings projet, saut double pour fluidité (analogue à Celeste), et accélération progressive évitant les mouvements saccadés. Attachez-le à un CharacterBody2D avec CollisionShape2D et Sprite2D. Piège : oubliez gravity * delta cause chutes irréalistes sur machines lentes.
Configurer la scène du joueur
Créez une nouvelle scène 'Player.tscn' :
- Ajoutez un CharacterBody2D racine.
- Enfants : Sprite2D (importez un sprite pixel art 32x64px), CollisionShape2D (RectangleShape2D ajusté à 20x50px), AnimatedSprite2D optionnel.
- Attachez 'player.gd' au root.
- Dans Project Settings > Input Map, ajoutez 'jump' mappé à Espace.
Testez : F6 pour run scène. Le joueur accélère/décélère comme dans un vrai platformer. Analogie : comme un vecteur qui s'incline vers la cible via move_toward().
Script de l'ennemi : patrouille et collision mortelle
extends CharacterBody2D
@export var patrol_speed: float = 100.0
@export var patrol_distance: float = 200.0
@export var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")
var direction: float = 1.0
var start_position: Vector2
@onready var player: Node2D = get_tree().get_first_node_in_group("player")
func _ready() -> void:
start_position = global_position
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
# Patrouille aller-retour
global_position.x += patrol_speed * direction * delta
if abs(global_position.x - start_position.x) > patrol_distance:
direction *= -1
scale.x *= -1
# Détection joueur et mort
if player and global_position.distance_to(player.global_position) < 30:
player.queue_free() # Game Over simple
move_and_slide()L'ennemi patrouille sur une distance fixe, flippe visuellement, et tue le joueur au contact (signal-based extensible). Utilise @onready pour player groupé. Piège : sans get_slide_collision_count(), les murs bloquent mal ; testez avec TileMap.
Intégrer l'ennemi dans la scène principale
- Créez 'Main.tscn' : Node2D racine.
- Ajoutez TileMap pour sol/plateformes (créez TileSet avec collision).
- Instancez Player.tscn à (100, 300).
- Ajoutez Enemy.tscn à (500, 280), groupez Player dans 'player'.
- Camera2D enfant de Player, activée avec drag margins.
Script caméra suiveuse avancée
extends Camera2D
@export var follow_speed: float = 5.0
@export var deadzone_margin: float = 0.2
@onready var player: CharacterBody2D = get_parent()
func _physics_process(delta: float) -> void:
if not player:
return
var target_pos: Vector2 = player.global_position
# Suivi fluide avec deadzone (comme Hollow Knight)
if abs(global_position.x - target_pos.x) > get_viewport_rect().size.x * deadzone_margin:
global_position.x = lerp(global_position.x, target_pos.x, follow_speed * delta)
if abs(global_position.y - target_pos.y) > get_viewport_rect().size.y * deadzone_margin * 0.5:
global_position.y = lerp(global_position.y, target_pos.y, follow_speed * delta * 0.8)
# Limites scène (éditez les valeurs)
global_position.x = clamp(global_position.x, 100, 1800)
global_position.y = clamp(global_position.y, 100, 600)Caméra lerp-smooth suit avec deadzone pour gameplay focus, clampée aux borders. Attachez à Player. Piège : sans delta dans lerp, saccades sur FPS variables ; viewport_rect() auto-scale.
Script HUD : score et vies dynamiques
extends CanvasLayer
@onready var score_label: Label = $ScoreLabel
@onready var lives_label: Label = $LivesLabel
var score: int = 0
@export var lives: int = 3
signal score_updated(new_score: int)
signal lives_changed(new_lives: int)
func _ready() -> void:
score_updated.connect(_on_score_updated)
lives_changed.connect(_on_lives_changed)
update_ui()
func add_score(points: int) -> void:
score += points
score_updated.emit(score)
func lose_life() -> void:
lives -= 1
lives_changed.emit(lives)
if lives <= 0:
get_tree().reload_current_scene()
func _on_score_updated(new_score: int) -> void:
score_label.text = "Score: " + str(new_score)
func _on_lives_changed(new_lives: int) -> void:
lives_label.text = "Vies: " + str(new_lives)
func update_ui() -> void:
score_label.text = "Score: 0"
lives_label.text = "Vies: " + str(lives)HUD CanvasLayer gère score/vies via signaux découplés, reset scène sur 0 vie. Ajoutez Labels enfants. Connectez depuis Player (add_score(10) sur coin). Piège : sans CanvasLayer, scale UI buggé.
Assembler le jeu complet et tester
Dans Main.tscn :
- Ajoutez HUD.tscn enfant racine.
- Player.connect("collect_coin", hud.add_score) (implémentez signal coin dans player.gd).
- Export Project > Windows/Desktop.
Testez : collectez pièces invisibles via Area2D+signal, perdez vies sur ennemi. Performant à 120 FPS. Analogie : signaux comme events Unity, évitant polling coûteux.
Script gestionnaire de jeu global
extends Node
@onready var player: Node2D = $Player
@onready var hud: Node2D = $HUD
var high_score: int = 0
func _ready() -> void:
player.get_node("Area2D").body_entered.connect(_on_enemy_hit)
func _on_enemy_hit(body: Node2D) -> void:
if body == player:
hud.lose_life()
func save_high_score() -> void:
high_score = max(high_score, hud.score)
var file = FileAccess.open("user://highscore.save", FileAccess.WRITE)
file.store_32(high_score)
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
save_high_score()Singleton AutoLoad pour highscore persistant (user:// cross-platform), hook enemy hits. Ajoutez à autoload. Piège : FileAccess sans check_exists() crash sur mobile.
Bonnes pratiques
- Groupes et signaux : Toujours grouper ("player") et émettre signaux pour découplage (évite get_node() fragile).
- Exports : @export pour tout tunable en éditeur, accélère itérations x10.
- Physique synchrone : gravity de ProjectSettings, jamais hardcodée.
- Perf : Utilisez TileMap pour niveaux, Profiler intégré pour FPS drops.
- Versioning : Git + .godot/ignore pour exporter sans binaries.
Erreurs courantes à éviter
- Pas de delta : velocity += gravity sans *delta → vitesse infinie sur slow-mo.
- Collision one-way : Oubliez one_way_collision_margin sur TileMap → joueur coince.
- Autoload manquant : GameManager sans Project Settings > Autoload → signaux perdus.
- Scale négatif sans flip_h : Sprite déformé ; utilisez scale.x ou AnimatedSprite flip_h.
Pour aller plus loin
- Docs officielles : Godot 4 Platformer Demo
- Avancé : Ajoutez shaders pour parallax, Navigation2D pour IA.
- Multiplateforme : Export templates Vulkan pour WebGPU.