Tower Defense Three.JS

Créer un jeu de Tower Defense avec Three.JS – Partie 4 : Création et suppression de tours

Ce chapitre est la quatrième partie de notre tutoriel de programmation de notre jeu de type Tower Defense avec Three.JS.

Tower Defense Three.JS
Notre jeu de Tower Defense

Si vous souhaitez commencer par le début, voici un lien vers la première partie :

Si vous êtes à jour, commençons !

Ce chapitre est également disponible au format vidéo !

Introduction et objectif

Dans la partie précédente, nous avions implémenté un système de curseur contrôlé par la souris ou l’écran tactile. Dans cette nouvelle partie, nous utiliserons ce curseur pour cibler la position de création des tours de notre jeu !

Création d'une tour
Création d’une tour avec le curseur – L’objectif de ce chapitre

Créer et supprimer des tours sur notre carte de jeu

Le modèle 3D de notre tour

Commençons par créer une variable globale tower_mesh dans notre fichier index.html:

var tower_mesh	= undefined;		// ThreeJS Mesh - TOWER

Puis, dans notre fonction init, créons un modèle 3D pour notre tour. Vous pouvez charger un modèle 3D d’un fichier externe, ou utiliser les primitives Three.JS comme dans cet exemple :

// TOWER MESH

const material = new THREE.MeshLambertMaterial({ color : 0xc0392b});
const tower_geometry = new THREE.BoxGeometry( 1, 3, 1 );
tower_mesh = new THREE.Mesh( tower_geometry, material );

Si vous choisissez l’option de charger un modèle 3D externe, vous pouvez utiliser ce lien pour vous aider :

Chaque nouvelle tour créée sera un clone du Mesh de notre variable tower_mesh.

La gestion des tours

Créons un fichier towermanager.js, puis créons-y TowerManager, une classe exportable :

export class TowerManager
{
    constructor()
    {
        //code
    }
}

Dans le constructeur de notre nouvelle classe, créons trois variables :

// ---- Tower List ----
this.towerArray = new Array();

// ---- Temporary variables ----
this.newTowerMeshToCreate = undefined;
this.selectedTower = undefined;

La variable towerArray constitue notre liste, dans laquelle les tours sont stockées.

Les deux autres variables sont des conteneurs temporaires que nous utiliserons lors d’événements de création d’une tour ou de sélection d’une tour existante.

Puis, créons une simple classe Tower:

class Tower
{
    constructor()
    {
        this.mesh = undefined;
    }
}

Cette classe est pour le moment simplement utilisée pour stocker une propriété mesh de type Mesh Three.JS.

Notre liste towerArray stockera des instances de cette classe Tower.

Lorsque c’est chose faite, continuons de créer notre classe TowerManager. Créons les méthodes addTower et deleteTower :

addTower(newtowermesh)
{
  var newtower = new Tower();
  newtower.mesh = newtowermesh;
  this.towerArray.push(newtower);
}

deleteTower(TowerObj)
{
    const index = this.towerArray.indexOf(TowerObj);
    if (index > -1) {
      this.towerArray.splice(index, 1);
    }
}

La méthode addTower nécessite un paramètre de type Mesh Three.JS. A partir de ce dernier, elle créera et ajoutera un objet Tower dans la liste towerArray.

La méthode deleteTower accepte un paramètre de type Tower, et le supprimera de la liste towerArray.

Pour finir, créons une méthode getTowerAtPosition, qui retourne l’objet Tower à la positions donnée :

getTowerAtPosition(x, z)
{
    for(var i = 0 ; i < this.towerArray.length ; i++ )
    {
        if(this.towerArray[i].mesh.position.x == x && this.towerArray[i].mesh.position.z == z )
        {
            return this.towerArray[i];
        }
    }
    return null;
}

Notre module est désormais terminé ! importons le dans index.html, et créons-y une variable globale de type TowerManager :

import {TowerManager } from './towermanager.js';

[...]

var towerMngr = new TowerManager();

L’interface graphique de notre jeu

Avant de commencer l’ajout et la suppression de tours, créons une interface graphique pour notre jeu.

Nous avons besoin de deux fenêtres pop-up, une pour chaque action. Voici un aperçu des interfaces que nous allons créer :

Gui Tower Defense
Notre GUI

Commençons par nous occuper de notre pop-up de création dans index.html :

<!-- CREATE MENU -->
<div  id="createTowerDiv" class="popupdiv">
  <h2 style="text-align : center;">Create Tower ?</h2>
  <div style="display:flex;  align-items: center; justify-content: center;">
      <button class="buttonYesNo buttonyes" id="buttonyes">Yes</button>
      <div style="width : 5%"></div>
      <button class="buttonYesNo buttonno" id="buttonno" >No</button>
  </div>
</div>

Puis, créons la pop-up de sélection. Cette interface prévoit deux balises span pour y afficher la position de la tour sélectionnée :

<!-- TOWER INFO MENU -->
<div  id="TowerInfoDiv" class="popupdiv">
  <h2 style="text-align : center;">Selected Tower Info</h2>
  <p>Position : <span id="posXinfo">NULL</span> / <span id="posZinfo" >NULL</span></p>
  </br>
  <div style="display:flex;  align-items: center; justify-content: center;">
      <button class="buttonYesNo buttonno" id="buttondelete" >Delete Tower</button>
      <div style="width : 5%"></div>
      <button class="buttonYesNo buttonyes" id="buttonclose">Close</button>
  </div>
</div>

Ensuite, occupons nous du CSS associé à ces balises :

.buttonYesNo
{
    width : 40%;
    height : 48px;

    font-size : 1.5em;
    border-radius : 6px;
    border : none;
}

.buttonyes
{
    background-color : #16a085;
    color : white;
}

.buttonno
{
    background-color : #c0392b;
    color : white;
}

.popupdiv
{
   display: none;
   opacity : 0.7;
   position : absolute;
   left : 5%;
   bottom : 5%;
   box-sizing: border-box;
   padding : 25px;
   width: 90%;
   color : white;
   font-family: roboto-font, sans-serif;
   background-color : black;
   border-radius : 6px;
}

Nos deux pop-up sont invisibles par défaut, grâce à la règle CSS display: none.

Ensuite, lorsque nos interfaces sont prêtes, créons un nouveau fichier gui.js. Dans ce module, produisons quatre méthodes exportables :

  • createTowerGui_open – Ouvrir l’interface de création.
  • createTowerGui_close – Fermer l’interface de création.
  • infoTowerGui_open – Ouvrir l’interface puis remplir les champs d’informations de la tour séléctionée.
  • infoTowerGui_close – Fermer l’interface d’informations de la tour sélectionnée.
export function createTowerGui_open()
{
    document.getElementById("createTowerDiv").style.display = "block";
}

export function createTowerGui_close()
{
    document.getElementById("createTowerDiv").style.display = "none";
}

export function infoTowerGui_open(tower_posx, tower_posz)
{
    document.getElementById("posXinfo").innerHTML = tower_posx;
    document.getElementById("posZinfo").innerHTML = tower_posz;

    document.getElementById("TowerInfoDiv").style.display = "block";
}

export function infoTowerGui_close()
{
    document.getElementById("TowerInfoDiv").style.display = "none";

    document.getElementById("posXinfo").innerHTML = "NULL";
    document.getElementById("posZinfo").innerHTML = "NULL";
}

Lorsque c’est chose faite, importons notre nouveau module dans index.html:

import {createTowerGui_open, createTowerGui_close , infoTowerGui_open, infoTowerGui_close} from './gui.js'

Ainsi, nos interfaces sont désormais prêtes. Continuons notre projet !

Réagir aux événements Raycaster

Dans le chapitre précédent, nous avions mis en place un curseur basé sur la classe Raycaster de Three.JS. Nous allons utiliser ce curseur pour effectuer des actions sur notre carte de jeu :

  • Si la case ciblée est vide, nous proposons de créer une tour.
  • Si la case est déjà occupée par une tour, nous affichons les informations de cette tour.

Commençons avec la fonction onMouseUp. Dans cette fonction, définissons sur undefined les deux variables temporaires de towerMngr ( notre instance de TowerManager) :

function onMouseUp(event)
{
	cursor_cube.material.emissive.g = 0;
	towerMngr.newTowerMeshToCreate = undefined;
	towerMngr.selectedTower = undefined;
}

Il est maintenant nécessaire de créer une variable globale cursorValid.

Puis, dans onMouseDown, lorsque un élément éligible au ciblage du Raycaster est sélectionné, nous définissons la valeur de cursorValid sur true. Sinon, nous définissons sa valeur sur false :

var cursorValid = false;

[...]

function onMouseDown(event)
{
      [...]
      if(intersects.length > 0)
      {
          [...]
          cursorValid = true;
      }
      else
      {
          [...]
          cursorValid = false;
      }
}

Désormais, nous allons utiliser cette variable dans onMouseUp. Si cette dernière est égale à true, nous vérifions si une tour est déjà créée sur la position de notre curseur :

if( cursorValid)
{
    var checkTower = towerMngr.getTowerAtPosition(cursor_cube.position.x, cursor_cube.position.z);
}

Puis, dans notre structure if, utilisons le résultat de la vérification stocké dans la variable checkTower. Deux cas sont possibles :

  • La valeur de checkTower est null – Aucune tour n’est actuellement créée sur la case, nous ouvrons l’interface de création.
  • La valeur de checkTower est différente de null – Une tour est déjà créée sur cette case, nous ouvrons l’interface d’informations de la tour.

Créer une tour

Ainsi, dans le premier cas, nous créons un clone de tower_mesh.

Puis, nous lui attribuons la position courante du curseur et nous stockons ce nouvel objet dans la variable temporaire newTowerMeshToCreate de towerMngr.

Pour finir, nous ouvrons et fermons les interfaces graphiques appropriées.

if(checkTower == null)
{
  	var newtower = tower_mesh.clone();
  	newtower.position.set(	cursor_cube.position.x, 1 , cursor_cube.position.z);
  	towerMngr.newTowerMeshToCreate = newtower;

  	infoTowerGui_close();
  	createTowerGui_open();
}

Désormais, lorsque nous sélectionnons une case vide, notre interface de création s’ouvrira. Il faut maintenant attribuer des actions aux boutons Yes et No de notre interface.

Create Tower
Notre interface de création

Créons un événement JavaScript, dans notre fonction init, pour réagir à un clic sur le bouton Yes. Puis, dans la fonction liée à cet évènement, nous utiliserons la variable temporaire newTowerMeshToCreate de towerMngr.

document.getElementById("buttonyes").addEventListener('click', function()
{
    event.stopPropagation();

    var tmpTower = towerMngr.newTowerMeshToCreate;
    scene.add(tmpTower);
    towerMngr.addTower(tmpTower);

    towerMngr.newTowerMeshToCreate = undefined;
    createTowerGui_close();
});

Dans cette fonction, nous l’ajoutons à la scène et dans la liste towerArray de towerMngr, grâce à notre méthode addTower.

Puis, une fois le traitement terminé, nous redéfinissons la valeur de newTowerMeshToCreate sur undefined, et nous fermons l’interface.

Création de la Tour
Création de la tour

Nous sommes désormais capables de placer une tour en appuyant sur le bouton Yes ! Pour finir, créons un événement pour le bouton No :

document.getElementById("buttonno").addEventListener('click', function()
{
    event.stopPropagation();
    towerMngr.newTowerMeshToCreate = undefined;
    createTowerGui_close();
});

Cette fonction simple réinitialise la variable temporaire newTowerMeshToCreate sur undefined et ferme l’interface.

La fonctionnalité de création de tours dans notre carte est désormais terminée !

Supprimer une tour

Retournons dans la fonction onMouseUp.

Si la variable checkTower est différente de null, nous définissons son contenu comme valeur de la variable temporaire selectedTower. Puis, nous ouvrons et fermons les interfaces appropriées :

if(checkTower == null)
{
    [...]
}
else 
{
    towerMngr.selectedTower = checkTower;
    createTowerGui_close();
    infoTowerGui_open(checkTower.mesh.position.x, checkTower.mesh.position.z);
}

Désormais, lorsque nous sélectionnons une case occupée par une tour, notre interface d’information s’ouvrira.

Comme dans la partie précédente, il faut désormais attribuer des actions aux boutons Delete Tower et Close de notre interface.

Interface d'informations d'une tour
Notre interface d’informations

Dans notre fonction init, créons un évènement JavaScript pour réagir à un clic sur le bouton Delete Tower.

Premièrement, notre fonction événement supprime la tour de la scène et de la liste towerArray de towerMngr. Puis, nous fermons l’interface et redéfinissons la variable temporaire selectedTower sur undefined.

document.getElementById("buttondelete").addEventListener('click', function()
{
    event.stopPropagation();
    towerMngr.deleteTower(towerMngr.selectedTower);
    scene.remove(towerMngr.selectedTower.mesh);

    infoTowerGui_close();
    towerMngr.selectedTower = undefined;
});

Ainsi, nous sommes désormais capables de supprimer une tour existante. Pour finir, créons un évènement pour le second bouton (Close) :

document.getElementById("buttonclose").addEventListener('click', function()
{
    event.stopPropagation();
    infoTowerGui_close();
    towerMngr.selectedTower = undefined;
});

La fonctionnalité de suppression de tours est désormais terminée !

Finalisation des évènements

Il manque une dernière étape pour clôturer ce chapitre. Dans le chapitre précédent, nous avions créé des événements de type document.addEventListener pour les événements pointerdown et pointerup.

Mais, il est aujourd’hui nécessaire de transformer ces lignes de code. Changeons document.addEventListener par renderer.domElement.addEventListenerUniquement pour les évenements pointerdown et pointerup !

renderer.domElement.addEventListener('pointerdown', onMouseDown, false);
renderer.domElement.addEventListener('pointerup', onMouseUp, false);

Pourquoi ce changement ?

Lorsque nous cliquons sur les boutons de nos interfaces graphiques, les actions liées sont correctement déclenchées. Cependant, les événements pointerdown et pointerup de l’élément document sont également activés, car un clic est détecté sur la page !

Changer la cible de ces deux événements est donc nécessaire.

Résultat final

Téléchargez le code final : Github.

Félicitations, vous avez réussi à terminer ce nouveau chapitre. Désormais, nous sommes capables de créer et supprimer des tours dans notre carte de jeu !

Pour finir ce chapitre, voici un aperçu de notre réalisation :

Création et suppression de tours

Dans le prochain chapitre, nous intégrerons des ennemis dans notre carte de jeu !

Un commentaire

Les commentaires sont fermés.