Skip to content
Learni
Voir tous les tutoriels
Développement de Jeux

Comment créer un jeu de plateforme 2D avec Godot en 2026

Read in English

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

player.gd
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 = -1

Ce 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' :

  1. Ajoutez un CharacterBody2D racine.
  2. Enfants : Sprite2D (importez un sprite pixel art 32x64px), CollisionShape2D (RectangleShape2D ajusté à 20x50px), AnimatedSprite2D optionnel.
  3. Attachez 'player.gd' au root.
  4. 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

enemy.gd
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.
Run F5 : joueur saute, ennemi patrouille, collision tue. Astuce : TileMap layers pour collisions séparées (sol vs murs).

Script caméra suiveuse avancée

camera.gd
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

hud.gd
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

game_manager.gd
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.
Découvrez nos formations Learni Godot Expert pour monétiser vos jeux indie en 2026.