Créer un jeu multi-niveaux

Dans cette activité, nous présentons comment :

  • Créer plusieurs niveaux (pouvant être complètement différents), chaque niveau représentant une scène de jeu différente.
  • Structurer notre code en plusieurs fichiers, un par niveau
  • Comprendre le changement de scènes, et la transition d’une scène à l’autre.
  • définir des variables en tant qu’attributs de classes, et non 

Nous allons créer un jeu comportant 4 niveaux : un niveau appelé “selection” présentera un plateau de jeu avec 3 portes. Chaque porte débouchera sur  une salle particulière, nommés respectivement  “niveau1″, “niveau2” et “niveau3“, et il sera possible de revenir en arrière vers  “selection” en prenant la porte de sortie de chaque niveau.

Démo du résultat à obtenir avec ce tutoriel​

Base de départ pour ce tutoriel

Nous partons d’une base constituée d’un jeu dans lequel un personnage est présent sur un monde de taille fixe avec une gravité prédéfinie. Ce personnage peut aller à gauche, à droite, et sauter avec la flèche du haut s’il touche le sol. Les animations sur ce personnage ont été définies. Cette base a été créée à partir du tutoriel  Créer son premier jeu de plate-forme en découvrant Phaser sur lequel on a enlevé toute la partie “génération des étoiles”, “génération du score”, “génération des bombes”. 

Figure 1 : un personnage pouvant bouger dans un monde simple

Cette base est directement accessible sur codesandbox sous forme de template: lorsque vous créez une nouvelle sandbox, choisissez « explore templates » et et recherchez dans la barre de recherche « phaser-base-plateforme ». Nous allons procéder ainsi :

  1. Nous allons dans un premier temps transformer cette scène sous forme d’une classe nommée  “selection“. 
  2. Puis nous allons organiser notre code en plusieurs fichiers, en isolant “selection” dans un fichier
  3. Nous ajouterons une nouvelle scene niveau1″ scène stockée dans un fichier disctint, et le lier à la scène principale “selection“.
  4. Nous répèterons les memes manipulation pour deux scènes supplémentaires “niveau2” et “niveau3“, 
  5. Et nous améliorerons  enfin les transitions entre ces scènes.

I - Transformer une scène sous forme de classe

Sur Phaser 3, on travaille avec des scènes. Globalement, un écran d’accueil, un niveau, une transition d’un mode à un autre, est défini par une scène. Une scène contient trois fonctions principales, comme déjà vu :

  • La fonction preload() : charge des assets (notamment des images) pour tout le jeu
  • La fonction create() : exécute des instructions lorsque la scène est lancée
  • La fonction update() : surveille les mises à jour, et événements de la scène

Dans les tutoriels simples, il n’y a qu’une scène, le code est simplifié. Mais si on veut mettre en place plusieurs scènes, il va falloir créer autant de fonctions preload(), create() et update() que l’on a de scènes. Pour charger plusieurs scènes dans Phaser 3, on va devoir modifier le code de manière conséquente en passant par une syntaxe « orientée objet ».

Dans un premier temps, nous allons passer à une syntaxe orientée objets, en utilisant une classe. Nous partons d’une sandbox créée par le template  “phaser-base-plateforme” et vous conseillons fortement de faire de même au vu de la difficulté de compréhension de certaines notions, mais vous pouvez prendre tout projet plus avancé – auquel cas il vous faudra adapter votre code aux erreurs et noms de variables différents – . Initialement, on a la structuration  de code suivante, stockée dans un fichier unique Index.js.

// chargement des librairies
import Phaser from "phaser";

// configuration générale du jeu
var config = {
 /* ... */
  scene: {
    preload: preload, // la phase preload est associée à la fonction preload, du meme nom (on aurait pu avoir un autre nom)
    create: create, // la phase create est associée à la fonction create, du meme nom (on aurait pu avoir un autre nom)
    update: update // la phase update est associée à la fonction update, du meme nom (on aurait pu avoir un autre nom)
  }
};

// création et lancement du jeu à partir de la configuration config
var game = new Phaser.Game(config);

/* variables globales accessibles dans toutes les fonctions */
var player;
/* ... */

function preload() { /*...*/ }

function create()  { /*...*/ }
 
function update()  { /*...*/ } 

Nous n’avons qu’une seule scène, dont le nom n’est pas mentionné. Pour passer à la syntaxe objet, il va nous falloir donner un nom à cette scène; Nous l’appelons « selection», et modifions le code de la facon suivante :

  • Nous englobons nos 3 fonctions preload(), create() et update() dans une classe nommée “selection”  qui hérite de la classe Phaser.Scene
  • Ces 3 fonctions sont désormais des méthodes de la classe “selection“. Il nous faut donc enlever le mot clé function devant chacune d’entre elle
  • Nous ajoutons à cette classe un constructeur constructor() qui contient une seule instruction :   super({ key: “selection” }); (sur laquelle nous reviendrons plus tard)
  •  Nous déplaçons la variable config ainsi que la ligne de création du jeu new Phaser.Game(config);  après la définition de la classe
  • Nous modifions la valeur du paramètre scene de config  : il ne s’agit plus d’un tableau associant preload à preload(), create à create() et update à update, mais d’un tableau contenant l’ensemble des scènes du jeu, ici [selection]
  • Enfin, nous lançons la scène “selection” en ajoutant l’instruction game.scene.start(“selection”);
La structure de votre code doit ressembler au squelette suivant :
// chargement des librairies
import Phaser from "phaser";

/* variables globales accessibles dans toutes les fonctions */
var player;
/* ... */

// définition de la classe "selection"
class selection extends Phaser.Scene {
 
  constructor() {
     super({key : "selection"}); // mettre le meme nom que le nom de la classe
  }
 
  preload() { /*...*/ }

  create()  { /*...*/ }
 
  update()  { /*...*/ }

}

// configuration générale du jeu
var config = {
 /* ... */
  scene : [selection]
  }
};

// création et lancement du jeu à partir de la configuration config
var game = new Phaser.Game(config);
game.scene.start("selection"); // lancement de la scene selection
 

Modifiez votre code pour que ce dernier utilise une structure similaire à celle présentée ci-dessus.  Votre code doit continuer à  s’exécuter sans générer d’erreur. 

Si votre code est fonctionnel : félicitations, vous venez de créer votre première scène sous forme de classe. 

II - Organisation du code en plusieurs fichiers

II.1 - Réorganisation du code

Nous avons dans noter sandbox les répertoires suivants :

  • src/ pour « sources » contiendra tous les fichiers source de notre jeu
  • src/assets/ contientra l’ensemble des assets du jeu. Dans une organisation plus poussée, on pourrait organiser ce répertoire en sous-répertoires
  • on va rajouter un répertoire src/js dans lequel on mettra tous les fichiers javascrip

Notre code est écrit entièrement dans un fichier src/index.js, lui même appelé à partir d’un fichier index.html. Ce code contient les instruction de configuration et de lancement de jeu, ainsi que la classe selection.

Nous allons “isoler” la classe selection et les variables globales utilisées dans cette classe dans un fichier  à part. Par la suite, chaque classe sera créée dans un fichier dédié, et nous transformerons les variables globales en attributs de classe.

Créez tout d’abord un répertoire js dans src, puis un fichier src/js/selection.js . Dans index.js, sélectionnez tout le code des variables globales et de la classe et déplacez le dans selection.js. Dans selection.js, ajoutez tout en haut du fichier  la ligne suivante :

import Phaser from "phaser"; 

Puis toujours dans selection.js, faites précéder la déclaration de la classe par le mot-clé export défaut : remplacez la ligne :

 class selection extends Phaser.Scene {
 // ... 

par la ligne :

export default class selection extends Phaser.Scene {
// ... 

Avec ces instructions,  nous avons tout simplement exporté la classe selection pour qu’elle puisse être utilisée par n’importe quel fichier qui utilisera selection.js. Notre fichier selection.js est prêt , il faut le charger dans index.js : dans index.js on ajoute en début de fichier la ligne suivante :

import selection from "/src/js/selection.js"; 

Relancez votre jeu : ce dernier doit être à nouveau fonctionnel! 

II.2 - Résumé des structures de index.js et selection.js

Au terme de ces modifications, la structure du fichier index.js  est la suivante :

// chargement des librairies
import Phaser from "phaser";
import selection from "/src/js/selection.js";

// configuration générale du jeu
var config = {
 /* ... */
  scene : [selection]
  }
};

// création et lancement du jeu à partir de la configuration config
var game = new Phaser.Game(config);
game.scene.start("selection"); // lancement de la scene selection
 

La structure du fichier selection.js  est la suivante :

// chargement des librairies
import Phaser from "phaser";

/* variables globales accessibles dans toutes les fonctions */
var player;
/* ... */

// définition de la classe "selection"
class selection extends Phaser.Scene {
 
  constructor() {
     super({key : "selection"}); // mettre le meme nom que le nom de la classe
  }
 
  preload() { /*...*/ }

  create()  { /*...*/ }
 
  update()  { /*...*/ }

} 

III - Ajout d'un nouveau niveau

Nous allons ajouter une nouvelles scène  nommée niveau1. Nous présentons la procédure pour  ajouter pour ce niveau, il suffira de la reproduire pour ajouter les niveaux suivants

III.1 Création du niveau 1 à partir d'un code existant

Créez un nouveau fichier  niveau1.js  dans le répertoire /src/js. Ajoutez-y tout le code suivant :

// chargement des librairies
import Phaser from "phaser";

export default class niveau1 extends Phaser.Scene {
  // constructeur de la classe
  constructor() {
    super({
      key: "niveau1" //  ici on précise le nom de la classe en tant qu'identifiant
    });
  }
  preload() {}

  create() {
    this.add.image(400, 300, "img_ciel");
    this.groupe_plateformes = this.physics.add.staticGroup();
    this.groupe_plateformes.create(200, 584, "img_plateforme");
    this.groupe_plateformes.create(600, 584, "img_plateforme");
    // ajout d'un texte distintcif  du niveau
    this.add.text(400, 100, "Vous êtes dans le niveau 1", {
      fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif',
      fontSize: "22pt"
    });

    this.player = this.physics.add.sprite(100, 450, "img_perso");
    this.player.refreshBody();
    this.player.setBounce(0.2);
    this.player.setCollideWorldBounds(true);
    this.clavier = this.input.keyboard.createCursorKeys();
    this.physics.add.collider(this.player, this.groupe_plateformes);
  }

  update() {
    if (this.clavier.left.isDown) {
      this.player.setVelocityX(-160);
      this.player.anims.play("anim_tourne_gauche", true);
    } else if (this.clavier.right.isDown) {
      this.player.setVelocityX(160);
      this.player.anims.play("anim_tourne_droite", true);
    } else {
      this.player.setVelocityX(0);
      this.player.anims.play("anim_face");
    }
    if (this.clavier.up.isDown && this.player.body.touching.down) {
      this.player.setVelocityY(-330);
    }
  }
}
 

Reprenons ce code : même si vous avez compris que ce code crée un nouveau niveau dans un fichier niveau1.js et qu’on va pouvoir passer du niveau “selection” vers ce dernier, il y a quelques bizarreries :

  1. La méthode preload() est vide! En effet, les images n’ont pas besoin d’être rechargées à partir du moment où elles ont été chargées une fois dans une scène précédente (ce sera le cas pour le niveau “selection”). Mais si les visuels venaient à changer ou à être spécifiques à la scène, il faudrait bien entendu compléter cette méthode
  2. La création des animations a disparu, pourtant ces animations sont appelées dans la partie update() : la encore, une fois une animation créée dans un niveau, elle appartient à tout le jeu et peut etre reprise partout ailleurs sans être redéfinie.
  3. Il n’y a plus de variables globales : elles ont été remplacées par des attributs de classe. Regardez bien ce code : on n’utilise plus groupe_platesformes mais this.groupe_plateformes, on n’utilise plus player mais this.player. Ceci est un choix qu’on aurait pu faire des le niveau selection mais que l’on présente ici : le fait de rattacher les variables en tant qu’attribut crée moins de confusions quand on les manipule uniquement dans une classe.

Notez également, pour ce niveau, la présence du constructeur avec la clé “niveau1” ( key : “niveau1” ) et la présence d’un texte ( this.add.text(…) ) indiquant que l’on est sur le niveau 1. Ces éléments seront à adapter pour les autres niveaux créés.

Testons sans plus attendre notre niveau pour vérifier qu’il marche bien net que l’on peut y accéder.

III.2 Modification du fichier index.js

Dans le fichier index.js  on ajoute la ligne suivante en début de fichier :

import niveau1 from "/src/js/niveau1.js"; 

Toujours dans le fichier index.js  sur la variable config, nous allons charger la scène niveau1 :

Modifier la ligne suivante : 

  scene: [selection] 

En rajoutant niveau1 après selection. Vous devez obtenir la ligne ci-dessous : 

  scene: [selection, niveau1] 

 fichiers seront désormais chargés lors du lancement du jeu. S’il n”y a pas de problème, lorsque vous lancez votre jeu, ce dernier se lance correctement mais vous êtes toujours dans le niveau selectionIl ne reste plus qu’à mettre en place une transition du niveau selection vers niveau1 

III.3 Ajout d'une transition simple de selection vers niveau1

Ajoutons une transition simple permettant d’illustrer le changement de niveau  : passons de selection vers niveau1 quand le joueur appuie sur la barre espace. Nous utilisons Phaser.Input.Keyboard.JustDown(clavier.space) pour de détecter quand la touche espace vient d’etre pressée. 

Dans le fichier selection.js, au sein de la fonction update(), ajoutez la condition suivante :

  if (Phaser.Input.Keyboard.JustDown(clavier.space) == true) {
      this.scene.start("niveau1");
    } 

Sauvegardez et relancez votre jeu. Déplacez votre personnage puis appuyez sur la touche espace. Et hop nous voilà au niveau 1! vous avez réussi votre première transition d’une scène à une autre.

IV - Ajout de deux autres niveaux

IV.1 Création des niveaux 2 et 3 à partir d'un code existant

Une fois l’ajout d’un niveau effectué, l’ajout de deux autres niveaux est trivial. Créez deux nouveaux fichiers niveau2.js et niveau3.js dans le répertoire /src/js/,  et copiez-y tout le code que vous aviez dans le fichier  niveau1.js.

Modifiez ensuite dans niveau2.js les trois éléments faisant référence à niveau 1 en les transformant en niveau2, à savoir le nom de la classe, le nom de la clé dans le constructeur et le texte affiché dans le niveau

export default class niveau2 extends Phaser.Scene { 
   key: "niveau2" //  ici on précise le nom de la classe en tant qu'identifiant 
this.add.text(400, 100, "Vous êtes dans le niveau 2", { 

faites la même chose dans niveau3.js

IV.2 Modification du fichier index.js

Dans le fichier index.js  on ajoute les lignes suivantes en début de fichier :

import niveau2 from "/src/js/niveau2.js";
import niveau3 from "/src/js/niveau3.js"; 

Enfin on modifie le paramètre scene dans la variable config comme suit :

  scene: [selection, niveau1,niveau2, niveau3] 

Tous nos niveaux sont chargés. On ajoute enfin les transitions simplifiées entre niveaux.

IV.3 Ajout de transitions simples de niveau1 vers niveau2, et niveau2 vers niveau3

Ajoutons une transition simple permettant d’illustrer le changement de niveau  : passons de selection vers niveau1 quand le joueur appuie sur la barre espace. 

Dans le fichier niveau1.js, au sein de la fonction update(), ajoutez la condition suivante (attention, comme on a transformé toutes nos variables en attribut, il s’agit de this.clavier.space et non juste clavier.space comme dans selection) :

  if (Phaser.Input.Keyboard.JustDown(this.clavier.space) == true) {
      this.scene.start("niveau2");
    } 

Dans le fichier niveau2.js, au sein de la fonction update(), ajoutez la condition suivante :

  if (Phaser.Input.Keyboard.JustDown(this.clavier.space) == true) {
      this.scene.start("niveau3");
} 

Relancez votre jeu et testez que vous passez bien de selection à niveau1, de niveau1 à niveau2, et de niveau2 à niveau3 en appuyant à chaque fois sur la barre espace.

V - Gestion améliorée des transitions entre les scènes

Maintenant que les transitions sont fonctionnelles, nous allons modifier notre code pour inclure des portes qui permettront de naviguer entre les niveaux. Nous adopterons un fonctionnement simplifié mais similaire à ce qui a été présenté sur le tutoriel “ouvrir une porte en appuyant sur espace” qui détaille les mécanismes rapidement utilisés ci-dessous.

V.1 - Ajout de portes de navigation de selection vers les autres niveaux

Commencez par récupérer l’archive contenant les assets des portes en cliquant sur ce lien et dézippez les trois fichiers door1.png, door2.png et door3.png dans votre dossier d’assets.

Chargez ensuite ces trois images avec les mots-clés “img_porte1″, “img_porte2” et “img_porte3” : dans le fichier selection.js, sur la méthode preload(), ajoutez les lignes de chargement suivantes : 

this.load.image('img_porte1', 'assets/door1.png');
this.load.image('img_porte2', 'assets/door2.png');
this.load.image('img_porte3', 'assets/door3.png'); 

Plaçons ensuite ces portes, en tant que sprites statiques (immobiles) à différentes coordonnées pré-calculées, référencés par des attributs porte1, porte2 et porte3. Toujours dans selection.js, mais dans la méthode create() cette fois, ajoutez les lignes suivantes :

    this.porte1 = this.physics.add.staticSprite(600, 414, "img_porte1");
    this.porte2 = this.physics.add.staticSprite(50, 264, "img_porte2");
    this.porte3 = this.physics.add.staticSprite(750, 234, "img_porte3");
 

Rechargez votre jeu : les trois portes sont placées.

V.2 - Activation des portes

  • Nous allons écrire le code permettant d’activer ces trois portes. Chacune des portes emmènera vers un niveau distinct, respectivement porte1 vers niveau1, porte2 vers niveau2 et porte3 vers niveau3. Nous proposons le principe d’activation suivant : lorsque l’on presse la touche espace, la porte sur laquelle on se situe est ouverte. Nous utilisons this.physics.overlap() pour déterminer si les hitbox de deux objets en paramètre s’intersectent dans la scène.
Dans le fichier selection.js, au niveau de la fonction update(), modifiez le code gérant l’appui sur la touche espace pour qu’il corresponde au code suivant : 
    if (Phaser.Input.Keyboard.JustDown(clavier.space) == true) {
      if (this.physics.overlap(player, this.porte1)) this.scene.start("niveau1");
      if (this.physics.overlap(player, this.porte2)) this.scene.start("niveau2");
      if (this.physics.overlap(player, this.porte3)) this.scene.start("niveau3");
    } 

relancez votre jeu et testez plusieurs fois ce dernier : chaque porte débouche bien sur un niveau précis. Pour être complet, nous allons rajouter à chaque niveau la porte retour.

V.3 - Mise en place d'une porte retour

Dans niveau1.js , ajoutez dans create() le sprite statique correspondant à la porte 1, référencé par l’attribut porte_retour :

    this.porte_retour = this.physics.add.staticSprite(100, 550, "img_porte1"); 

 Au niveau de la fonction update(), modifiez le code gérant l’appui sur la touche espace pour qu’il corresponde au code suivant :

    if (Phaser.Input.Keyboard.JustDown(this.clavier.space) == true) {
      if (this.physics.overlap(this.player, this.porte_retour)) {
        this.scene.start("selection");
        }
    } 

Testez à nouveau votre jeu : la porte vers le niveau 1 emmène vers le niveau 1, et la porte retour qui s’y trouve ramène bien vers le niveau selection.

Complétez votre jeu en rajoutant les portes retours pour les niveaux 2 et 3.  Changez bien les textures des portes pour qu’elles correspondent bien à la porte du niveau – img_porte2 pour niveau2, et img_porte3 pour niveau3 -.

V.4 -Gestion améliorée des scènes

Vous l’aurez remarqué, mais lorsque vous naviguez entre les niveaux, vous recommencez à chaque fois le niveau à zéro! Quand on lance une scene avec this.scene.start(cible) , la scene actuelle est définitivement arrêtée et la scène cible est entièrement (re)lancée. Les mécanismes de gestion des scènes sont bien plus complexes qu’on ne le pense, en effet plusieurs scènes peuvent tourner en parallèle. 

Une transition simple, plus fluide, consiste à utiliser this.scene.switch(cible) plutot que this.scene.start(cible) . Avec cette simple modification : la scène actuelle n’est pas stoppée mais juste mise en pause, et la scène cible lancée depuis le début ou reprise de pause si elle était en pause.

Remplacez dans tous les fichiers this.scene.start(…) par this.scene.switch(…) et admirez le résultat! désormais quand vous revenez dans le niveau selection vous êtes automatiquement sur la porte que vous aviez laissée. Si vous revenez dans un niveau particulier, vous ne recommencez plus au dessus de la porte mais directement sur la porte.

En fonction de ce que l’on veut faire ou des règles envisagées sur le jeu, l’utilisation de scene.start() ou scene.switch() peut être plus appropriée.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

20 + = 22