Tower Defense Three.JS

Créer un jeu de Tower Defense avec Three.JS – Partie 5 : Les ennemis

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 !

Three.JS Tower Defense Mobs
Les ennemis de notre jeu !

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.

Chemin des ennemis
Le chemin des ennemis

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 :

Three.JS Tower Defense mob path
Le chemin de nos ennemis

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 3D Mesh Three.JS de notre ennemi.
  • Les valeurs hp et speed 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 type mapPosition, 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 type Array contiendra nos instances de Mob.
  • 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 :

Three.JS Tower Defense mob path
Rappel : La structure du chemin

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 type Mesh qui sera copié pour définir l’apparence du nouvel ennemi créé.
  • scene représente notre instance Scene Three.JS.
  • Le dernier paramètre, mapdata, est une structure similaire à notre variable map0_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 :

  1. Nous avons défini, dans la variable map0_data, un chemin matérialisé par une suite de positions (étapes) sur la carte de jeu.
  2. Puis, nous avons créé deux classes : MobsManager et Mob.
  3. Ensuite, nous avons créé une méthode loadPathTargets de MobsManager capable de charger les données relatives aux étapes du chemin emprunté par les ennemis.
  4. Puis, les méthodes getStepPosition et getNextStep de MobsManager.
  5. Pour finir, nous avons créé createMob et deleteMobs, pour respectivement créer et supprimer des ennemis de la carte de jeu depuis la classe MobsManager.

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 :

  1. delta – Le temps écoulé depuis le dernier appel de la boucle principale render.
  2. mapdata – La structure de données de la carte de jeu, similaire à notre variable map0_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 :

  1. delta – Le temps écoulé depuis le dernier appel de la boucle principale render.
  2. mapdata – La structure de données de la carte de jeu, similaire à notre variable map0_data.
  3. 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 :

Les ennemis de notre jeu !

A suivre !