Dans ce nouveau chapitre de notre projet Tower Defense, nous allons inclure les ennemis dans la carte de jeu !
Vous êtes actuellement dans la cinquième partie de notre aventure. Pour commencer par le début du projet, rendez-vous sur l’article ci-dessous :
Cette partie risque d’être plus dense et intense que les précédentes, mais si vous êtes prêts pour cette cinquième partie, commençons !
Objectif du chapitre
Dans le chapitre précédent, nous avions implémenté les fonctionnalités de création et suppression de tours dans la carte de jeu. Dans cette nouvelle partie, nous y ajouterons les ennemis !
Ajouter des ennemis dans notre carte de jeu
Introduction
Dans un jeu de type Tower Defense, les ennemis suivent un parcours à travers la carte de jeu. Dans notre cas, ils devrons suivre le chemin que nous avions créé dans les chapitres précédents.
Le modèle 3D de nos ennemis
Commençons par déclarer mob_mesh
, une variable globale dans notre fichier index.html
:
var mob_mesh = undefined; // ThreeJS Mesh - MOB
Puis, dans notre fonction init
, initialisons cette variable avec un Mesh
Three.JS. Vous pouvez également charger un modèle 3D externe de votre choix !
// MOB MESH const mob_material = new THREE.MeshLambertMaterial({ color : 0x16a085}); const mob_geometry = new THREE.BoxGeometry( 0.5, 0.5, 0.5 ); mob_mesh = new THREE.Mesh( mob_geometry, mob_material ); mob_mesh.position.y = 0.75;
Comme d’habitude, si vous choisissez l’option de charger un modèle 3D externe, vous pouvez vous aider de ce lien :
Chaque nouvel ennemi créé et ajouté dans la scène sera un clone du Mesh
contenu dans notre variable mob_mesh
.
Le chemin des ennemis
Commençons par ouvrir le fichier map.js
, et modifions notre variable map0_data
. Nous allons y créer un nouvel index mobpath
qui stockera un double Array
similaire à celui de l’index data
:
export var map0_data = { "data" : [ [2, 1, 0, 0, 0, 0, 0, 0, 0, 2], [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], [2, 0, 0, 0, 0, 0, 0, 0, 1, 2] ], "mobpath" : [ [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 2, 0, 0, 0, 0, 0, 3, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 5, 0, 0, 0, 0, 4, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 6, 0, 0, 0, 0, 0, 7, 0], [0, 0, 0, 0, 0, 0, 0, 0, 8, 0] ] };
Cette nouvelle structure est principalement composée de cases ayantla valeur 0
. Par contre, le chemin des ennemis est matérialisé par la valeur des autres cases, triées par ordre croissant :
Ainsi, les ennemis suivrons le parcours défini en passant par chaque étape dans l’ordre croissant, nous chargerons ces informations plus tard !
La gestion des ennemis
Commençons par créer un fichier mobsmanager.js
, puis créons-y mapPosition
, une simple classe utilisée pour stocker une position sur les axes X
et Z
.
class mapPosition { constructor(newx, newz) { this.x = newx; this.z = newz; } }
Création de la classe Mob
Puis, créons-y une classe Mob
. Cette classe sera destinée à stocker toutes les informations d’un ennemi. Pour le moment, créons uniquement le constructeur et les propriétés de classe :
export class Mob { constructor() { this.mesh = undefined; this.hp = 10; this.speed = 2; this.currentStep = 1; this.target = undefined; // instance of mapPosition this.readyForNextStep = false; } }
- La propriété
mesh
contiendra l’objet 3DMesh
Three.JS de notre ennemi. - Les valeurs
hp
etspeed
représentent respectivement les points de vie et la vitesse de l’ennemi. currentStep
représente l’étape étape actuelle de l’ennemi sur le parcours.- L’élément
target
, de typemapPosition
, stocke la prochaine étape du parcours, actuellement ciblée par l’ennemi. Comme expliqué, chaque ennemi enchaînera les étapes du parcours les unes après les autres. - Le booléen
readyForNextStep
nous servira à déterminer lorsque l’ennemi à atteint sa cible – Et en demande une nouvelle.
Création de la classe MobsManager
Lorsque c’est fait, créons une classe MobsManager
:
export class MobsManager { constructor() { this.mobArray = new Array(); this.pathTargets = new Array(); } }
Cette classe sera utilisée pour gérer une liste d’ennemis de type Mob
. Voici quelques précisions sur le constructeur :
- La liste
mobArray
de typeArray
contiendra nos instances deMob
. - L’
Array
pathTargets
stocke la liste des différentes étapes parcourues par les ennemis sur leur chemin.
La méthode loadPathTargets
de MobsManager
– Charger les étapes du chemin
Puis, créons une méthode loadPathTargets
utilisée pour charger les données de la carte relatives au chemin des ennemis.
La variable map0_data
contenant les données qui nous intéressent sera utilisée en paramètre de cette méthode.
loadPathTargets(mapdata) { var size = mapdata.mobpath.length; this.pathTargets[0] = undefined; for(var x = 0 ; x < size ; x++) { for(var y = 0 ; y < size ; y++) { if(mapdata.mobpath[y][x] != 0) { this.pathTargets[ mapdata.mobpath[y][x] ] = new mapPosition(x, y); } } } }
Le code de cette méthode parcours les données de la carte à l’index mobpath
. Chaque case de la structure est analysée, si la valeur n’est pas 0
, cette case représente une étape du chemin – Nous l’ajoutons dans pathTargets
.
Pour rappel, voici à quoi ressemble la structure du chemin emprunté par les ennemis :
La méthode getStepPosition
de MobsManager
Cette méthode basique permet d’obtenir la position d’une case dans le tableau mobpath
depuis un numéro d’étape du parcours :
getStepPosition(stepid) { return this.pathTargets[stepid]; }
L’objet retourné sera de type mapPosition
. Et contiendra la position de l’étape sur deux valeurs : x
et z
.
La méthode getNextStep
de MobsManager
Cette méthode utilise getStepPosition
pour retourner la position de l’étape suivante. S’il n’y en a pas, la valeur renvoyée sera undefined
.
getNextStep(currentstep) { var newstep = currentstep + 1; if( this.pathTargets[newstep] ) { return this.getStepPosition(newstep); } else { return undefined; } }
La méthode createMob
de MobsManager
Cette méthode sera chargée de créer les ennemis de notre jeu. Ces derniers seront des instances de Mob
et seront stockés dans la liste mobArray
de notre classe MobsManager
.
Commençons par créer une méthode createMob
qui nécessite trois paramètres :
createMob(basemesh, scene, mapdata) { //code }
- Le paramètre
basemesh
représente le modèle 3D de typeMesh
qui sera copié pour définir l’apparence du nouvel ennemi créé. scene
représente notre instanceScene
Three.JS.- Le dernier paramètre,
mapdata
, est une structure similaire à notre variablemap0_data
.
Puis, dans le corps de cette nouvelle méthode, créons une variable temporaire de type Mob
.
var tmpmob = new Mob();
Lorsque c’est fait, définissons son apparence par un clone du paramètre basemesh
, et définissons sa position (au début du chemin) grâce à la valeur retour de la méthode getStepPosition
.
tmpmob.mesh = basemesh.clone(); // base position var baseposition = this.getStepPosition(tmpmob.currentStep); var size = mapdata.mobpath.length; tmpmob.mesh.position.x = (baseposition.x*2) - (size/2)*2; // position x tmpmob.mesh.position.z = (baseposition.z*2) - (size/2)*2; // position z
Puis, définissons sa prochaine cible – La prochaine étape du chemin qu’il doit atteindre :
tmpmob.target = this.getNextStep(tmpmob.currentStep);
Enfin, ajoutons ce nouvel ennemi à la scène et dans la liste mobArray
:
this.mobArray.push(tmpmob); scene.add(tmpmob.mesh);
Nous avons terminé le code de createMob
. Cette méthode nous servira prochainement pour ajouter des ennemis sur la carte de jeu.
La méthode deleteMobs
de MobsManager
À présent, nous devons être capable de retirer des ennemis de la carte de jeu. Créons une méthode deleteMobs
dans la classe MobsManager
.
Cette méthode doit être capable d’utiliser une liste d’objets Mob
pour les retirer de la liste mobArray
et de la scène :
deleteMobs(mobstodelete_array, scene) { for(var i = 0 ; i < mobstodelete_array.length ; i++) { const index = this.mobArray.indexOf(mobstodelete_array[i]); if (index > -1) { this.mobArray.splice(index, 1); } scene.remove(mobstodelete_array[i].mesh); } }
Déplacer les ennemis sur le chemin en fonction du temps
Introduction et point d’avancement du projet
Commençons par faire un point rapide sur l’avancement du chapitre :
- Nous avons défini, dans la variable
map0_data
, un chemin matérialisé par une suite de positions (étapes) sur la carte de jeu. - Puis, nous avons créé deux classes :
MobsManager
etMob
. - Ensuite, nous avons créé une méthode
loadPathTargets
deMobsManager
capable de charger les données relatives aux étapes du chemin emprunté par les ennemis. - Puis, les méthodes
getStepPosition
etgetNextStep
deMobsManager
. - Pour finir, nous avons créé
createMob
etdeleteMobs
, pour respectivement créer et supprimer des ennemis de la carte de jeu depuis la classeMobsManager
.
Nous entrons dans la dernière partie de ce chapitre, mettre en mouvement les ennemis et gérer leur apparition et disparition de la scène.
La méthode updatePosition
de la classe Mob
Commençons par créer une méthode updatePosition
pour la classe Mob
. Cette méthode sera utilisée pour mettre à jour la position de chaque instance de Mob
en fonction du temps.
export class Mob { constructor() { this.mesh = undefined; this.hp = 10; this.speed = 2; this.currentStep = 1; this.target = undefined; // instance of mapPosition this.readyForNextStep = false; } updatePosition(delta, mapdata) { //code } }
La méthode updatePosition
nécessite deux paramètres :
delta
– Le temps écoulé depuis le dernier appel de la boucle principalerender
.mapdata
– La structure de données de la carte de jeu, similaire à notre variablemap0_data
.
Lorsque c’est fait, vérifions que la propriété target
est définie :
if(!this.target) //bad target { return; }
Puis, définissons quelques variables :
var is_X_ok = false; var is_Z_ok = false; var size = mapdata.mobpath.length; var convertedpositiontarget_x = (this.target.x*2) - (size/2)*2; // position x var convertedpositiontarget_z = (this.target.z*2) - (size/2)*2;
Les variables booléennes is_X_ok
et is_Z_ok
, passerons sur true
si la valeur de position de l’ennemi Mob
est égale a celle de sa cible target
respectivement sur les axes X
et Z
.
Les variables convertedpositiontarget_x
et convertedpositiontarget_z
représentent la position de la cible target dans le référentiel de notre scène 3D. Sans cette conversion, target stocke la position absolue de la cible issue de la structure map0_data
.
Lorsque c’est fait, nous pouvons utiliser ces variables pour déplacer notre ennemi Mob
sur les axes X
et Z
!
// --------- Z AXIS ----------- if(this.mesh.position.z < convertedpositiontarget_z) { this.mesh.position.z += this.speed * delta; if(this.mesh.position.z > convertedpositiontarget_z) { this.mesh.position.z = convertedpositiontarget_z; } }
Sur l’axe Z
, si la valeur de la position de l’ennemi Mob
est inférieure à la position de convertedpositiontarget_z
, nous augmentons cette valeur !
Dans le cas contraire, si la valeur de la position de l’ennemi Mob
est inférieure à la position de convertedpositiontarget_z
, nous faisons l’opération inverse :
else if(this.mesh.position.z > convertedpositiontarget_z) { this.mesh.position.z -= this.speed * delta; if(this.mesh.position.z < convertedpositiontarget_z) { this.mesh.position.z = convertedpositiontarget_z; } }
Mais, si la position de l’ennemi Mob
est égale à celle de sa cible sur l’axe Z
, nous définissons la valeur de is_Z_ok
sur true
:
else if(this.mesh.position.z == convertedpositiontarget_z) { // THE SAME VALUE is_Z_ok = true; }
Il ne nous reste plus qu’a copier ce code et l’adapter pour l’axe X
:
// --------- X AXIS ----------- if(this.mesh.position.x < convertedpositiontarget_x) { this.mesh.position.x += this.speed * delta; if(this.mesh.position.x > convertedpositiontarget_x) { this.mesh.position.x = convertedpositiontarget_x; } } else if(this.mesh.position.x > convertedpositiontarget_x) { this.mesh.position.x -= this.speed * delta; if(this.mesh.position.x < convertedpositiontarget_x) { this.mesh.position.x = convertedpositiontarget_x; } } else if(this.mesh.position.x == convertedpositiontarget_x) { // THE SAME is_X_ok = true; }
Pour finir, dans le cas ou les variables is_Z_ok
et is_X_ok
sont égales à true
, nous définissons la propriété de classe readyForNextStep
sur true
.
Concrètement, cela signifie que l’ennemi à atteint la position de sa target
. Ce dernier est prêt pour avoir une nouvelle cible.
if(is_X_ok && is_Z_ok) { this.readyForNextStep = true; }
La méthode updatePosition
est désormais terminée, nous l’utiliserons pour mettre à jour la position des ennemis dans notre carte de jeu.
La méthode updateMobsPosition
de la classe MobsManager
La classe Mob
est terminée, continuons les modifications de MobsManager
. Créons-y une méthode updateMobsPosition
:
updateMobsPosition(delta, mapdata, scene) { //code }
Cette méthode nécessite trois arguments :
delta
– Le temps écoulé depuis le dernier appel de la boucle principalerender
.mapdata
– La structure de données de la carte de jeu, similaire à notre variablemap0_data
.- Notre scène Three.JS.
Commençons par créer une variable mobstodelete
de type Array
:
var mobstodelete = new Array();
Nous utiliserons cette variable pour stocker les ennemis à retirer de la carte de jeu.
Puis, créons une boucle for
pour parcourir chaque instance de Mob
de la liste mobArray
:
for(var i = 0 ; i < this.mobArray.length ; i++) { //code }
Dans le corps de cette boucle, vérifions si le Mob couramment analysé est prêt pour avoir une nouvelle cible. Pour cela, nous utilisons la propriété readyForNextStep
.
Si c’est le cas, nous lui donnons une nouvelle cible grâce a la méthode getNextStep
. Cependant, si la nouvelle cible n’est pas valide, cela signifie que l’ennemi Mob est arrivé à la fin du parcours – Dans ce cas, nous l’ajoutons à la liste mobstodelete
.
if (this.mobArray[i].readyForNextStep) // if we need a new step { var mobtarget = this.getNextStep(this.mobArray[i].currentStep); this.mobArray[i].currentStep++; this.mobArray[i].target = mobtarget; this.mobArray[i].readyForNextStep = false; if (!this.mobArray[i].target) // if invalid target, we delete this mob - end of path or invalid { mobstodelete.push(this.mobArray[i]); } }
Lorsque c’est fait, utilisons la méthode updatePosition
pour faire avancer le Mob
actuel sur la carte de jeu.
Toujours dans la boucle for
:
this.mobArray[i].updatePosition(delta, mapdata);
La boucle for
est désormais terminée ! Juste après cette dernière, utilisons la méthode deleteMobs
pour supprimer les ennemis arrivés à la fin du parcours :
this.deleteMobs(mobstodelete, scene);
Notre méthode updateMobsPosition
est terminée, voici un aperçu de son code complet :
updateMobsPosition(delta, mapdata, scene) { var mobstodelete = new Array(); for(var i = 0 ; i < this.mobArray.length ; i++) { if (this.mobArray[i].readyForNextStep) // if we need a new step { var mobtarget = this.getNextStep(this.mobArray[i].currentStep); this.mobArray[i].currentStep++; this.mobArray[i].target = mobtarget; this.mobArray[i].readyForNextStep = false; if (!this.mobArray[i].target) // if invalid target, we delete this mob - end of path or invalid { mobstodelete.push(this.mobArray[i]); } } this.mobArray[i].updatePosition(delta, mapdata); } this.deleteMobs(mobstodelete, scene); }
Finalisation du chapitre – Utilisation de MobsManager
Le module mobsmanager.js
est terminé, utilisons le dans index.html
! Commençons par importer ce nouveau module :
import {MobsManager } from './mobsmanager.js'
Puis, créons une variable globale mobsMngr
de type MobsManager
, et utilisons la méthode loadPathTargets
pour charger les données relatives au chemin des ennemis :
var mobsMngr = new MobsManager(); mobsMngr.loadPathTargets(map0_data);
Lorsque notre instance de MobsManager
est prête, ajoutons l’appel de la méthode updateMobsPosition
dans la boucle principale render
de notre code :
function render() { var delta = clock.getDelta(); [...] mobsMngr.updateMobsPosition(delta, map0_data, scene); }
Il est maintenant possible d’ajouter des ennemis sur la carte de jeu avec la méthode createMob
. Dans notre cas, nous utiliserons la fonction JavaScript setInterval
pour créer des ennemis à intervalle régulier :
setInterval( function(){ mobsMngr.createMob(mob_mesh, scene, map0_data); }, 3000);
N’oubliez pas d’attendre la fin du chargement de tous les éléments de notre univers Three.JS avant d’appeler ce code.
Code et résultat final
Félicitations, nous avons réussi à inclure des ennemis dans notre carte de jeu ! Voici un aperçu du résultat final :