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 6·35 min

Game feel : particules, événements, caméra et son

À mécanique égale, un jeu avec retours visuels et sonores paraît deux fois meilleur. Les développeurs appellent ça le juice : des étincelles quand on ramasse une pièce, de la poussière à l'atterrissage, l'écran qui tremble quand on est touché, un petit son pour chaque action. Chacun de ces effets tient en quelques lignes avec Phaser.

Les particules

Un émetteur de particules génère de nombreux petits sprites éphémères avec des vitesses, durées et tailles aléatoires. Phaser les gère très efficacement : des centaines de particules ne coûtent presque rien.

src/scenes/GameScene.js
  creerEffets() {
    this.etincelles = this.add.particles(0, 0, 'particule', {
      speed: { min: 80, max: 200 },
      angle: { min: 0, max: 360 },
      lifespan: 450,
      scale: { start: 1, end: 0 },
      tint: [0xffec27, 0xffa300, 0xfff1e8],
      emitting: false,
    }).setDepth(10)
 
    this.poussiere = this.add.particles(0, 0, 'particule', {
      speedX: { min: -60, max: 60 },
      speedY: { min: -40, max: -10 },
      lifespan: 300,
      scale: { start: 0.8, end: 0 },
      tint: 0xc2c3c7,
      gravityY: 200,
      emitting: false,
    }).setDepth(10)
 
    this.events.on('atterrissage', (x, y) => this.poussiere.explode(6, x, y))
    // Les écouteurs de this.events survivent au redémarrage de la scène : on les retire
    this.events.once('shutdown', () => this.events.off('atterrissage'))
  }

Appelez creerEffets() dans create, juste après la création du joueur.

Les options de l'émetteur :

OptionEffet
speed, anglevitesse et direction ; { min, max } donne une valeur aléatoire par particule
speedX, speedYalternative : vitesses horizontale et verticale séparées
lifespandurée de vie en millisecondes
scale: { start, end }taille qui évolue pendant la vie de la particule
tintcouleur ; un tableau fait choisir au hasard
gravityYgravité propre aux particules
emitting: falsen'émet rien en continu : on déclenche à la demande
Gros plan du joueur entouré d'étincelles jaunes et orange après avoir ramassé une pièce
Une explosion de 16 étincelles au ramassage d'une pièce. À droite, la poussière grise au pied du slime.

emitter.explode(nombre, x, y) libère d'un coup un nombre de particules à une position. C'est ce qu'on appelle dans ramasserPiece et toucherSlime. Pour un effet continu (fumée, traînée derrière le joueur), on laisse emitting à true et on utilise emitter.startFollow(sprite).

setDepth(10) place les particules au-dessus du décor et des personnages.

Décommentez maintenant les lignes this.etincelles.explode(...) du chapitre 5.

Les événements personnalisés

Au chapitre 4, le joueur émettait un événement quand il touchait le sol :

this.scene.events.emit('atterrissage', this.x, this.body.bottom)

La scène l'écoute et fait voler la poussière. Ce découplage est précieux : le joueur ignore tout des particules, la scène ignore tout de la façon dont on détecte un atterrissage. Demain, on pourra aussi jouer un son ou secouer la caméra sur une grosse chute sans toucher à Player.js.

Tous les objets Phaser importants sont des émetteurs d'événements : this.events pour la scène, this.input, this.load, this.cameras.main, this.registry.events, chaque sprite…

Le piège du redémarrage

Quand une scène redémarre (le joueur clique sur « rejouer »), Phaser recrée les objets mais conserve les écouteurs ajoutés à this.events. Sans la ligne this.events.once('shutdown', ...), chaque partie ajouterait un écouteur de plus : à la cinquième partie, un atterrissage déclencherait cinq nuages de poussière. C'est une fuite silencieuse très fréquente dans les jeux Phaser. La règle : tout écouteur ajouté à un émetteur qui survit à la scène se retire à l'événement shutdown.

Les écouteurs du clavier (this.input.keyboard.on), eux, sont nettoyés automatiquement par Phaser à l'arrêt de la scène.

Les effets de caméra

La caméra a des effets intégrés, déjà croisés dans les chapitres précédents :

this.cameras.main.shake(200, 0.01)          // tremblement : durée, intensité
this.cameras.main.flash(150, 255, 255, 255) // éclair blanc
this.cameras.main.fadeOut(400, 0, 0, 0)     // fondu au noir
this.cameras.main.fadeIn(300)               // retour depuis le noir
this.cameras.main.zoomTo(1.5, 600)          // zoom animé
this.cameras.main.pan(x, y, 1000)           // déplacement animé

Chacun émet un événement de fin (camerafadeoutcomplete, camerashakecomplete…) pour enchaîner la suite. Le shake dans perdreVie est discret (intensité 0,01, soit 1 % de la taille de l'écran) : au-delà, l'effet devient fatigant. Réservez les grosses secousses aux explosions.

Le son

Phaser charge et joue des fichiers audio avec un gestionnaire complet : volume, boucle, fondu, son positionnel, sprites audio.

// preload
this.load.audio('musique', ['audio/theme.ogg', 'audio/theme.mp3'])
this.load.audio('piece', 'audio/piece.wav')
 
// create
this.musique = this.sound.add('musique', { loop: true, volume: 0.4 })
this.musique.play()
 
// n'importe où
this.sound.play('piece')

Donner plusieurs formats laisse le navigateur choisir celui qu'il sait lire. Les navigateurs bloquent tout son avant la première interaction de l'utilisateur ; Phaser attend automatiquement ce premier clic ou appui pour débloquer l'audio. C'est une raison de plus d'avoir un menu qui demande d'appuyer sur une touche.

Des sons synthétisés sans fichier

Pour rester sans aucun fichier externe, on génère des sons rétro avec la Web Audio API, que Phaser utilise déjà en interne. Un oscillateur produit une onde dont la fréquence glisse d'une note à une autre, avec un volume qui décroît :

src/sfx.js
const SONS = {
  saut: { type: 'square', de: 300, a: 600, duree: 0.12, volume: 0.06 },
  piece: { type: 'triangle', de: 880, a: 1760, duree: 0.1, volume: 0.12 },
  ecrase: { type: 'square', de: 220, a: 60, duree: 0.15, volume: 0.08 },
  aie: { type: 'sawtooth', de: 240, a: 50, duree: 0.35, volume: 0.08 },
  victoire: { type: 'triangle', de: 523, a: 1568, duree: 0.8, volume: 0.12 },
}
 
// Joue un petit son synthétisé avec la Web Audio API, sans fichier audio
export function sfx(scene, nom) {
  const ctx = scene.sound.context
  if (!ctx || ctx.state !== 'running' || scene.sound.mute) return
 
  const son = SONS[nom]
  const t = ctx.currentTime
  const oscillateur = ctx.createOscillator()
  const gain = ctx.createGain()
 
  oscillateur.type = son.type
  oscillateur.frequency.setValueAtTime(son.de, t)
  oscillateur.frequency.exponentialRampToValueAtTime(son.a, t + son.duree)
  gain.gain.setValueAtTime(son.volume, t)
  gain.gain.exponentialRampToValueAtTime(0.001, t + son.duree)
 
  oscillateur.connect(gain).connect(scene.sound.destination ?? ctx.destination)
  oscillateur.start(t)
  oscillateur.stop(t + son.duree)
}

Quelques explications :

  • scene.sound.context est le contexte audio de Phaser. S'il n'est pas encore débloqué (state !== 'running'), on ne joue rien plutôt que d'accumuler des sons en attente
  • la forme d'onde change le timbre : square sonne « 8 bits », triangle est plus doux, sawtooth plus agressif, parfait pour un son de dégât
  • une fréquence qui monte paraît positive (saut, pièce, victoire), une fréquence qui descend paraît négative (écrasement, dégât). Ce simple principe suffit à rendre les sons lisibles
  • en connectant à scene.sound.destination, nos sons passent par le volume général de Phaser et respectent le bouton muet

Pour aller plus loin sans fichier, des outils comme jsfxr génèrent des sons rétro et les exportent en WAV, à charger ensuite avec this.load.audio.

Couper le son

Dans GameScene, une touche pour couper et rétablir le son :

src/scenes/GameScene.js
  creerControles() {
    this.input.keyboard.on('keydown-P', () => this.mettreEnPause())
    this.input.keyboard.on('keydown-ESC', () => this.mettreEnPause())
    this.input.keyboard.on('keydown-M', () => {
      this.sound.mute = !this.sound.mute
    })
  }

this.sound est global au jeu : couper le son dans une scène le coupe partout. La pause est traitée au chapitre suivant.

Mesurer l'effet

Faites l'expérience : jouez une minute avec les effets, puis commentez creerEffets, les shake et les sfx, et rejouez. La mécanique est strictement identique, et pourtant le jeu paraît mou et sans retour. C'est exactement ce qu'apporte le juice.

Deux limites à garder en tête : l'accessibilité (proposez de désactiver les secousses d'écran, certaines personnes y sont sensibles) et la lisibilité (trop de particules masquent l'action).


Des particules déclenchées par des événements, des effets de caméra et des sons courts qui montent ou descendent : avec une cinquantaine de lignes, le jeu répond à chaque action du joueur. Il reste à l'habiller d'une interface et à gérer les différents états de la partie.

Précédent
Pièces, pics et ennemis : groupes et collisions
Suivant
Interface, chronomètre, pause et fin de partie

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