Rajouter une fonction de tir, des balles et des cibles

Dans ce tutoriel, nous montrons comment faire tirer des balles à un personnage dans un jeu développé en Phaser 3. Nous regardons notamment comment gérer efficacement les balles et leur impact sur des cibles. Ce tutoriel est volontairement simple et n’intègre que des fonctions réduites (pas d’animation de tir, d’animation d’impact via des particules …) mais rien qui ne peut être ajouté par vos soin en suivant d’autres tutoriels.

Ce que contient ce tutoriel :  

  • Gestion d’une balle : direction, vitesse
  • Création de cibles avec points de vie.
  • Impact de la balle sur la cible, diminution des points de vie et disparition.
Ce que NE contient PAS ce tutoriel :
  • Animations spécifiques pour le tir
  • Impact de balles, explosions via des particules
  • Gestion avancée d’une arme (animations, recharge, gestion des munitions) 

Le résultat est un jeu dans lequel un personnage peut tirer sur des cibles. Lorsqu’une cible est touchée, elle perd un point de vie. Lorsque ses points de vie atteignent 0, elle disparait. Point important ; les balles sont détruites si elles sortent de l’écran.

Une cible nécessites plusieurs tirs pour être détruites . Les commande de jeu sont les suivantes  :

  • pavé directionnel : déplacement du joueur
  • touche A : tir

Base de départ pour ce tutoriel

Nous partons d’un jeu dans lequel un personnage est présent sur un monde de taille fixe avec un fond, un sol et 3 plates-forme. Ce personnage peut aller à gauche, à droite, et sauter avec la flèche du haut s’il touche le sol (voir Figure 1). Ce code le résultat du tutoriel “making your first phaser 3 game” :

https://phaser.io/tutorials/making-your-first-phaser-3-game  

Ce code est disponible dans codesandbox via le lien suivant :

https://codesandbox.io/s/phaser-plateforme-simple-26hbt?fontsize=14&hidenavigation=1&theme=dark

Figure 1 : un personnage pouvant bouger dans un monde simple

Par la suite, nous pourrons supprimer les plate-formes pour les remplacer par une carte générée par Tiled, mais garderons les mouvements et animations du personnage.

Création d'une balle et tir dans une direction donnée

La création d’une balle se fait en plusieurs étapes préliminaires : nous allons tout d’abord déterminer le paramètre de direction, puis nous définissons une touche de tir (la gâchette), nous créons enfin une balle et lançons cette dernière dès que la gâchette est pressée.

Définition et mise à jour d'un attribut direction du joueur

Avant de créer une munition, il  nous faut savoir dans quelle direction tirer cette munition :  ceci soulève deux sous-questions triviales qu’il faut anticiper pour éviter tout soucis :

  1. Comment stocker et détecter les changements de direction du joueur. 
  2. dans quelle direction tirer au départ si le joueur fait directement feu sans avoir commencé à bouger son personnage dans une direction ou l’autre.

Dans le code de départ, notre personnage est désigné par la variable player, qui est un objet. Nous allons tout d’abord ajouter un attribut  direction à player pour stocker la direction de tir. Nous choisissons d’initialiser cette variable avec la valeur ‘right’ (droite), qui sera la direction de tir par défaut en début de jeu. 

Dans javascript, on peut ajouter des attributs aux objets à la volée, simplement en les appelant sur des objets déjà créés. Ainsi dans la fonction create(), après avoir défini player, nous ajoutons la ligne suivante : 

// creation d'un attribut direction pour le joueur, initialisée avec 'right'
player.direction = 'right';  

Il nous faut ensuite mettre à jour cet attribut à chaque fois que le joueur se déplace. Nous modifions les commandes de déplacement dans la fonction update() en ajoutant des instructions permettant de mettre à jour la valeur de direction selon le déplacement du joueur.

Dans le code on va modifier le code ci-dessous en ajoutant les deux lignes de mise à jour de player.direction player.direction = ‘left’; et player.direction = ‘right’; de sorte à obtenir le code suivant :

 if (cursors.left.isDown) {
    // enregistrement de la direction : gauche
    player.direction = 'left';
    player.setVelocityX(-160);
    player.anims.play('left', true);
}
else if (cursors.right.isDown) {
    // enregistrement de la direction : droite
    player.direction = 'right';
    player.setVelocityX(160);
    player.anims.play('right', true);
}  

Désormais, la valeur de direction est mise à jour en fonction du déplacement du joueur.

Mise en place de la gachette

Il est temps de mettre en place un déclencheur, c’est à dire une touche à presser pour tirer. Pour simplifier le développement, nous créons, hors de toute fonction, une variable boutonFeu qui désignera le bouton de feu :

// mise en place d'une variable boutonFeu
var boutonFeu;  

Nous associons ensuite la touche ‘A’ à ce bouton : au sein de la fonction create(), on ajoute la ligne suivante juste après avoir initialisé la variable cursor , c’est à dire à la suite de  l’instruction  cursors = this.input.keyboard.createCursorKeys() ;  :

// création du clavier - code déja présent sur le jeu de départ
cursors = this.input.keyboard.createCursorKeys();

// affectation de la touche A à boutonFeu
boutonFeu = this.input.keyboard.addKey('A'); 

Désormais, quand on appuie sur ‘A’, on active boutonFeu. L’étape suivante consiste à associer une action à boutonFeu. Dans la fonction update(), on peut associer une fonction à exécuter quand boutonFeu est pressé. Il y a un point important à noter : Où devra partir la balle et dans quelle direction ? Il faudra la faire partir à coté du joueur et dans la direction donnée par le joueur. Les coordonnées du joueur seront données par les attributs x et y du joueur, et la direction par l’attribut  direction que l’on a créé précédemment.

On va d’abord définir une fonction tirer() qui va prendre en paramètre le joueur qui tire, ce qui nous permettra de récupérer ses coordonnées x et y et la direction dans laquelle il regarde. Cette fonction se déclenchera lorsque la touche ‘A‘ est pressée. Dans un premier temps cette fonction  se contentera d’afficher une alerte à l’écran pour vérifier son exécution  et affichera les valeurs des attributs cités. L’objectif est simplement d’illustrer le déclenchement de la touche de tir, et que les paramètres nécessaires sont connus. Par la suite nous modifierons la fonction.  Hors de toute fonction, nous ajoutons les instructions suivantes :

//fonction tirer( ), prenant comme paramètre l'auteur du tir
function tirer(player) {
    // mesasge d'alerte affichant les attributs de player
	alert ("joueur en position"+player.x + ","+player.y + ", direction du tir: "
	+ player.direction) ; 
}  

n remplacera l’instruction  alert(…) par autre chose plus tard.

Il ne reste plus qu’à associer  cette fonction à la touche ‘A’ (associée à boutonFeu) pour l’exécuter lorsque l’on appuie sur cette dernière. Dans la fonction update() nous ajoutons les instructions suivantes :

// déclenchement de la fonction tirer() si appui sur boutonFeu 
if ( Phaser.Input.Keyboard.JustDown(boutonFeu)) {
   tirer(player);
}  

Lancez le jeu et tirez en appuyant sur ‘A’. Une fenêtre popup apparaît en affichant coordonnées et la direction du joueur (Figure 2). Testez plusieurs fois pour constater que les valeur sont bien mises à jour à chaque appui et la valeur de direction correcte.

Figure 2 : lorsque le tir est déclenché, la popup indique les coordonnées et la direction du joueur

Chargement des images pour les balles

De manière analogue à ce qui a pu être fait précédemment, nous ajoutons dans le répertoire assets un image de balle que nous utilisons dans notre jeu. L’image utilisée dans ce tutoriel (Figure 3) est disponible via ce lien : balle.png

Figure 3 - sprite d'une balle

Par soucis de simplicité, nous prenons une balle ronde. il n’y aura pas besoin de tourner l’image selon que l’on tire vers droite ou vers la gauche. Dans la fonction preload() nous ajoutons ensuite l’instruction suivante :

// chargement de l'image balle.png
 this.load.image("bullet", "assets/balle.png");  

le mot-clé bullet désigne désormais l’image balle.png.

Gestion des balises : création et lancement

Pour gérer efficacement les balles, nous allons les grouper via un groupe, appelé groupeBullets, à partir duquel chaque balle va être créée. Utiliser un groupe offre une meilleure gestion les collisions avec d’autres éléments.

// mise en place d'une variable groupeBullets
var groupeBullets;  

Dans la fonction create(), on ajoute l’instruction suivante permettant de créer un groupe vide  :

// création d'un groupe d'éléments vide
groupeBullets = this.physics.add.group();  

Tout est prêt. Il ne nous reste plus qu’à créer une balle lorsque la touche ‘A’ est pressée.

Pour créer une balle. Il nous faut lui apporter les coordonnées de départ, et sa vitesse. Ces deux éléments vont dépendre du sens du tir (gauche ou droite) qui est contenu dans le paramètre direction. Nous modifions la fonction  tirer() en remplaçant ses instructions par les instructions ci-après  dans lesquelles on a les instructions suivantes :

  • Nous allons utiliser une variable locale  coefDir qui sera a 1 si la direction tirée est à droite, et à -1 sinon. Cela permet d’avoir une symétrie aussi bien dans le positionnement de départ de la balle, que dans le vecteur vitesse.
  • Nous allons faire partir la balle à 25 pixels à gauche ou a droite du centre du joueur , et 4 pixels au dessus. Ces coordonnées sont calculées depuis la position du joueur et la valeur de coefDif.
  • la balle créée s’appelle bullet dans ces instructions et se crée depuis le groupe groupeBullets
  • L’instruction bullet.setCollideWorldBounds(true); permet de faire cogner la balle aux bords du monde. Ceci nous permet de voir les impacts de balle sur les bords de l’écran.
  • L’instruction bullet.body.allowGravity =false; annule la gravité sur la balle.
  • L’instruction bullet.setVelocity(1000 * coefDir, 0); définit la vitesse de la balle . Grace à coefDir, cette vitesse sera positive (déplacement de la gauche vers la droite) ou négative (direction inverse). 
function tirer(player) {
        var coefDir;
	    if (player.direction == 'left') { coefDir = -1; } else { coefDir = 1 }
        // on crée la balle a coté du joueur
        var bullet = groupeBullets.create(player.x + (25 * coefDir), player.y - 4, 'bullet');
        // parametres physiques de la balle.
        bullet.setCollideWorldBounds(true);
        bullet.body.allowGravity =false;
        bullet.setVelocity(1000 * coefDir, 0); // vitesse en x et en y
}  

Nous enregistrons et testons ce ce jeu (Figure 4) :

  • Le personnage peut toujours se déplacer librement.
  • lorsque l’on tire avec ‘A’ une balle est tirée, dans la direction dans laquelle regarde le personnage.
  • Du fait de la collision avec les bords du monde, on note la présence d’impacts de balles sur les bords de la fenêtre.

Le dernier point pourra paraître peu esthétique,  On envisagera  par la suite une destruction de ces balles par détection de collision avec le bord du monde.

Figure 4 : on peut tirer des balles

La première grosse partie est terminée, nous ajoutons ensuite des cibles pour découvrir et appréhender les mécanismes d’impacts balle / cible.

Ajout de cibles aléatoires simples

L’ajout de cibles est très proches de l’ajout d’étoile, traité précédemment dans un autre tutoriel, et suit le même parcours : chargement des images, positionnement des cibles, et ajout d’une fonction pour gérer l’impact d’une balle sur une cible. Ces différentes étapes sont présentées dans cette section.

Création des cibles, chargement des assets

De manière analogue à ce qui a pu être fait précédemment, nous ajoutons tout d’abord dans le répertoire assets une image de cible que nous utiliserons dans notre jeu. L’image utilisée dans ce tutoriel (Figure 5) est disponible via ce lien : cible.png

Figure 5 - sprite d'une cible

Dans la fonction preload() nous ajoutons ensuite l’instruction suivante :

// chargement de l'image cible.png
 this.load.image("cible", "assets/cible.png");  

e mot-clé cible désigne désormais l’image cible.png.

Pour des raisons d’organisation , on groupe les cibles créées dans un groupe d’éléments nommé groupeCibles. Hors de toute fonction, on ajoute cette variable à la suite de la déclaration des autres variables :

// mise en place d'une variable groupeCibles
var groupeCibles;  

On crée ensuite ce groupe et on y ajoute dans la foulée 8 cibles réparties sur l’axe des x : coordonnées de la première cible à (24, 0), puis espacement horizontal tous les 107 pixels.

Dans la fonction create(), on ajoute les instructions suivantes pour créer ces cibles (à ajouter de préférence avant la déclaration de player, si vous voulez que votre personnage ne se retrouve pas caché derrière les cibles lorsqu’il se déplace sur l’une d’entre elles) :

// ajout de 8 cibles espacées de 110 pixels
 cibles = this.physics.add.group({
            key: 'cible',
            repeat: 7,
            setXY: { x: 24, y: 0, stepX: 107 }
        });  

On complète toujours la fonction create() en ajoutant une collision entre les cibles et les plate-formes, avec l’instruction suivante, à placer après la définition des cibles et des plate-formes  :

       // ajout du modèle de collision entre cibles et plate-formes
        this.physics.add.collider(cibles, platforms);  

A ce stade (Figure 6), les cibles sont affichées sur notre plateau de jeu : après être apparues en haut de l’écran, elles tombent simultanément et s’arrêtent sur la première plate-forme rencontrée. Le personnage peut passer devant ces cibles sans les percuter.

Un tir sur ces dernières reste inopérant pour l’instant, la balle se contentera de traverser la cible.

Dans la section suivante, nous ajoutons simplement la destruction d’une cible dès qu’elle se superpose à une balle.

Figure 6 : les cibles sont apparentes

Détruire les cibles, ou la superposition balle-cible

Pour gérer l’impact cible – balle, il nous suffit, à chaque superposition entre un élément du groupe cibles et un élément du groupe balle, de déclencher une fonction de callback prenant ces deux paramètres – la balle et la cible qui se superposent-. Il nous suffira alors de détruire les deux objets passés en paramètres.

La superposition (overlap) est très similaire à la collision (collider) dans sa définition. a ceci près qu’il n’y a pas d’échange d’énergie ici entre les deux entités qui se touchent.

En dehors de toute fonction, rajoutons d’abord la fonction hit(uneBalle, uneCible) qui va être la fonction qui se déclenchera lorsque la balle uneBalle se superposera à la cible uneCible). Cette fonction se contentera de détruire les deux paramètres cités :

// fonction déclenchée lorsque uneBalle et uneCible se superposent
function hit (uneBalle, uneCible) {
    uneBalle.destroy(); // destruction de la balle
    uneCible.destroy();  // destruction de la cible.   
}  

Ajoutons enfin l’instruction indiquant que la fonction hit() doit être appelée lorsqu’une balle «touche » une cible (ou plutôt que les deux se recouvrent).

Dans la fonction create(), après les lignes de déclaration de groupeBullets et cibles, on ajoute : 

this.physics.add.overlap(groupeBullets, cibles, hit, null,this);
  

Testez votre jeu. Les cibles disparaissent au fur et à mesure qu’elles sont touchées. Mission accomplie : nous avons les instructions minimales pour gérer des tirs et détruire des cibles.

Pour aller un peu plus loin :points de vie et balles perdues

Un peu de difficulté : rebonds et points de vie sur les cibles

Le jeu est trop facile ? ajoutons un peu de difficulté :

  • Pour chaque cible générée : nous allons ajouter un attribut pointsVie, entre 1 et 5. Chaque tir réussit diminuera la vie de la cible de 1. La cible ne disparaitra que si ses points de vie sont à 0. 
  • Rendons les cibles mouvantes : simplement nous ajoutons un coefficient de rebond de 1 à chaque cible. 
  • Pour ajouter un peu plus de chaos, les cibles ne tombent plus du ciel depuis le même endroit, mais la coordonnée y est tirée aléatoirement entre 0 et 250. 

Une fois les cibles créées dans create(), on parcourt les éléments de cible, et pour chaque élément cibleTrouvee du groupe cibles , on applique les modifications précédemment exposées. Nous ajoutons les instructions suivantes  à la suite de la création des cibles.

// modification des cibles créées
cibles.children.iterate(function (cibleTrouvee) {
   // définition de points de vie
   cibleTrouvee.pointsVie=Phaser.Math.Between(1, 5);;
   // modification de la position en y
   cibleTrouvee.y = Phaser.Math.Between(10,250);
   // modification du coefficient de rebond
   cibleTrouvee.setBounce(1);
});  

On met enfin à jour la fonction hit() pour ne détruire la cible que si ses points de vie sont à 0, et les diminuer autrement. on remplace ses instructions par les instructions suivantes :

function hit (bullet, cible) {
  cible.pointsVie--;
  if (cible.pointsVie==0) {
    cible.destroy(); 
  } 
   bullet.destroy();
}  

Les cibles sont désormais mouvantes et nécessitent plusieurs tirs pour être détruites. 

Détruire les balles qui sortent de la zone du monde

Cette partie est la moins évidente, car les mécanismes diffèrent un peu. Précédemment on avait écrit dans la fonction tirer(),  lors de la création d’une balle, l’instruction suivante : bullet.setCollideWorldBounds(true);

Il nous suffirait de supprimer cette instruction pour que la balle tirée ne se cogne plus aux bords du monde.  Oui mais voila, elle ne “disparaitrai” pas pour autant, elle continuerait son existence, hors du monde, et ne ferait à terme qu’alourdir le programme en consommant de la ressource.

Pour avoir un code propre et efficace, il est nécessaire de bien détruire la balle lorsque cette dernière touche les bords du monde. Les mécanismes de “collision” avec les bords du monde sont particuliers et suivent la logique suivante. D’abord, on annonce les objets dont on doit surveiller s’ils sortent du monde ou pas. Dans la fonction tirer(), apres la ligne bullet.setCollideWorldBounds(true); on ajoute l’instruction suivante :

// on acive la détection de l'evenement "collision au bornes"
        bullet.body.onWorldBounds = true;  

Enfin, dans la fonction create(), on ajoute les instructions à effectuer lorsqu’un objet surveillé se situe aux bornes du monde.  Dans le code qui suit :

  • on récupère l’objet surveillé
  • on verifie s’il appartient à un groupe de balles
  • si oui : il s’agit d’une balle, on le détruit
// instructions pour les objets surveillés en bord de monde
this.physics.world.on("worldbounds", function(body) {
        // on récupère l'objet surveillé
        var objet = body.gameObject;
        // s'il s'agit d'une balle
        if (groupeBullets.contains(objet)) {
            // on le détruit
            objet.destroy();
        }
    });
}  

Les plus observateurs l’auront remarqué : le second paramètre de la méthode on( ) est bizarre function(body) {…} : il s’agit d’une fonction! En javascript :

  • on peut passer des noms de fonctions externes en paramètre, comme on l’a fait avec collider(),
  • mais on peut passer également tout le contenu entier de la fonction à la place. Ceci permet de ne pas avoir à créer une fonction plus loin pour y faire référence avec son nom ailleurs, et permet parfois une factorisation du code. mais la notation est moins intuitive et peut être parfois déroutante.

il ne vous reste plus qu’à sauvegarder et admirer le résultat de votre jeu (Figure 7). 

Figure 7 - notre jeu est prêt : on peut détruire les cibles en tirant dessus, et les balles disparaissent si elles sortent de l'écran de jeu

Auteur : Benoit Darties

Laisser un commentaire

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

78 + = 81