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 5·45 min

Pièces, pics et ennemis : groupes et collisions

Le niveau est jouable mais vide. Ce chapitre ajoute tout ce qui fait réagir le joueur : des pièces à ramasser, des pics qui blessent, des slimes qui patrouillent, et un drapeau d'arrivée. Au passage, on voit les groupes, la différence entre collider et overlap, les corps statiques et une petite intelligence artificielle d'ennemi.

Collider ou overlap

MéthodeLes objets se bloquentCallbackUsage
physics.add.collider(a, b, cb)ouià chaque contactsol, murs, caisses
physics.add.overlap(a, b, cb)non, ils se traversentquand ils se chevauchentpièces, pics, ennemis, zones

Le callback reçoit les deux objets dans l'ordre des arguments : overlap(joueur, pieces, cb) appelle cb(joueur, piece) avec la pièce précise touchée, même si on a passé tout le groupe.

Les groupes

Un groupe rassemble des objets du même type. On peut tester une collision contre le groupe entier en un seul appel, et appliquer des réglages communs.

TypeCréationPour
groupe dynamiquethis.physics.add.group(config)objets qui bougent : ennemis, projectiles
groupe statiquethis.physics.add.staticGroup()objets fixes : pics, décor solide

Un corps statique ne bouge jamais et ne subit pas la gravité. Il est beaucoup moins coûteux : Phaser le range dans un arbre spatial et ne le recalcule pas à chaque image.

Placer les objets du niveau

Complétez creerObjets dans GameScene :

src/scenes/GameScene.js
  creerObjets(lignes) {
    this.pieces = this.physics.add.group({ allowGravity: false, immovable: true })
    this.pics = this.physics.add.staticGroup()
    this.slimes = this.physics.add.group()
 
    lignes.forEach((ligne, y) => {
      ;[...ligne].forEach((c, x) => {
        const px = x * TILE + TILE / 2
        const py = y * TILE + TILE / 2
        const basDeCase = (y + 1) * TILE
 
        if (c === 'P') this.depart = { x: px, y: py }
        if (c === 'C') this.pieces.create(px, py, 'piece')
        if (c === 'S') this.slimes.add(new Slime(this, px, basDeCase - 10))
        if (c === '^') {
          const pic = this.pics.create(px, basDeCase - 8, 'pic')
          pic.body.setSize(26, 10).setOffset(3, 6)
        }
        if (c === 'F') this.drapeau = this.physics.add.staticImage(px, basDeCase - 32, 'drapeau')
      })
    })
 
    this.tweens.add({
      targets: this.pieces.getChildren(),
      y: '-=6',
      duration: 700,
      ease: 'Sine.easeInOut',
      yoyo: true,
      repeat: -1,
      delay: (cible, cle, valeur, index) => index * 90,
    })
  }

Détails importants :

  • Les pièces sont dans un groupe dynamique sans gravité (allowGravity: false) : elles flottent. group.create(x, y, texture) crée un sprite physique et l'ajoute au groupe
  • Les positions : un sprite est positionné par son centre. Pour poser un objet sur le sol, on part du bas de la case (basDeCase) et on remonte de la moitié de sa hauteur
  • La hitbox des pics est plus petite que l'image (26 × 10 au lieu de 32 × 16) : on ne se blesse que sur les pointes, pas en frôlant l'espace entre deux pics. Un jeu tolérant sur les hitbox d'obstacles paraît juste ; un jeu tolérant sur les hitbox du joueur paraît facile
  • Le drapeau est une image statique seule, sans groupe
Niveau peuplé : pièces au-dessus des plateformes, pics au sol, slimes verts et joueur
Le niveau peuplé à partir des caractères C, S et ^ du fichier JSON.
Vue zoomée en mode debug : pièces et slime entourés en rose, pics entourés en bleu
En mode debug : corps dynamiques en rose, corps statiques en bleu. La hitbox des pics ne couvre que les pointes.

Faire flotter les pièces

Un seul tween anime toutes les pièces. y: '-=6' est une valeur relative : chaque pièce monte de 6 pixels par rapport à sa position. delay accepte une fonction : chaque pièce démarre 90 ms après la précédente, ce qui crée une vague au lieu d'un mouvement synchronisé et mécanique.

ease choisit la courbe d'accélération. Sine.easeInOut ralentit en haut et en bas, comme un objet qui flotte. Les autres familles utiles : Quad, Cubic, Back (léger dépassement), Bounce, Elastic.

L'ennemi : la classe Slime

Le slime avance, fait demi-tour contre un mur et au bord du vide. Ce second point transforme un obstacle bête en ennemi crédible.

src/objects/Slime.js
import Phaser from 'phaser'
 
export class Slime extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    super(scene, x, y, 'slime', 0)
    scene.add.existing(this)
    scene.physics.add.existing(this)
 
    this.body.setSize(24, 16).setOffset(2, 4)
    this.direction = -1
    this.vitesse = 60
    this.mort = false
    this.play('slime-marche')
  }
 
  preUpdate(time, delta) {
    super.preUpdate(time, delta)
    if (this.mort) return
 
    const corps = this.body
    if (corps.blocked.left) {
      this.direction = 1
    } else if (corps.blocked.right) {
      this.direction = -1
    } else if (corps.blocked.down) {
      // Demi-tour au bord du vide : pas de tuile juste devant, sous les pieds
      const devantX = this.direction > 0 ? corps.right + 2 : corps.left - 2
      const tuile = this.scene.sol.getTileAtWorldXY(devantX, corps.bottom + 4)
      if (!tuile) this.direction *= -1
    }
 
    this.setVelocityX(this.vitesse * this.direction)
    this.setFlipX(this.direction > 0)
  }
 
  ecraser() {
    this.mort = true
    this.body.enable = false
    this.anims.stop()
    this.scene.tweens.add({
      targets: this,
      scaleY: 0.2,
      y: this.y + 8,
      alpha: 0,
      duration: 250,
      ease: 'Quad.easeIn',
      onComplete: () => this.destroy(),
    })
  }
}

La détection du vide utilise la tilemap : getTileAtWorldXY(x, y) renvoie la tuile à une position en pixels, ou null si la case est vide. On regarde juste devant le slime et juste sous ses pieds.

Pourquoi fixer la vitesse à chaque image

On pourrait appeler setVelocityX une seule fois dans le constructeur. Mais quand un objet est ajouté à un groupe physique, le groupe applique ses réglages par défaut au corps, dont une vitesse nulle. En réaffirmant la vitesse dans preUpdate, le slime est insensible à cet ordre d'initialisation, et aussi aux collisions qui l'arrêteraient.

body.enable = false dans ecraser coupe toute physique : le slime mourant ne blesse plus et ne bloque plus. Le tween l'écrase verticalement et le fait disparaître, puis destroy() le retire définitivement du jeu et de son groupe.

Les interactions

Complétez creerCollisions :

src/scenes/GameScene.js
    this.physics.add.collider(this.joueur, this.sol)
    this.physics.add.collider(this.slimes, this.sol)
    this.physics.add.overlap(this.joueur, this.pieces, this.ramasserPiece, null, this)
    this.physics.add.overlap(this.joueur, this.pics, () => this.blesserJoueur(this.joueur.x))
    this.physics.add.overlap(this.joueur, this.slimes, this.toucherSlime, null, this)
    this.physics.add.overlap(this.joueur, this.drapeau, this.victoire, null, this)

Le quatrième argument (null) est un processCallback optionnel : une fonction qui peut annuler la collision au cas par cas. Le cinquième (this) est le contexte d'appel, pour que this désigne la scène dans les méthodes.

Les pics passent this.joueur.x comme origine du coup : le joueur est alors repoussé vers le haut, là où il était déjà.

Ramasser une pièce

src/scenes/GameScene.js
  ramasserPiece(joueur, piece) {
    piece.disableBody(true, true)
    this.registry.inc('score', 10)
    this.etincelles.explode(12, piece.x, piece.y)
    sfx(this, 'piece')
  }

disableBody(desactiver, masquer) coupe le corps et cache la pièce, sans la détruire. C'est plus économique qu'un destroy et permet de la réactiver avec enableBody (utile pour des pièces qui réapparaissent).

this.registry est un espace de données partagé entre toutes les scènes, détaillé au chapitre 7. Les étincelles et le son arrivent au chapitre 6 : commentez ces deux lignes en attendant.

Écraser ou être blessé

src/scenes/GameScene.js
  toucherSlime(joueur, slime) {
    const tombe = joueur.body.velocity.y > 0
    const auDessus = joueur.body.bottom <= slime.body.top + 12
    if (tombe && auDessus) {
      slime.ecraser()
      joueur.rebondir()
      this.registry.inc('score', 50)
      this.etincelles.explode(8, slime.x, slime.y)
      sfx(this, 'ecrase')
    } else {
      this.blesserJoueur(slime.x)
    }
  }

La règle classique : si le joueur descend et que ses pieds sont au niveau du haut du slime, il l'écrase et rebondit. Sinon, c'est le joueur qui prend le coup. La marge de 12 pixels compense le fait que la physique avance par pas : à grande vitesse, les pieds peuvent déjà être légèrement enfoncés dans le slime au moment où la collision est détectée.

Perdre une vie

src/scenes/GameScene.js
  blesserJoueur(depuisX) {
    if (this.joueur.invincible || this.fini) return
    this.perdreVie()
    if (!this.fini) this.joueur.blesser(depuisX)
  }
 
  perdreVie() {
    const vies = this.registry.get('vies') - 1
    this.registry.set('vies', vies)
    this.cameras.main.shake(200, 0.01)
    sfx(this, 'aie')
    if (vies <= 0) this.finDePartie(false)
  }

Après un coup, Player.blesser (chapitre 4) rend le joueur invincible 1,6 seconde en le faisant clignoter, et le repousse à l'opposé de l'ennemi. Sans cette invincibilité, un overlap déclenché à chaque image viderait les trois vies en trois images.

Tomber dans un trou

La chute se teste dans update, puisque rien ne « touche » le joueur :

src/scenes/GameScene.js
  update() {
    if (!this.fini && this.joueur.y > this.carte.heightInPixels + 64) {
      this.perdreVie()
      if (!this.fini) {
        this.joueur.setPosition(this.depart.x, this.depart.y).setVelocity(0, 0)
      }
    }
  }

Le joueur perd une vie et réapparaît au départ. Pour un niveau plus long, on ajouterait des points de sauvegarde : il suffirait de mettre à jour this.depart en passant devant un drapeau intermédiaire.

Victoire et fin de partie

src/scenes/GameScene.js
  victoire() {
    if (this.fini) return
    this.registry.inc('score', this.registry.get('temps') * 5)
    sfx(this, 'victoire')
    this.finDePartie(true)
  }
 
  finDePartie(gagne) {
    this.fini = true
    this.physics.pause()
    this.joueur.anims.stop()
    if (!gagne) this.joueur.setTint(0xff004d)
 
    this.time.delayedCall(900, () => {
      this.cameras.main.fadeOut(400, 0, 0, 0)
      this.cameras.main.once('camerafadeoutcomplete', () => {
        this.scene.stop('UI')
        this.scene.start('GameOver', { gagne, score: this.registry.get('score') })
      })
    })
  }
  • le temps restant rapporte 5 points par seconde : un bonus qui récompense la vitesse
  • physics.pause() fige tout le monde physique
  • setTint colore le sprite : rouge pour une défaite
  • time.delayedCall(900, fn) attend un peu avant de quitter : le joueur a le temps de comprendre ce qui s'est passé. Un changement d'écran instantané est toujours déroutant

La garde if (this.fini) return est essentielle : l'overlap avec le drapeau se déclenche à chaque image tant que le joueur est dessus. Sans elle, la victoire serait comptée soixante fois par seconde.


Groupes pour les objets semblables, overlap pour ce qui se traverse, collider pour ce qui bloque, et des gardes pour que chaque événement ne compte qu'une fois : le jeu a désormais des règles. Le chapitre suivant lui donne du caractère avec des particules, des sons et des effets de caméra.

Précédent
Le joueur : physique Arcade, clavier et animations
Suivant
Game feel : particules, événements, caméra et son

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