iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
JavaScript · Débutant

Phaser

Créer un jeu de plateforme complet avec Phaser 4 : scènes, textures générées, tilemap, physique Arcade, animations, ennemis, particules, sons, UI, mobile et déploiement.

Phaser 4JavaScriptViteArcade PhysicsWeb Audio
01Introduction et installation02Scènes, textures générées et menu03Tilemap, niveau et caméra04Le joueur : physique Arcade, clavier et animations05Pièces, pics et ennemis : groupes et collisions06Game feel : particules, événements, caméra et son07Interface, chronomètre, pause et fin de partie08Mobile, build et mise en ligne
Chapitre 4·45 min

Le joueur : physique Arcade, clavier et animations

C'est le chapitre qui fait d'un décor un jeu. On crée le personnage, on lui donne un corps physique soumis à la gravité, on le pilote au clavier et on l'anime. Puis on soigne ce qui distingue un jeu agréable d'un jeu frustrant : l'accélération, le saut à hauteur variable, le « coyote time » et le tampon de saut.

La physique Arcade

Phaser propose deux moteurs physiques : Arcade et Matter.js.

MoteurFormesCoûtPour
Arcaderectangles et cercles alignés sur les axestrès faibleplateformes, shoot'em up, top-down
Matterpolygones, rotations, articulationsélevépuzzles physiques, objets qui roulent et s'empilent

Pour un jeu de plateforme, Arcade est le bon choix : rapide, prévisible, et suffisant pour 95 % des besoins.

Un objet physique Arcade possède un corps (body) avec :

PropriétéRôle
velocityvitesse en pixels par seconde
accelerationvariation de vitesse par seconde
dragfreinage quand aucune accélération n'est appliquée
maxVelocityvitesse plafond
gravitys'ajoute à la gravité du monde (1200 dans notre config)
blocked{ up, down, left, right } : de quel côté le corps touche un obstacle solide
touchingidem, pour les contacts avec d'autres corps

Le corps est un rectangle indépendant de l'image : on le réduit souvent pour que les collisions paraissent justes.

Les animations

Une animation est une suite de frames jouée à une cadence donnée. On les déclare une fois, dans la scène Boot, et elles sont disponibles dans tout le jeu. Complétez creerAnimations :

src/scenes/BootScene.js
  creerAnimations() {
    this.anims.create({ key: 'repos', frames: [{ key: 'joueur', frame: 0 }] })
    this.anims.create({
      key: 'course',
      frames: [{ key: 'joueur', frame: 1 }, { key: 'joueur', frame: 2 }],
      frameRate: 10,
      repeat: -1,
    })
    this.anims.create({ key: 'saut', frames: [{ key: 'joueur', frame: 3 }] })
    this.anims.create({
      key: 'slime-marche',
      frames: [{ key: 'slime', frame: 0 }, { key: 'slime', frame: 1 }],
      frameRate: 4,
      repeat: -1,
    })
  }

Avec une vraie spritesheet chargée par this.load.spritesheet, on écrit plus souvent :

frames: this.anims.generateFrameNumbers('joueur', { start: 1, end: 6 })

La classe Player

On encapsule tout le comportement du joueur dans une classe qui étend Phaser.Physics.Arcade.Sprite. La scène reste lisible, et le joueur devient réutilisable dans d'autres niveaux.

src/objects/Player.js
import Phaser from 'phaser'
import { sfx } from '../sfx.js'
 
const ACCELERATION = 1800
const VITESSE_MAX = 260
const FREINAGE = 2000
const IMPULSION_SAUT = 600
const COYOTE_MS = 100
const TAMPON_SAUT_MS = 120
 
export class Player extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    super(scene, x, y, 'joueur', 0)
    scene.add.existing(this)
    scene.physics.add.existing(this)
 
    this.setCollideWorldBounds(true)
    this.body.setSize(14, 30).setOffset(5, 2)
    this.setMaxVelocity(VITESSE_MAX, 900)
    this.setDragX(FREINAGE)
 
    this.fleches = scene.input.keyboard.createCursorKeys()
    this.touches = scene.input.keyboard.addKeys({
      gauche: 'Q',
      gaucheQwerty: 'A',
      droite: 'D',
      saut: 'Z',
      sautQwerty: 'W',
    })
 
    this.coyote = 0
    this.tamponSaut = 0
    this.invincible = false
    this.controleBloque = 0
    this.etaitAuSol = true
  }

Points clés du constructeur :

  • scene.add.existing(this) ajoute le sprite à l'affichage, scene.physics.add.existing(this) lui crée un corps physique. Sans ces deux lignes, un objet construit avec new est invisible et immobile
  • setSize(14, 30).setOffset(5, 2) : le sprite fait 24 × 32 pixels, mais le corps est plus étroit que les bras. Les collisions paraissent plus justes, et le joueur ne reste pas accroché aux bords des plateformes
  • createCursorKeys() renvoie les flèches, Espace et Shift
  • addKeys enregistre des touches supplémentaires. On gère ZQSD et WASD : un jeu web est joué sur des claviers AZERTY comme QWERTY
Gros plan du joueur avec son corps physique en rose, plus étroit que le sprite
En mode debug : le corps physique (rectangle rose) fait 14 × 30 pixels, plus étroit que le sprite de 24 × 32.

Pour le module sfx.js importé ici, créez pour l'instant un fichier qui exporte une fonction vide : export function sfx() {}. On le remplira au chapitre 6.

preUpdate : la logique à chaque image

Les objets de jeu ont une méthode preUpdate(time, delta) appelée automatiquement à chaque image. En la surchargeant, le joueur se met à jour tout seul, sans que la scène ait à s'en occuper.

src/objects/Player.js
  preUpdate(time, delta) {
    super.preUpdate(time, delta)
    if (this.scene.fini) return
 
    const tactile = this.scene.registry.get('tactile') ?? {}
    const { fleches, touches } = this
 
    const gauche = fleches.left.isDown || touches.gauche.isDown || touches.gaucheQwerty.isDown || tactile.gauche
    const droite = fleches.right.isDown || touches.droite.isDown || tactile.droite
    const sautTenu = fleches.up.isDown || fleches.space.isDown || touches.saut.isDown || touches.sautQwerty.isDown || tactile.saut
    const sautPresse =
      Phaser.Input.Keyboard.JustDown(fleches.up) ||
      Phaser.Input.Keyboard.JustDown(fleches.space) ||
      Phaser.Input.Keyboard.JustDown(touches.saut) ||
      Phaser.Input.Keyboard.JustDown(touches.sautQwerty) ||
      tactile.sautPresse
    if (tactile.sautPresse) tactile.sautPresse = false
 
    const auSol = this.body.blocked.down

super.preUpdate est obligatoire : c'est lui qui fait avancer les animations. L'oublier donne un personnage figé sur sa première frame.

Deux façons de lire une touche :

  • touche.isDown : vrai tant que la touche est enfoncée (pour courir)
  • Phaser.Input.Keyboard.JustDown(touche) : vrai une seule fois au moment de l'appui (pour sauter). Sans ça, maintenir la touche ferait sauter en boucle

Les lignes tactile préparent les boutons à l'écran du chapitre 8 ; elles n'ont aucun effet sur ordinateur.

Déplacement horizontal

src/objects/Player.js
    // Déplacement horizontal avec accélération et freinage
    this.controleBloque -= delta
    if (this.controleBloque > 0) {
      this.setAccelerationX(0)
    } else if (gauche) {
      this.setAccelerationX(-ACCELERATION)
      this.setFlipX(true)
    } else if (droite) {
      this.setAccelerationX(ACCELERATION)
      this.setFlipX(false)
    } else {
      this.setAccelerationX(0)
    }

On ne fixe pas directement la vitesse : on applique une accélération, plafonnée par maxVelocity, et le drag freine quand on relâche. Le personnage démarre et s'arrête en quelques images au lieu de passer instantanément de 0 à pleine vitesse. Essayez de remplacer par setVelocityX(±260) pour sentir la différence : le mouvement devient raide.

setFlipX(true) retourne l'image horizontalement : une seule série de frames suffit pour les deux directions.

controleBloque servira au chapitre 5 : après un coup, le joueur perd le contrôle un quart de seconde pour que le recul soit visible.

Le saut qui fait plaisir

src/objects/Player.js
    // Coyote time : on peut encore sauter un court instant après avoir quitté le bord
    this.coyote = auSol ? COYOTE_MS : this.coyote - delta
    // Tampon de saut : un appui juste avant l'atterrissage est mémorisé
    this.tamponSaut = sautPresse ? TAMPON_SAUT_MS : this.tamponSaut - delta
 
    if (this.tamponSaut > 0 && this.coyote > 0) {
      this.setVelocityY(-IMPULSION_SAUT)
      this.coyote = 0
      this.tamponSaut = 0
      sfx(this.scene, 'saut')
    }
 
    // Saut à hauteur variable : relâcher tôt coupe l'élan
    if (!sautTenu && this.body.velocity.y < -200) {
      this.setVelocityY(-200)
    }

Trois techniques utilisées par tous les bons jeux de plateforme :

TechniqueProblème résolu
Coyote timele joueur appuie sur saut une fraction de seconde après avoir quitté le bord et tombe : injuste. On l'autorise à sauter pendant 100 ms après avoir quitté le sol
Tampon de sautle joueur appuie juste avant de toucher le sol et rien ne se passe. On mémorise l'appui pendant 120 ms
Hauteur variableun appui bref fait un petit saut, un appui long un grand saut. Si la touche est relâchée pendant la montée, on réduit la vitesse verticale

Ces trois mécanismes représentent une quinzaine de lignes. Aucun joueur ne les remarque, mais tous sentent leur absence.

Atterrissage et animations

src/objects/Player.js
    if (auSol && !this.etaitAuSol) {
      this.scene.events.emit('atterrissage', this.x, this.body.bottom)
    }
    this.etaitAuSol = auSol
 
    if (!auSol) this.anims.play('saut', true)
    else if (Math.abs(this.body.velocity.x) > 20) this.anims.play('course', true)
    else this.anims.play('repos', true)
  }
Gros plan du personnage en pixel art en train de courir sur l'herbe
Zoom ×3 sur l'animation de course. Avec pixelArt: true, les pixels restent nets.

L'événement atterrissage sera utilisé au chapitre 6 pour faire voler de la poussière. Le joueur ne sait pas ce qui se passe quand il atterrit : il le signale, et la scène décide.

anims.play(cle, true) : le second argument true signifie « ne rien faire si cette animation joue déjà ». Sans lui, l'animation de course redémarrerait à chaque image et resterait bloquée sur sa première frame.

Rebond et dégâts

Deux méthodes utilisées au chapitre suivant :

src/objects/Player.js
  rebondir() {
    this.setVelocityY(-380)
  }
 
  blesser(depuisX) {
    this.invincible = true
    this.controleBloque = 250
    this.setVelocity(this.x < depuisX ? -220 : 220, -320)
    this.scene.tweens.add({
      targets: this,
      alpha: 0.2,
      duration: 100,
      yoyo: true,
      repeat: 7,
      onComplete: () => {
        this.alpha = 1
        this.invincible = false
      },
    })
  }
}

Placer le joueur dans le niveau

Le point de départ est la case P du niveau. Dans GameScene, ajoutez l'import et une méthode creerObjets qui parcourt la grille (on la complétera au chapitre 5) :

src/scenes/GameScene.js
import { Player } from '../objects/Player.js'
 
// ...
 
  creerObjets(lignes) {
    lignes.forEach((ligne, y) => {
      ;[...ligne].forEach((c, x) => {
        const px = x * TILE + TILE / 2
        const py = y * TILE + TILE / 2
        if (c === 'P') this.depart = { x: px, y: py }
      })
    })
  }

Le point-virgule en début de ligne n'est pas une faute de frappe : sans lui, une ligne commençant par [ se collerait à la précédente, un piège classique de l'insertion automatique des points-virgules en JavaScript.

Puis dans create, après la carte :

src/scenes/GameScene.js
    this.creerObjets(niveau.map)
    this.joueur = new Player(this, this.depart.x, this.depart.y)
    this.creerCollisions()
    this.creerCamera()

Et les collisions avec le sol :

src/scenes/GameScene.js
  creerCollisions() {
    const largeur = this.carte.widthInPixels
    const hauteur = this.carte.heightInPixels
    // Pas de mur en bas : le joueur peut tomber dans les trous
    this.physics.world.setBounds(0, 0, largeur, hauteur + 200)
    this.physics.world.setBoundsCollision(true, true, true, false)
 
    this.physics.add.collider(this.joueur, this.sol)
  }
  • world.setBounds définit les limites du monde physique ; par défaut elles correspondent à l'écran, pas au niveau
  • setBoundsCollision(gauche, droite, haut, bas) : le bas est ouvert pour que les trous soient mortels
  • physics.add.collider(a, b) : a et b se bloquent mutuellement. Un seul appel suffit pour que le joueur tienne sur toute la tilemap

Lancez le jeu : le personnage court, saute, monte sur les plateformes par-dessous, et la caméra le suit.

Le joueur en plein saut entre le sol et les plateformes
Le joueur en plein saut : la frame 3 s'affiche dès qu'il quitte le sol.

Régler les sensations

Les constantes en haut de Player.js sont vos réglages de « game feel ». Quelques repères :

ConstantePlus hautPlus bas
ACCELERATIONdémarrage nerveuxpersonnage lourd
FREINAGEarrêt netglisse (sol gelé)
IMPULSION_SAUTsaut plus hautsaut plus bas
gravité du mondechute rapide, saut « sec »saut flottant

Hauteur maximale de saut = IMPULSION_SAUT² / (2 × gravité) = 600² / 2400 = 150 pixels, soit un peu moins de 5 tuiles. Garder ce calcul en tête évite de dessiner des plateformes inaccessibles.


Un sprite physique, quelques touches, trois animations, et surtout une poignée de mécanismes invisibles qui rendent le saut agréable. Le chapitre suivant peuple le niveau : pièces, pics, ennemis et drapeau.

Précédent
Tilemap, niveau et caméra
Suivant
Pièces, pics et ennemis : groupes et collisions

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main