Créer son premier jeu de plate-forme en découvrant Phaser

I - Avant-propos : présentation de Phaser et du jeu développé

I.1 comprendre la philosophie Phaser

Phaser est une librairie js permettant de créer des jeux vidéos. Elle propose de nombreux outils incluant un moteur physique, un gestionnaire d’animation, de son, de collisions et bien d’autres choses encore. L’objectif de ce tutoriel est de découvrir les principaux mécanismes de Phaser et de pouvoir réaliser un premier jeu de plate-formes simple décrit ci-dessous et présenté en démo. 

Le jeu mis en place est issu du tutoriel d’initiation à Phaser, et a été adapté pour être plus compréhensible et comprendre les mécanismes pas toujours intuitifs de Phaser.

I.2 Présentation du jeu réalisé

Le descriptif  général du jeu que nous allons réaliser est le suivant  :

  • Le jeu que nous mettons en place est un jeu de plate-formes, avec gravité.
  • Le monde de jeu est entièrement présenté à l’écran, il n’y a pas de scrolling.
  • Le joueur incarne un personnage qui doit ramasser des étoiles tombées du ciel.
  • Le personnage peut se déplacer à gauche et à droite, et sauter sur les différentes plates-formes
  • Lorsque toutes les étoiles affichées à l’écran ont été ramassées, une nouvelle série d’étoiles apparaît, ainsi qu’une bombe. Si le personnage touche cette bombe, il perd la partie.
  • A chaque fois série d’étoiles ramassées , une bombe supplémentaire apparait, rendant
    le jeu de plus en plus difficile.
  • Les bombes ne disparaissent jamais, et rebondissent sur les parois et l’écran de jeu.
  • Chaque fois que le personnage ramasse une étoile, son score 
    augmente de 10. L’objectif est de faire le plus gros score.

I.3 Démo du résultat à obtenir avec ce tutoriel

I.4 Vocabulaire élémentaire

Nous listons ici le vocabulaire spécifique que nous allons employer sur ce tutoriel :

  • Un sprite est un élément de jeu qui a vocation à se déplacer et/ou à changer d’apparence au cours du jeu, à l’inverse d’un élément de décor ou d’une plate-forme fixe.
  • Un spritesheet est une grande image comprenant plusieurs images – généralement de même taille –  décomposant le mouvement d’un sprite, appelées frames. Pour être correctement exploitable, il est nécessaire de connaitre la taille des frames comprenant le spritesheet. 
  • Un asset est un fichier de ressources du jeu. Il peut s’agir d’une image de fond, d’un spritesheet, d’un fichier sonore, d’une carte … Lorsqu’on code un jeu, les assets sont généralement regroupés dans le même répertoire.
  • la hitbox est la zone sensible d’un élément de jeu aux plate-formes, ou aux projectiles ennemis. Cette zone permet de définir précisément si des éléments se touchent, et/ se superposent. Il peut s’agir d’un polygone avec une forme complexe, ou d’un simple rectangle. La hitbox est fortement liée au moteur physique du jeu.

II - Mise en place d’un squelette de développement

Phaser permet de développer des jeux jouables sur navigateur web. Pour développer sur phaser il nous faut un environnement de développement javascript comprenant un éditeur de code, la librairie Phaser, et un rendu web afin de pouvoir visualiser le résultat. Vous pouvez mettre en place votre propre environnement de développement, récupérer la librairie phaser et l’intégrer comme vous le souhaitez.

Néanmoins, il est fortement conseillé de passer par codesandbox (codesandbox.io), un outil en ligne permettant de déployer très rapidement des projets web. Un template spécialement concu pour Phaser y a été mis en place. Ce tutoriel s’appuie sur une organisation du projet similaire à celle que vous pourriez avoir sur codesandbox.io en réalisant les étapes de prise en main suivantes, notamment sur l’organisation des fichiers.

  • Créer un compte sur codesandbox (via Github), et se connecter;
  • créer une nouvelle sandbox (create sandbox);
  • choisir « explore templates » et rechercher dans la barre de recherche « phaser environnement vide » ;
  • sélectionner ce template. Et voila, votre environnement de développement est prêt!

Une fois enregistrée (ctrl+s) cette sandbox pourra être à nouveau accessible sur codesandbox.io dans le tableau de bord (dashboard) , répertoire My drafts (Brouillons) ou All sandboxes selon le cas.

En passant par le template de codesandbox mentionné précédemment, vous avez déjà tous les éléments de code présentés dans la section II.  Néanmoins nous allons les détailler dans les sections suivantes pour avoir une compréhension totale du fonctionnement de Phaser et du template proposé.

II.1 Organisation des fichiers et bonnes pratiques

Un exemple de bonne organisation  est présenté dans le template «phaser environnement vide »

Nous allons utiliser 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

II.2 La scène de jeu et ses fonctions preload(), create() et update()

Tout jeu phaser contient a minima une scène de jeu : une scène de jeu est un écran de jeu contenant des sprites, des plates-formes, des animations, et les interactions que vont avoir ces éléments entre eux. On pourrait grossièrement l’associer à un “niveau de jeu”.  

La scène est au coeur d’un jeu phraser, il en faut au moins une, mais on peut en avoir plusieurs. Chaque scène est décrite par trois fonctions essentielles qu’il va nous falloir définir : preload(), create() et update(). Dans ce tutoriel il n’y a qu’une scène, la scène de jeu principale. Si on avait un jeu multi-niveaux, on aurait besoin d’autant de scènes que de niveau, chacune ayant ses propres fonctions preload(), create() et update().

Nous entendons par squelette de développement la mise en place d’une scène de jeu unique ainsi que de ses 3 fonctions, vides pour l’instant. 

II.2.1 : la fonctions preload()

La fonction preload() est appelée juste une fois  lors du chargement de la scène dans le jeu Phaser.  Si la scène venait à être  relancée, cette fonction ne serait pas rappelée pour autant. On va y trouver principalement des instructions de chargement des assets (images, son ..), bien que d’autres instructions soient possibles. Ces instructions de chargement serviront à charger un fichier en mémoire, et à lui attribuer un identifiant de référence

Pour créer une fonction preload() vide, Nous rajoutons à notre code – et en dehors de toute fonction – les instructions suivantes :

function preload() {
    // vide pour l'instant
} 

II.2.2 : la fonctions create()

La fonction create() est appelée à chaque fois que la scène de jeu est lancée ou relancée. On va y trouver la plupart des instructions de création du niveau : création des sprites, des plates-formes, des animations, des ennemis et bien d’autres… On y trouve également des instructions permettant de définir des comportements par défaut : les mécanismes de collision entre un personnage et le sol, par exemple.

Pour créer une fonction create() vide, Nous rajoutons à notre code – et en dehors de toute fonction – les instructions suivantes :

function create() {
    // vide pour l'instant
} 

II.2.3 : la fonctions update()

La fonction update() est un peu spéciale : il s’agit d’une fonction qui est exécutée de manière permanente et récurrente tant que la scène de jeu est active et tourne! Dit autrement il s’agit d’une fonction exécutée au sein de la boucle de jeu : toutes les instructions de cette fonction vont être répétées indéfiniment; dès que la fonction update() termine son exécution, elle se ré-exécute à nouveau et ainsi de suite. On peut imaginer que toutes les instructions de la fonction update() seraient dans une grande boucle while, sauf qu’ici le while est implicite et est géré par Phaser. 

On va y trouver dans cette fonction la plupart des instructions dépendantes du comportement du joueur, des ennemis, ou du système d’une manière générale t bien d’autres… On y trouve également des instructions permettant de définir des comportements par défaut : les mécanismes de collision entre un personnage et le sol, par exemple.

Pour créer une fonction create() vide, Nous rajoutons à notre code – et en dehors de toute fonction – les instructions suivantes :

function update() {
    // vide pour l'instant
} 

II.2.4 Configuration et création d’une l’instance de jeu

L’étape suivante consiste à configurer Phaser pour notre jeu. On crée une variable nommée config comprenant toutes les informations de configuration. Ces informations sont présentées  au format json sous forme de liste de variables séparées par des virgules,  pour lesquelles on associe une valeur à chacune. Une liste de variables est définie par des accolades { }, et ses éléments sont séparés par des virgules. Chaque élément est un couple variable : valeur.  La valeur associée peut etre simple (par exemple un nombre), ou complexe, par exemple une sous-liste de variables / valeurs. Rajoutons à notre programme le morceau de code suivant – en dehors de toute fonction – :

var config = {
  type: Phaser.AUTO,
  width: 800, // largeur en pixels
  height: 600, // hauteur en pixels,
  scene: {
    // une scene est un écran de jeu. Pour fonctionner il lui faut 3 fonctions  : create, preload, update
    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)
  }
}; 

Les variables présentées dans config et leurs valeurs respectives sont détaillées ci-dessous  :

  • type définit le type de jeu (choisi automatiquement par Phaser : Phaser.AUTO )
  • width et height sont les dimensions de la fenêtre de jeu– en pixels.  Le jeu sera donc présenté ici dans un cadre de 800×600 pixels. On pourra demander plus tard à adapter la résolution en fonction de la taille d’écran
  • scene est l’élément central du jeu. Il s’agit de la liste des scènes de jeu. Il n’y a qu’une scène ici. Il faut lui préciser quelles sont les les noms des fonctions de préchargement (preload), de création (create), et de mise à jour (update). Nous avons utilisé les mêmes noms preload / create  et update pour ces fonctions, mais il faut  simplement le préciser ici avec la liste {preload : preload, create : create, update : update}. Ceci peut paraitre un peu déroutant du fait de la redite mais est normal.

Si on a besoin d’éléments de configuration supplémentaire, on les rajouter dans le bloc identifié par config, en veillant toujours à séparer les variables par des virgules

Une fois les paramètres de configuration définis, nous lançons une instance de notre jeu avec l’appel à la méthode Phaser.Game(), en lui passant en paramètre les élément de configuration fraichement définis  via la variable config :

// création et lancement du jeu
new Phaser.Game(config); 

Nous pouvons déjà visualiser un résultat : sauvez-vos modifications (ctrl + s si vous utilisez codesandbox) et visualisez le résultat : un écran noir de 800 par 600 pixels apparait. Il s’agit de l’écran de jeu dans lequel apparaitrons tous les visuels.

II.2.5 Mise à jour de la configuration : ajout du moteur physique

Nous avons un jeu, mais nous n’avons pas encore configuré le moteur physique à utiliser.

Nous allons modifier la variable config  précédemment définie pour qu’elle contienne désormais des informations de configuration du moteur physique. Au sein de la variable config, ajoutez le morceau de code suivant en veillant à l’intégrer correctement par rapport aux autres éléments déjà définis  (ajout de virgules pour séparer avec les autres éléments de config)

var config = { // ligne déjà écrite
    // ...  (ajout d'une virgule apres le dernier élement)
    
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 300 },
            debug: false
        }
    }
   // ...

}; // ligne fermant le bloc config 

Ce bloc rajoute l’élément de configuration physics :, dont la valeur est une liste  plusieurs couples elements / valeurs :

  • default indique le moteur physique à utiliser. Ici nous utilisons ‘arcade’ : il s’agit d’un moteur physique  « simplifié » , qui gère la gravité et les collisions ou superpositions entre éléments dont la zone de hitbox est un rectangle ou un cercle uniquement : concrètement cela veut dire que nos sprites auront une enveloppe corporelle rectangulaire sans rotation ou circulaire, même si leur texture laisse entrevoir des contours plus travaillés.  Il s’agit d’une limitation forte mais ce modèle physique  reste amplement suffisant pour l’heure; d’autres modèles plus complexes peuvent être utilisés.
  •     le moteur physique étant ‘arcade’, nous ajoutons des éléments de configuration propres à ce dernier :
    • gravity indique la gravité sur notre jeu de plate-formes : ici une gravité sur l’axe des y uniquement, de l’ordre de 300 : à pleine vitesse, tout sprite sujet à la gravité descendra de 300 pixels par seconde sur l’écran
    • debug, ici positionné à false, permet de debugger le jeu : lorsque sa valeur est passée à true, on peut voir la hitbox de chaque sprite ainsi que son vecteur de déplacement (distance + force) . Activer le debug peut se révéler particulièrement interessant pour comprendre les mécanismes de collision / superposition.

III - Réalisation du plateau de jeu

Dans cette partie nous allons créer tout le plateau de jeu. Notre écran de jeu fait 800 x 600 pixels, comme défini dans la variable config.  La coordonnées (0,0) est située tout en haut à gauche de l’écran, la coordonnées (800,600) tout en bas à droite. Le centre de la scène est en coordonnée (400,300).

L’objectif est d’obtenir un plateau similaire à l’image ci-dessous :

  • Ce plateau est composé à partir de deux images seulement : 
  • une image sky.png  de 800 x 600 pixels, qui représente le ciel bleu
  • une image platform.png de 400 x 32 pixels, qui représente une plate-forme.

Le sol est composé de deux images de plate-formes mises cote à cote, tandis que les plates-formes.

l'image platform.png (400 x 32 pixels)

III.1 Chargement des assets (fonction preload)

Nous chargeons tout d’abord les images de notre jeu pour pouvoir les utiliser par la suite. Ceci se fait une fois à  l’initialisation de la scène, donc dans la fonction preload(). Ce chargement va associer à chaque image chargée un mot-clé, qui sera utilisé par la suite pour désigner l’image. Ajoutez tout d’abord la ligne suivante :

 // tous les assets du jeu sont placés dans le sous-répertoire src/assets/
  this.load.image("img_ciel", "src/assets/sky.png"); 

Cette ligne permet de charger l’image sky.png du répertoire src/assets/ et de lui attribuer le mot-clé ‘img_ciel’. On pourra ensuite utiliser ce mot-clé pour référencer cette image.

En vous inspirant de cette ligne, ajoutez une ligne de code juste en dessous permettant de charger le fichier platform.png (situé dans le même répertoire src/assets/) et lui attribuer le mot clé “img_plateforme“.

III.2 Placement du fond d'écran (fonction create)

Commençons la création de notre scène en plaçant  l’image de fond: au sein de la fonction create() nous appelons l’instruction permettant de placer une image (référencée par un mot-clé) à des coordonnées données :  ajoutons simplement cette ligne de code :

  this.add.image(400, 300, "img_ciel"); 

Quand on ajoute une image, son point d’ancrage est situé par défaut en son centre. Rappelons que l’écran de jeu faisant 800 x 600 pixels.  Cette instruction place ainsi l’image  img_ciel aux de sorte que son centre se situe aux coordonnées 400 x 300, soit le centre du de la fenêtre de jeu!  Ce qui permet de couvrir toute la fenêtre de jeu. 

Sauvegardez et raffraichissez l’écran : le ciel doit désormais apparaître sur votre jeu.

III.3 création des plates-formes et du sol

Sur la figure présentant le plateau de jeu, il y a en fait 5 plates-formes : 

  • le sol, constitué de deux plates-formes voisines,
  • une plate-forme flottante à gauche
  • et deux plates-formes flottantes à gauche.

Chaque élément peut-être considéré de façon individuelle, par rapport à son motif, son positionnement. Mais il est aussi pertinent de les envisager groupées, et de les considérer comme une seule grosse entité. Le fait de les considérer ainsi permet d’appliquer les mêmes caractéristiques physique un seule fois sur le groupe, et non par plate-forme. (cette approche sera reprise par la suite sur les étoiles et les bombes). Ainsi  la création d’un groupes permet de gérer simultanément les éléments d’une meme famille.

Par exemple, on ne va pas définir les mécanismes de collision entre chaque plate-forme et le joueur, mais on va grouper les plate-formes dans un groupe, et définir une seule fois les mécanismes de collision entre les  ce groupe (sous entendu entre les éléments de ce groupe) et le joueur. Il en est ainsi pour tout type d’éléments qui peuvent se grouper.

 Au sein du programme principal, on va d’abord créer la variable groupe_platformes qui va permettre de référencer le groupe de plate-formes.. Le groupe groupe_plateformes contiendra ici le sol (deux plate-formes)  et trois plates-formes  sur lesquelles sauter. Pour déclarer  la variable qui référence ce groupe, on ajoute tout d’abord la ligne  suivante, qui doit  doit être définie de manière globale – en dehors de toute fonction –  afin qu’on puisse accéder par la suite depuis n’importe laquelle des fonctions (preload(), create(), ou update() ) : 

var groupe_plateformes; 

Nous allons ensuite créer un groupe contenant les plate-forme, qui sera référencé par platforms. La création de ce groupe se fait en début de jeu, donc au sein de la fonction create. Par ailleurs, ce groupe sera dit statique par utilisation du mot clé staticGroup : concrètement, cela signifie que les éléments que l’on va ajouter au groupe seront non seulement fixes, mais pas sujets à la gravité. On ne pourra pas les pousser.Au sein de la fonction create(), on ajoute la ligne suivante :

  groupe_plateformes = this.physics.add.staticGroup();
  

Notre groupe statique est créé. Il est pour l’instant vide.  Une fois le groupe créé, on va créer les plates-formes , le sol, et les ajouter au groupe groupe_plateformes. Ajoutons déjà le sol. Juste après la création du groupe, ajoutons les deux instructions suivantes dans la fonction create() :

  groupe_plateformes.create(200, 584, "img_plateforme");
  groupe_plateformes.create(600, 584, "img_plateforme"); 

En temps normal; il faudrait deux instructions : (1) d’abord créer un objet représentant une plate-forme en définissant ses coordonnées, (2) puis l’ajouter au groupe groupe_plateformesLa méthode create() de groupe_plateformes est bien utile car elle permet de faire ces choses en une ligne. Les coordonnées (200, 584) et (600,584) définissent les centres des deux plate-formes ajoutées.

Sauvegardez et raffraichissez l’écran : vous avez désormais un sol. 

Ajoutez une nouvelle plate-forme aux coordonnées (50, 300) : 

  groupe_plateformes.create(50, 300, "img_plateforme");
 

Avec cette instruction, on vient d’ajouter un élément représenté par l’image associée au mot clé img_plateforme. On vient de placer son centre aux coordonnées (50, 300) ce qui correspond la plate-forme en haut à gauche sur le plateau de jeu. Cette plate-forme dépasse surement à gauche de l’écran, mais ce n’est absolument pas un problème ici, car notre zone d’affichage est figée sur le rectangle dont les coordonnées opposées sont (0,0) et (800, 600).

En vous inspirant de cette ligne, ajoutez deux plate-formes aux coordonnées (600, 450) et (750, 270)

Vérifiez enfin que votre affichage correspond au plateau de jeu envisagé.

IV - Mise en place du personnage principal

Dans cette section nous allons ajouter le personnage du joueur. Cet ajout consiste dans un premier temps à charger un nouvel objet, puis à le faire se déplacer en fonction du pavé directionnel du clavier, et enfin à animer son déplacement selon la direction choisie.

IV.1 Chargement des asset (fonction preload)

Nous allons d’abord charger le fichier image du personnage. Il s’agit d’un fichier spécial qui contient toutes les animations de notre personnage, qu’on appelle un spritesheet. Ce fichier comporte les différents mouvements décomposés du joueur. Contrairement au fichier plate-forme, tout Sprite qui utilisera cette texture ne doit pas se baser sur toute la taille de l’image, mais seulement sur une partie.  Lorsqu’on charge un spritesheet, il est nécessaire de préciser la taille d’un motif. Alors en connaissant cette taille et en lisant les dimensions globales du fichier, Phaser peut déterminer le nombre de motifs présents sur le fichier. Prenons l’exemple du fichier ci-dessous, dude.png) qui représentera notre personnage: 

Ici ce fichier fait 48 pixels de haut, et 288 pixels de large. Il présente 9 motifs alignés sur une seule ligne, soit, par motif,  32 pixels de large (le frameWidth) pour 48 pixels de haut (le frameHeight) (voir Figure ci-dessous). Ces  La syntaxe diffère un peu du chargement d’une simple image puisqu’il s’agit d’un spritesheet que l’on va charger. Aussi au sein de la fonction preload() , à la suite des instructions de chargement des images, ajoutez la ligne suivante :

this.load.spritesheet("img_perso", "src/assets/dude.png", {
    frameWidth: 32,
    frameHeight: 48
  }); 

Cette ligne nous dit que nous chargeons dans notre scène de jeu (identifiée par le mot-clé this) un spritesheet qui va être référencé sous le mot-clé img_perso. Le fichier spritesheet est situé dans src/assets/dude.png et comme il s’agit d’un fichier contenant plusieurs image il est important de préciser la taille du cadre à prendre dans cette image, 32 pixels de large (frameWidth : 32) et 48 pixels de haut (frameHeight :48 ). Désormais notre image est découpée en frames, ici 9 frames numérotées de 0 à 9 : la frame 0 représente le personnage tourné à gauche et immobile, la frame 4 représente le personnage de face, les frames 5 à 8 représentent le personnage tourné à droite …

IV.2 Création du Sprite personnage et gestion des collisions (fonction create)

Ajoutons désormais notre personnage principal. De la même facon que pour groupe_plateformes, nous ajoutons dans le programme principal – en dehors de toute fonction – une variable nommée player. Cette variable sera une référence-objet vers le Sprite qui identifiera notre joueur. Ajoutez tout d’abord la ligne suivante :

var player; // désigne le sprite du joueur 

Ensuite nous allons créer ce joueur : dans la fonction create(), nous ajoutons notre sprite à la scène de jeu, en position (100, 450), et lui attribuons l’image associée à img_perso.

player = this.physics.add.sprite(100, 450, 'img_perso'); 

Allez voir ensuite votre jeu en actualisant la page d’affichage plusieurs fois pour bien identifier les nouveaux éléments. Plein de choses se sont passées :

  • Tout d’abord, le personnage est apparu aux coordonnées indiquées, en l’air.
  • Sa texture, que définie par le spritesheet  img_perso, est la première frame de ce dernier.
  • Immédiatement le personnage a été attiré vers le bas : c’est l’effet de la gravité verticale du jeu, entièrement gérée par le moteur physique
  • Mais le personnage ne s’est pas arrêté: il a traversé le sol, puis la zone d’affichage.

Ceci est normal à notre niveau de développement. Nous n’avons pas précisé avec quels éléments le personnage pouvait rentrer en collision Empêchons tout d’abord le personnage de sortir de l’écran de jeu : nous ajoutons dans la fonction create(), à la suite de la création du personnage, l’instruction suivante :

    player.setCollideWorldBounds(true); 

Cette instruction dit tout simplement que l’objet référencé par player doit désormais se cogner aux bords de la fenêtre de jeu et ne plus la traverser (setCollideWorldBounds = “mise en place de collision avec les frontières du monde“). Relancez le jeu, notre joueur s’arrête désormais  au bord de la fenêtre de jeu. 

C’est mieux mais pas parfait, puisque notre personnage traverse encore sol. On va donc rajouter la notion de collision entre les plate-formes et notre personnage de jeu, grâce à la méthode collider() du moteur physique de notre jeu. Ceci permet de dire que les deux éléments ne peuvent se chevaucher et sont sujets aux interactions physiques du monde. 

Comme nous avons mis toutes les plate-formes dans un groupe appelé groupe_plateformes, nous allons indiquer que le personnage doit rentrer en collision avec n’importe quel élément de ce groupe.  Dans la fonction create() on ajoute, une fois le groupe groupe_plateformes  et le sprite player définis, l’instruction suivante :

this.physics.add.collider(player, groupe_plateformes); 

Relancez votre jeu. Cette fois notre personnage s’arrête pile sur le sol. Gagné !
Evitons qu’il ne se blesse en tombant : ajoutons-lui un petit effet de rebond (en anglais Bounce)avec la ligne suivante, toujours dans la fonction create() :

player.setBounce(0.2); 

La valeur 0.2 permet de dire que quand le personnage rebondit, il le fait dans la direction opposée avec une intensité correspondant à 0.2 fois (ou 20%) la vitesse avec laquelle il arrivait sur l’objet en collision. En relançant a nouveau le jeu vous pouvez constater ce petit effet rebond désormais visible.

IV.3 Gestion des déplacements

Nous allons ensuite faire déplacer notre personnage. Pour cela il va nous falloir récupérer les touches pressées au clavier, puis associer une action à la touche pressée. Cette action sera généralement d’appliquer une vitesse de déplacement au personnage lorsque la touche est pressée, et de retirer cette vitesse lorsque la touche est relâchée.

IV.3.1 Ajout d'un clavier

On va tout d’abord définir une variable clavier qui va servir de référence vers un objet complexe représentant le clavier du joueur. Dans le programme principal, ajoutez cette variable avec la ligne suivante :

var clavier; 

Ensuite nous allons initialiser la variable clavier pour qu’elle désigne un écouteur clavier : il s’agit d’un objet complexe – mais déjà présent dans Phaser – qui va nous permettre de savoir si certaines touches du clavier ont été frappées au clavier (soit appui simple, soit touche enfoncée). Ici nous utilisons une méthode simplifiée, createCursorKeys(), qui permet de surveiller 6 touches en particulier : haut / bas / gauche / droite / espace et shift, chacune associée respectivement à l’attribut up, down, left, right, space ou shift de l’écouteur (cette partie semble un peu technique mais sera détaillée ci-après). Pour l’instant nous ne pourrons manipuler que 6 touches; ceci semble très réduit pour l’instant mais suffisant pour ce que l’on souhaite faire. Au sein de la fonction create(), nous ajoutons donc la ligne suivante :

clavier = this.input.keyboard.createCursorKeys(); 

Notre écouteur clavier est prêt. Notre clavier possLa prochaine étape consiste à surveiller certaines touches et à programmer les actions correspondantes lorsque  ces touches sont pressées.

IV.3.2 association des touches clavier, déplacement gauche / droite

En phase de jeu, nous sommes non plus dans la fonction create(), mais au sein de la fonction update(). C’est dans cette fonction que l’on analyse les événements survenus en cours de jeu. Ajoutez le bloc suivant : 

  if (clavier.right.isDown == true) {
    player.setVelocityX(160);
  } 

Que nous dit ce bloc? Que lorsque la touche droite du clavier (représentée par l’attribut right de l’écouteur clavier, littéralement clavier.right ) est pressée ( statut down égal à true)  on applique une vitesse sur le player, sur l’axe des X d’une valeur de 160 grâce à l’accesseur en écriture setVelocityX() ;  Une valeur négative indique que l’on irait vers la gauche, tandis qu’une valeur positive indique qu’on irait à droite.  Ici 160 correspond ici à l’intensité (nombre de pixels déplacés par secondes).

Sauvegardez, testez votre jeu. Dès que vous appuyez sur la flèche de droite, votre personnage se déplace vers la droite… mais ne s’arrête plus!

Modifiez votre code de la façon suivante en modifiant le bloc précédent écrit avec les instructions ci-dessous.  Il vous faudra compléter ces instructions pour que le personnage se déplace à gauche si la touche gauche (représentée par clavier.left) est pressée , et ne se déplace plus si ni l’une ni l’autre des touches n’est pressée. 

  if (clavier.right.isDown) {
    player.setVelocityX(160);
  } 
  else if ( /*** A COMPLETER ***/) {
    /*** A COMPLETER : appliquer une vélocité de -160 ***/ 
  } else {
    player.setVelocityX(0);
  } 

Testez votre jeu. Votre personnage se déplace bien à gauche et à droite, et stoppe quand vous n’appuyez sur aucune touche! Néanmoins il ne regarde pas encore du bon coté. Ce point sera corrigé par la suite.

IV.3.2 ajout de la fonction de saut

Nous allons compléter les mouvements avec l’action de saut. Le saut va être déclenché lorsque l’on appuie sur la touche espace (représenté par le mot-clé space) . Mais cette action n’est possible que si le corps du joueur touche le sol. Cette seconde conditions peut se modéliser facilement par le test player.body.touching.down.

Rajoutez, et complétez le code suivant en vous inspirant des actions de mouvements gauche et droite, pour que l’on applique une vélocité de -330 sur l’axe des Y avec la méthode setVelocityY() lorsque la touche espace (représentée par l’attribut clavier.space) est pressée.

    if (/*si ‘space’ est pressé */ && player.body.touching.down) {
       /*appliquer une velocite de -330 verticalement*/
    }
}
 
Testez votre jeu. Le personnage se déplace, et doit pouvoir sauter. Vérifiez qu’il ne peut pas sauter s’il est déjà en l’air. Reste le problème du sens dans lequel regarde notre personnage… nous allons régler cela en créant une animation.

IV.4 Définition et application des animations (create / update)

IV.4.1 Création et application d'une première animation

Pour gérer l’affichage des mouvements de notre personnage, nous allons créer des animations à partir du spritesheet stocké dans dude (Figure 2). La procédure est globalement la suivante :
– D’abord on crée une animation en définissant les images à utiliser du spritesheet et la vitesse
– On donne un nom à cette animation
– Enfin on la lance lorsque la touche correspondante est pressée.
Nous allons tout d’abord créer l’animation à lancer quand le joueur se déplace à gauche. Si l’on numérote les vignettes du spritesheet dude de 0 à 8, Cette animation doit utiliser les vignettes 0 à 3, et boucler.
Dans la fonction create(), on crée cette animation, et on lui donne comme nom (key) ‘anim_tourne_gauche

 // dans cette partie, on crée les animations, à partir des spritesheet
  // chaque animation est une succession de frame à vitesse de défilement défini
  // une animation doit avoir un nom. Quand on voudra la jouer sur un sprite, on utilisera la méthode play()
  // creation de l'animation "anim_tourne_gauche" qui sera jouée sur le player lorsque ce dernier tourne à gauche
  this.anims.create({
    key: "anim_tourne_gauche", // key est le nom de l'animation : doit etre unique poru la scene.
    frames: this.anims.generateFrameNumbers("img_perso", { start: 0, end: 3 }), // on prend toutes les frames de img perso numerotées de 0 à 3
    frameRate: 10, // vitesse de défilement des frames
    repeat: -1 // nombre de répétitions de l'animation. -1 = infini
  }); 

Comme on peut le lire sur ce code, on crée une nouvelle animation a la liste des animations (anims) du jeu (this). Cette animation a comme nom left, et ses frames sont récupérées à partir des vignettes 0 (start) à 3 (end) du spritesheet dude, ceci grâce à la méthode generateFrameNumbers qui fait tout le boulot
Une fois l’animation créée, il nous faut la lancer quand la touche left est pressée. Rien de plus simple : allons modifier l’action lancée quand on se déplace à gauche, la ligne ci-dessous (à ne pas ajouter de suite) permet de lancer l’animation “anim_tourne_gauche” sur le Sprite player : 

player.anims.play('anim_tourne_gauche', true); 

Le paramètre true permet de dire « il ne faut pas relancer l’animation si cette dernière était déjà en train d’être lancée ». Cela permet d’éviter les phénomènes de saccade. Vous pouvez ajouter cette ligne lorsque dans votre code existant la flèche gauche est pressée. Au final vous devez obtenir un bout de code similaire à ce dernier :

if (clavier.left.isDown) {      // lignes déja ajoutées précedemment
    player.setVelocityX(-160);  // lignes déja ajoutées précedemment
    player.anims.play("anim_tourne_gauche", true);
  }  //...lignes déja ajoutées précedemment 

En vous inspirant de ce code, ajoutez une animation similaire, nommée anim_tourne_droite, décrivant l’animation du joueur à lancer lorsque ce dernier se déplace à droite. Référez-vous au spritesheet pour trouver les indices de vignettes correspondantes. Puis associez-la à la touche droite du pavé directionnel, identifiée par le mot-clé right

Testez votre jeu. Vous pourrez constater que l’animation continue quand le personnage ne bouge plus. Ce qui est un peu gênant. Pour corriger ceci, il suffit d’ajouter une animation quand on n’avance plus. Dans create() on ajoute l’animation ‘anim_face’, uniquement constituée de la vignette 4.

IV.4.1 Création et application des autres animations
 // creation de l'animation "anim_tourne_face" qui sera jouée sur le player lorsque ce dernier n'avance pas.
  this.anims.create({
    key: "anim_face",
    frames: [{ key: "img_perso", frame: 4 }],
    frameRate: 20
  }); 
Re-testez votre jeu. Le personnage bouge désormais de façon impeccable !
player.anims.play('anim_face'); 

V - Ajouter des étoiles et un score

Nous allons créer un groupe d’étoiles dans lequel seront stockées les étoiles. Puis nous allons générer en début de partie les étoiles à différents points de la carte.

V.1 Création d’un ensemble d’étoiles

Dans le programme principal – en dehors de toute fonction – on rajoute une variable qui désignera notre groupe d’étoile :

var groupe_etoiles; // contient tous les sprite etoiles 

Dans la fonction preload() on charge ensuite l’image star.png et on lui attribue le mot-clé img_etoile :

  this.load.image("img_etoile", "src/assets/star.png"); 

Dans la fonction create() on va créer un nouveau groupe qui contiendra toutes nos étoiles. Notez qu’il s’agit d’un group et non d’un staticGroup comme c’était le cas pour les plate-formes. Ceci veut dire que les éléments de ce groupe seront sujets aux mécanismes physiques, à commencer par la gravité :

//  On rajoute un groupe d'étoiles, vide pour l'instant
  groupe_etoiles = this.physics.add.group(); 

Toujours au sein de la fonction create() : on crée 10 étoiles avec une boucle for directement ajoutés dans le groupe groupe_etoiles.

 // on rajoute 10 étoiles avec une boucle for :
  // on répartit les ajouts d'étoiles tous les 70 pixels sur l'axe des x
  for (var i = 0; i < 10; i++) {
    var coordX = 70 + 70 * i;
    groupe_etoiles.create(coordX, 10, "img_etoile");
  } 

Nos étoiles sont désormais créées. relancez votre jeu, une dizaine d’étoiles tombe désormais du ciel .. mais passent à travers les plate-formes. Rien de méchant, il nous suffit d’ajouter un collider entre les plate-formes et  le groupe d’étoiles. Ajoutez la ligne suivante : 

this.physics.add.collider(groupe_etoiles, groupe_plateformes); 

Désormais nos étoiles se heurtent bien au sol.

V.2 Personnalisation des étoiles : ajout d'un coefficient de rebond aléatoire

Pour ajouter un peu d’effet visuel, nous allons ajouter un coefficient de rebond aléatoire à chaque étoile, compris entre 0.2 et 0.4. Grace à la méthode Phaser.Math.FloatBetween(a,b), on peut récuperer une valeur flottante aléatoire comprise entre a et b. On va utiliser un iterateur, un mécanisme un peu complexe et non détaillé ici mais qui consiste à parcourir tous les éléments d’un groupe et leur appliquer les mêmes instructions. Au sein de la fonction create(), on ajoute donc les instructions suivantes :

  groupe_etoiles.children.iterate(function iterateur(etoile_i) {
    // On tire un coefficient aléatoire de rerebond : valeur entre 0.4 et 0.8
    var coef_rebond = Phaser.Math.FloatBetween(0.4, 0.8);
    etoile_i.setBounceY(coef_rebond); // on attribut le coefficient de rebond à l'étoile etoile_i
  }); 

Ce bloc d’instruction est  très bizarre mais est expliqué ci-après  :  on a utilisé un itérateur  (mot-clé iterate) sur les enfants (children) du groupe groupe_etoiles. Que signifie ce charabiat? Qu’on a fait un équivalent du “foreach”, en disant que pour chaque élément du groupe groupe_etoiles, on va appliquer les deux  instructions 1) définition d’un coefficient aléatoire entre 0.4 et 0.8 et 2) application de ce coefficient en tant que rebond avec la méthode setBounceY(). Ces deux instructions sont contenues dans la fonction  iterateur , qui est une fonction va etre lancée pour chaque élément de groupe_etoiles, c’est à dire pour chaque étoile  : à chaque itération, l’étoile en question est désignée par le paramètre etoile_i.

Quand on lance le jeu, on peut voir que les étoiles rebondissent désormais plus ou moins.

V.3 Collecte des étoiles (fonctions create et ramasserEtoile)

Il est temps de ramasses les étoiles.
En dehors de toute fonction, ajoutons une nouvelle fonction ramasserEtoile. Cette fonction est une fonction dite de callback (ou fonction rappel, mais personne n’utilise ce terme puisque 99% de la doc est en anglais) : il s’agit d’une fonction qui est  déclenchée lorsqu’un événement particulier se produit. Ici cette fonction sera déclenchée, lorsque notre personnage “marchera” sur une étoile, c’est à dire que leur deux hitbox s’intersectent. 

Dans un premier temps, cette fonction consistera simplement à désactiver l’étoile qui aura été passée en paramètre (la rendre invisible et désactiver sa hitbox).

function ramasserEtoile(un_player, une_etoile) {
  // on désactive le "corps physique" de l'étoile mais aussi sa texture
  // l'étoile existe alors sans exister : elle est invisible et ne peut plus intéragir
  une_etoile.disableBody(true, true);


} 

Pour déclencher cette action, nous allons ajouter une contrainte au moteur physique, qui dit que lorsque le personnage identifié par player intersecte (en anglais « overlap ») un objet du groupe groupe_etoiles, alors il faut exécuter la fonction ramasserEtoile() avec en paramètre les deux éléments qui s’intersectent : un player, et une étoile (l’étoile en question parmi toutes les étoiles du groupe groupe_etoiles ).

// si le player marche sur un élément de groupe_étoiles (c.-à-d. une étoile) :
  // on déclenche la function callback "collecter_etoile" avec en parametres les
  // deux élement qui se sont superposés : le player, et l'étoile en question
  // les actions à entreprendre seront écrites dans la fonction ramasserEtoile
  this.physics.add.overlap(player, groupe_etoiles, ramasserEtoile, null, this);
 

V.4 Régénération des étoiles

On avance sur le gameplay. L’étape suivante consiste à régénérer les étoiles une fois que ces dernières ont été toutes collectées. La méthode countActive() d’un groupe permet de savoir combien d’élément du groups sont encore actifs. Lorsque cette valeur retourne 0, il nous suffit de réactiver toutes les étoiles que nous avons désactivé lorsque nous les avons ramassées. Ainsi on aura l’impression qu’une nouvelle vague d’étoiles a été re-générée. La méthode enableBody() permet de réactiver l’étoile. On l’appelle via l’itérateur sur chaque élément du groupe stars
Le code ci-dessous est à placer dans la fonction ramasserEtoile(), puisqu’on va vérifier à chaque fois qu’une étoile est ramassée si c’était la dernière active ou pas. Notez que la encore on va utiliser un iterateur : s’il n’existe plus d’étoile active, alors pour chaque étoile (inactive) etoile_i du groupe groupe_etoiles , on va réactiver sa texture et sa hitbox avec la méthode enableBody(), et la repositionner sur ses coordonnées initiales (sur la même abscisse qu’avant (etoile_i.x) mais tout en haut de l’écran (0)

Notez l’attribut étoile_i.x ! il permet d’accéder facilement à l’abscisse de l’étoile en question. Chaque objet placé a en effet un attribut x et un attribut y qui permettent de connaitre la position dans l’espace de l’objet.

  // on regarde le nombre d'étoiles qui sont encore actives (non ramassées)
  if (groupe_etoiles.countActive(true) === 0) {
    // si ce nombre est égal à 0 : on va réactiver toutes les étoiles désactivées
    // pour chaque étoile etoile_i du groupe, on réacttive etoile_i avec la méthode enableBody
    // ceci s'ecrit bizarrement : avec un itérateur sur les enfants (children) du groupe (equivalent du for)
    groupe_etoiles.children.iterate(function iterateur(etoile_i) {
      etoile_i.enableBody(true, etoile_i.x, 0, true, true);
    });
} 

Re-testez votre jeu en ramassant toute les étoiles : une fois la dernière étoile ramassée, toutes les étoiles réapparaissent à leur position initiale.

V.5 Mise en place et affichage du score

L’ajout d’un score est relativement simple. D’abord on ajoute une variable score à notre programme principal, que l’on initialise à 0. On ajoute également une variable scoreText qui va nous permettre de gérer l’affichage du score : cette variable va référencer l’objet de type “texte” qui affichera le score.

var score = 0;
var zone_texte_score; 

Au sein de la function create(), nous allons placer le texte “score : 0” en haut à droite (coordonnées 16,16) avec une police de 32 px.

   zone_texte_score = this.add.text(16, 16, 'score: 0', { fontSize: '32px', fill: '#000' }); 

Il ne nous manque plus qu’à mettre à jour le score. Ceci se fait bien évidemment dans la fonction ramasserEtoile(), puisque le score est maj à chaque fois qu’une étoile est ramassée.

 //  on ajoute 10 points au score total, on met à jour l'affichage
  score += 10;
  zone_texte_score.setText("Score: " + score); 

V.5 Mise en place du score

VI - Ajout des bombes

Le dernier point consiste à ajouter des bombes. Les bombes permettent de terminer le jeu. Des que le joueur touche l’une d’entre elle, on termine la partie.

Nous allons tout d’abord ajouter un groupe groupe_bombes vide, sur un mécanisme similaire à ce que l’on a entrepris avec les étoiles. Dans le programme principal on ajoute :

VI.1 Génération de bombes

Dans le programme principal – en dehors de toute fonction – on rajoute une variable qui désignera notre groupe de bombes  :

var groupe_bombes; 

Dans la fonction create() on ajoute la ligne suivante pour créer un groupe vide groupe_bombes , de façon similaire à ce que l’on a fait avec les étoiles :

groupe_bombes = this.physics.add.group(); 

Ces bombes rebondiront sur le sol et les plates-formes. On n’oublie pas d’ajouter le collider entre les membres de ce groupe et les plates-formes.

this.physics.add.collider(groupe_bombes, groupe_plateformes); 

Générons enfin une nouvelle bombe lorsque toutes les étoiles ont été ramassées; Dans la fonction ramasserEtoile() , sur la partie de code indiquant que toutes les étoiles ont été ramassées (countActive() == 0) on rajoute les lignes suivantes  :

    // on ajoute une nouvelle bombe au jeu
    // - on génère une nouvelle valeur x qui sera l'abcisse de la bombe
    var x;
    if (player.x < 400) {
      x = Phaser.Math.Between(400, 800);
    } else {
      x = Phaser.Math.Between(0, 400);
    }

    var une_bombe = groupe_bombes.create(x, 16, "img_bombe");
    une_bombe.setBounce(1);
    une_bombe.setCollideWorldBounds(true);
    une_bombe.setVelocity(Phaser.Math.Between(-200, 200), 20);
    une_bombe.allowGravity = false;
  } 

Cette fonction effectue les actions suivantes :

  • On définit une variable x qui sera l’abscisse  de la nouvelle bombe. On définit cette abscisse de manière astucieuse : si l’absisse du joueur est dans la partie gauche de l’écran (son abscisse player.x est inférieure à 400) alors on va générer une abscisse aléatoire dans l’autre partie de l’écran (entre 400 et 800). Et réciproquement. Cela permet de ne pas générer une nouvelle bombe trop près du joueur. Notez l’attribut player.x ! il permet d’accéder facilement à l’abscisse de player.
  • On génère une nouvelle bombe appelée une_bombe en coordonnées (x, 16) , donc en haut de l’écran (16)
  • On lui met un coefficient de rebond de 1 (pour qu’elle rebondisse sans perte de vitesse)
  • On indique qu’elle ne pourra pas sortir de l’écran avec setCollideWorldBounds(true) 
  • On lui donne une vélocité initiale entre -200 et 200 sur l’axe des x et de 20 sur l’axe des y
  • On désactive la gravité sur cette bombe.

Vous pouvez tester votre jeu et vérifier l’apparition de bombes à chaque fois que vous ramassez toutes les étoiles.

VI.2 Collision avec le joueur et fin de partie

On ajoute une variable gameOver a notre programme principal, initialisée à false.

var gameOver = false; 

Dans la fonction update() si jamais gameOver passait à vrai, on arrête le jeu. On va écrire les instructions suivantes en tout début de fonction update(). Cela permettra de sortir de la boucle de jeu.

 if (gameOver) {
    return;
  } 

On crée ensuite une fonction chocAvecBombe() contenant les actions a effectuer si personnage référencé par player touche une des bombes de groupe_bombes, sur un modèle similaire à ce qui a été fait avec les stars. On ajoute la fonction suivante.

function chocAvecBombe(un_player, une_bombe) {
  this.physics.pause();
  player.setTint(0xff0000);
  player.anims.play("anim_face");
  gameOver = true;
} 

Cette fonction effectue les actions suivantes :

  • elle met le modèle physique en pause (tout se fige)
  • colore player en rouge (couleur rouge = 0xff0000)
  • lance l’animation anim_face pour que personnage du joueur soit en position immobile
  • met la variable booléenne gameOver a vrai.

Dans la fonction create(), on dit tout simplement d’exécuter la fonction chocAvecBombe si player intersecte avec groupe_bombes :

  this.physics.add.collider(player, groupe_bombes, chocAvecBombe, null, this); 

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

WC Captcha 8 × = 8