Fil d'Ariane
Mis à jour le mardi 18 octobre 2016
  • 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Créez un système de points de vie et de spawn aléatoire

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

Bienvenue pour cette nouvelle partie du cours ! Nous allons désormais nous concentrer sur tout l'aspect multijoueur et jouabilité ! Il va non seulement implémenter des nouveautés, mais aussi modifier certaines parties de notre code. Car oui, nous avons fait quelques erreurs – je vous rassure, elles sont volontaires ! ;)

Dans les chapitres précédents, nous avons utilisé registerAfterRender pour une meilleure clarté du code, mais il se trouve que registerAfterRender pose un problème de taille, que vous avez peut-être remarqué...

Ce comportement peut être extrêmement positif pour les animations de décor (les animations ne se lancent que quand on les regarde, ce qui fait gagner en performance), mais c'est un véritable problème pour les roquettes, qui doivent toutes être à la même place, donc calculées même si on ne les regarde pas !

Pour que la boucle de rendu les prenne en compte, il va nous falloir ajouter les roquettes et les nuages d'explosion dans  Game.js. C'est parti !

Les roquettes et les explosions qui bougent tout le temps

Premièrement, nous allons mettre en place deux fonctions dans  Game.prototype, en dessous de _initScene.

renderRockets : function() {
},
renderExplosionRadius : function(){
},

Ces deux fonctions vont être appelées à chaque frame dans  engine.runRenderLoop présent dans  Game. Ce sont elles qui vont s'occuper de gérer l'action de nos meshes à chaque frame.

// Permet au jeu de tourner
engine.runRenderLoop(function () {

    // Récuperet le ratio par les fps
    _this.fps = Math.round(1000/engine.getDeltaTime());

    // Checker le mouvement du joueur en lui envoyant le ratio de déplacement
    _player._checkMove((_this.fps)/60);

    // On apelle nos deux fonctions de calcul pour les roquettes
    _this.renderRockets();
    _this.renderExplosionRadius();
    
    // On rend la scène
    _this.scene.render();
    
    // Si launchBullets est à true, on tire
    if(_player.camera.weapons.launchBullets === true){
        _player.camera.weapons.launchFire();
    }
});

Maintenant que les deux fonctions tournent, nous devons regrouper toute les roquettes générées dans  Player.js. Pour cela, nous allons les mettre dans un tableau. Les explosions fonctionneront de la même façon. Occupons-nous de définir ces deux variables, juste au-dessus de la runRenderLoop.

// Les roquettes générées dans Player.js
this._rockets = [];

// Les explosions qui découlent des roquettes
this._explosionRadius = [];

Les roquettes

Il va falloir faire en sorte que  renderRockets déplace toute les roquettes présentes dans le tableau  _rockets. Pour cela, une simple boucle suffira !

renderRockets : function() {
    for (var i = 0; i < this._rockets.length; i++) {
        
    };
},

Ok, la boucle est prête et attend nos objets. En route pour la suite ! 

Vous allez devoir "découper" notre code présent dans  Weapons.js. L'idée est de placer intelligemmentcreateRocket() dans Game.js. Au final, cette fonction ne fera que créer la roquette et l'envoyer dans Game. Et c'est là que this.game dans  Player trouve tout son intérêt ! Puisque  Weapons est lié a  Player, et que  Player est lié à Game, il nous suffit de remonter le courant ! ^^ 

Dans  createRocket(), juste avant le registerAfterRender, on ajoute donc la ligne suivante :

this.Player.game._rockets.push(newRocket);

Maintenant que  newRocket est envoyé à  Game, on va pouvoir découper notre code présent dans registerAfterRender ! Ce qui va être complexe, c'est que la fonction gère aussi la création de la zone d'explosion. Il va donc falloir faire attention à tout cela ! Normalement, le code a déjà été pré-découpé grâce à la séparation que nous avons effectué dans le code. Ce qu'il vous faut faire, c'est entièrement couper le code dans la boucle et le mettre dans notre boucle  for dans renderRockets(). Bien sûr, vous allez être obligé de réadapter les variables appelées :

  •  newRocket devient this._rockets[i] ;

  •  Player.game.scene devient this.scene.

Vous voilà avec une boucle for. La fonction renderRockets() ressemble à ceci :

// On crée un rayon qui part de la base de la roquette vers l'avant
var rayRocket = new BABYLON.Ray(this._rockets[i].position,this._rockets[i].direction);

// On regarde quel est le premier objet qu'on touche
var meshFound = this._rockets[i].getScene().pickWithRay(rayRocket);

// Si la distance au premier objet touché est inférieure à 10, on détruit la roquette
if(!meshFound || meshFound.distance < 10){
    // On vérifie qu'on a bien touché quelque chose
    if(meshFound.pickedMesh && !meshFound.pickedMesh.isMain){
        // On crée une sphere qui représentera la zone d'impact
        var explosionRadius = BABYLON.Mesh.CreateSphere("sphere", 5.0, 20, this.scene);
        // On positionne la sphère là où il y a eu impact
        explosionRadius.position = meshFound.pickedPoint;
        // On fait en sorte que les explosions ne soient pas considérées pour le Ray de la roquette
        explosionRadius.isPickable = false;
        // On crée un petit material orange
        explosionRadius.material = new BABYLON.StandardMaterial("textureExplosion", this.scene);
        explosionRadius.material.diffuseColor = new BABYLON.Color3(1,0.6,0);
        explosionRadius.material.specularColor = new BABYLON.Color3(0,0,0);
        explosionRadius.material.alpha = 0.8;
        
        // Chaque frame, on baisse l'opacité et on efface l'objet quand l'alpha est arrivé à 0
        explosionRadius.registerAfterRender(function(){
            explosionRadius.material.alpha -= 0.02;
            if(explosionRadius.material.alpha<=0){
                explosionRadius.dispose();
            }
        });
    }
    this._rockets[i].dispose();
}

Avant de passer à la suite, nous devons résoudre deux bugs problématiques qui se présentent à nous.

  • Une roquette est bien supprimée, mais elle n'est pas enlevée de la liste des roquettes.

  • Il faudrait déplacer le mesh plus ou moins vite selon les FPS du joueur, pour que toutes les roquettes se déplacent à la même vitesse sur tous les ordinateurs.

Pour le premier point, c'est extrêmement simple. Juste après avoir supprimé le mesh de Babylon avec  dispose(), on l’enlève du tableau avec splice().

// On enlève de l'array _rockets le mesh numéro i (défini par la boucle)
this._rockets.splice(i,1);

Pour le deuxième problème, nous allons créer un  else() en plus de notre  if. Il va déterminer si un objet a été touché à moins de 10 unités et y déplacer translate(), présent en haut de la fonction.

// Si la distance au premier objet touché est inférieure à 10, on détruit la roquette
if(!meshFound || meshFound.distance < 10){
    // On vérifie qu'on a bien touché quelque chose
    if(meshFound.pickedMesh && !meshFound.pickedMesh.isMain)){
        // On crée une sphere qui représentera la zone d'impact
        var explosionRadius = BABYLON.Mesh.CreateSphere("sphere", 5.0, 20, this.scene);
        // On positionne la sphère là où il y a eu impact
        explosionRadius.position = meshFound.pickedPoint;
        // On fait en sorte que les explosions ne soient pas considérées pour le Ray de la roquette
        explosionRadius.isPickable = false;
        // On crée un petit material orange
        explosionRadius.material = new BABYLON.StandardMaterial("textureExplosion", this.scene);
        explosionRadius.material.diffuseColor = new BABYLON.Color3(1,0.6,0);
        explosionRadius.material.specularColor = new BABYLON.Color3(0,0,0);
        explosionRadius.material.alpha = 0.6;
        
        // Chaque frame, on baisse l'opacité et on efface l'objet quand l'alpha est arrivé à 0
        explosionRadius.registerAfterRender(function(){
            explosionRadius.material.alpha -= 0.02;
            if(explosionRadius.material.alpha<=0){
                explosionRadius.dispose();
            }
        });
    }
    this._rockets[i].dispose();
    this._rockets.splice(i,1);
}else{
    let relativeSpeed = 1 / ((this.fps)/60);
    this._rockets[i].translate(new BABYLON.Vector3(0,0,1),relativeSpeed,0);
}

Nous avons encore un autre problème ! La roquette ne part pas toujours droit. Selon l'axe de la caméra, elle ne part jamais exactement du centre du joueur.

let relativeSpeed = 1 / ((this.fps)/60);
this._rockets[i].position.addInPlace(this._rockets[i].direction.scale(relativeSpeed))

addInPlace nous permet d'ajouter un vecteur à un autre. Nous n'avons donc qu'à ajouter le vecteur de direction à la position actuelle !  scale nous permet de multiplier le vecteur de direction par la  relativespeed pour faire avancer plus ou moins vite selon les performances.

Nous avons donc passé la première étape : transférer le déplacement des roquettes de  Weapons vers Game. Pas d'inquiétude : ajouter  renderExplosionRadius() va être bien moins laborieux ! ;)

Le rayon d'explosion

Maintenant qu'il n'y a plus de gestion de explosionRadius dans l'objet  Weapons, tout va se passer dans Game.js ! Comme pour renderRockets(), nous allons créer une boucle qui va explorer tous les objets présents dans  _explosionRadius.

renderExplosionRadius : function(){
    for (var i = 0; i < this._explosionRadius.length; i++) {
    }
}

Et de nouveau comme pour la fonction précédente, nous allons récupérer tout ce qui est dans  registerAfterRender qui concerne  explosionRadius. Cette boucle se trouve dans  renderRockets(). Copiez ce qu'il y a dedans pour le mettre dans notre fonction toute prête. 

Votre fonction  renderExplosionRadius() devrait ressembler à ceci.

renderExplosionRadius : function(){
    if(this._explosionRadius.length > 0){
        for (var i = 0; i < this._explosionRadius.length; i++) {
            this._explosionRadius[i].material.alpha -= 0.02;
            if(this._explosionRadius[i].material.alpha<=0){
                this._explosionRadius[i].dispose();
                this._explosionRadius.splice(i, 1);
            }
        }
    }
}

Maintenant que tout est transféré, on remplace  registerAfterRender présent dans  renderRockets()  par un envoi de l'objet  explosionRadius dans le tableau  _explosionRadius.

this._explosionRadius.push(explosionRadius);

Nous y voilà ! Cette section était complexe à exécuter car il fallait déplacer des grosses parties de notre code. Je vous promets que nous n'aurons désormais plus à déplacer des parties de code de cette façon. L'objectif était de vous montrer comment migrer des parties de codes comme celles-ci. ;)

Passons aux choses sérieuses ! Il est temps d'activer les dégâts ! 

Rendre les armes mortelles, enfin !

Maintenant que les roquettes se déplacent, il va nous falloir travailler sur les dégâts qu'elles causent ! Le concept du lance-roquettes, c'est qu'il inflige des dégâts aux personnes présentes dans le rayon d'explosion. Nous allons donc devoir vérifier si les joueurs se trouvent dans son rayon d'impact. Étant donné que nous prévoyons d'ajouter un aspect multijoueur plus tard, nous allons accomplir plusieurs étapes :

  • Ajouter des variables à l'objet  Player.

  • Implémenter un tableau avec tous les joueurs.

  • Vérifier si un joueur est dans le rayon de dégât.

  • Infliger les dégâts au joueur concerné.

Re-travail de Player

Nous devons préparer l'objet  Player en lui ajoutant quelques variables nécessaires pour la suite.

Premièrement, dans  _initCamera, vous allez créer trois nouvelles fonctions qu'on va ajouter juste après avoir défini les paramètres de  playerBox

// La santé du joueur
this.camera.health = 100;
// L'armure du joueur
this.camera.armor = 0;

health et  armor n'ont pas besoin de réelles explications. Ils vont nous servir comme paramètres pour gérer l'état du joueur.

Maintenant que tout a été ajouté, on peut passer à la suite : vérifier si le joueur a été touché par le rayon de l'explosion. Direction Game.js !

Inspecter l'impact pour savoir si le joueur est touché

Dans la fonction  renderRockets de  Game, nous allons détecter si le joueur est pris dans l'impact en vérifiant à la création de la sphère d'explosion s'il y a un objet dedans. Pour cela, BabylonJS nous donne une fonction plutôt magique :  intersectsMesh. Vous allez donc la placer juste après la création de  explosionRadius, et avant qu'elle soit envoyée à  _explosionRadius

if (this._PlayerData.isAlive && this._PlayerData.camera.playerBox && explosionRadius.intersectsMesh(this._PlayerData.camera.playerBox)) {
    // Envoi à la fonction d'affectation des dégâts
    console.log('hit')
}

Ce qu'on fait ici, ce n'est que calculer les dégâts pour soi. Dans le jeu, chacun enverra les dégâts qu'il a reçu ainsi.

Maintenant, on devrait avoir un log dans la console JavaScript qui nous dit quand les hits sont effectués.

Les hits ne sont pas détectés. On dirait que ça ne fonctionne pas... J'ai fait quelque chose de mal ? :euh:

Absolument pas ! Nous avons juste oublié d'ajouter une ligne plus qu'essentielle :  computeWorldMatrix.

Juste avant la boucle  for que nous avons écrite, nous ajoutons donc :

// Calcule la matrice de l'objet pour les collisions
explosionRadius.computeWorldMatrix(true);

// On fait un tour de bouche pour chaque joueur de la scène
if (this._PlayerData.isAlive && this._PlayerData.camera.playerBox && explosionRadius.intersectsMesh(this._PlayerData.camera.playerBox)) {
    // Envoi à la fonction d'affectation des dégats
    console.log('hit')
}

La console nous annonce désormais quand le joueur prend des dégâts ! Super ! Mais il va falloir concrétiser ça en perte de points de vie sur le joueur ! Nous allons donc remplacer le log par :

// Envoi à la fonction d'affectation des dégâts
this._PlayerData.getDamage(30)

Et maintenant, il nous faut créer la fonction  getDamage à la volée ! :) 

Retour dans Player pour répartir les dégâts

Nous allons ajouter notre fonction tout en bas du prototype de  Player.

getDamage : function(damage){
        
},

On transfert ce que le joueur doit prendre comme dégâts et il est temps de lui faire subir les affres du combat (mouahaha :diable:).

getDamage : function(damage){
    var damageTaken = damage;
    // Si le joueur i a encore de la vie
    if(this.camera.health>damageTaken){
        this.camera.health-=damageTaken;
    }else{
        // Sinon, il est mort
        console.log('Vous êtes mort...');
    }
},

Avec notre liste de joueurs, nous vérifions si celui ciblé a encore des points de vie. Si c'est le cas, il prend les dégâts. Sinon, il meurt.

Mais ce code n'intègre pas la gestion de l'armure ! Nous allons modifier tout ça de telle sorte que l'armure pourra prendre jusqu'à la moitié des dégâts subis par le joueur. Voilà notre calcul savant :

getDamage : function(damage){
    var damageTaken = damage;
    // Tampon des dégâts par l'armure
    if(this.camera.armor > Math.round(damageTaken/2)){
        this.camera.armor -= Math.round(damageTaken/2);
        damageTaken = Math.round(damageTaken/2);
    }else{
        damageTaken = damageTaken - this.camera.armor;
        this.camera.armor = 0;
    }

    // Si le joueur i a encore de la vie
    if(this.camera.health>damageTaken){
        this.camera.health-=damageTaken;
    }else{
        // Sinon, il est mort
        console.log('Vous êtes mort...');
    }
},

Pour l'instant, la mort n'est pas réelle dans notre jeu ! Nous allons donc devoir créer la plus fataliste des fonctions :  playerDead.

Réinitialisons les joueurs

Il est maintenant temps de tuer nos joueurs ! Je sais, ce n'est pas très gai, mais il faut y passer. Et puis après la mort vient la vie ! Enfin, dans les jeux vidéo. >_<

Pour réinitialiser le joueur, il va falloir effectuer plusieurs choses :

  • Créer une caméra de remplacement.

  • Supprimer l'ancienne caméra ainsi que tout ce qui y est associé.

  • Enlever le  Player de la liste des  players.

  • Relancer l'initialisation de la caméra.

Préparer le terrain

Avant toutes ces étapes, nous allons créer la fonction qui va enclencher tout cela. Sous  getDamage(), ajoutez la fonction  playerDead() qui prendra en argument  i, représentant le numéro du joueur dans le tableau  players. Quand vous aurez créé cette fonction, ajoutez-la à  getDamage. Les deux fonctions devraient ressembler à ceci :

getDamage : function(damage){
    var damageTaken = damage;
    // Tampon des dégâts par l'armure
    if(this.camera.armor > Math.round(damageTaken/2)){
        this.camera.armor -= Math.round(damageTaken/2);
        damageTaken = Math.round(damageTaken/2);
    }else{
        damageTaken = damageTaken - this.camera.armor;
        this.camera.armor = 0;
    }

    // Si le joueur i a encore de la vie
    if(this.camera.health>damageTaken){
        this.camera.health-=damageTaken;
    }else{
        // Sinon, il est mort
        this.playerDead()
    }
},

playerDead : function() {
    console.log('playerDead lancé');
},

Maintenant que tout est prêt, on peut se lancer !

La caméra de "mort"

Notre objectif ici va être de changer la caméra à la mort du joueur pour une  arcRotateCamera. Nous allons donc aller dans notre fonction nouvellement créée,  playerDead, et y inscrire ceci :

playerDead : function(i) {
    this.deadCamera = new BABYLON.ArcRotateCamera("ArcRotateCamera", 
    1, 0.8, 10, new BABYLON.Vector3(
        this.camera.playerBox.position.x, 
        this.camera.playerBox.position.y, 
        this.camera.playerBox.position.z), 
    this.game.scene);
    
    this.game.scene.activeCamera = this.deadCamera;
    this.deadCamera.attachControl(this.game.scene.getEngine().getRenderingCanvas());
},

Nous avons simplement donné la position du joueur comme coordonnée pour la caméra. 

La donnée  activeCamera permet d'indiquer à Babylon quelle caméra utiliser quand il y en a plusieurs sur la scène. Ensuite, comme vu précédemment, nous attachons les contrôles de cette nouvelle caméra à l'ordinateur de l'utilisateur. Enregistrez et essayez de tuer votre  Player pour voir, vous devriez vous voir ! ^^

Regarde maman, je passe à la télé!
Regarde maman, je passe à la télé !

On peut voir notre boîte qui sert de collider pour les explosions. Le carré rouge correspond au bout de votre canon. Amusant non ? ^^

Supprimer tous les objets associés à l'ancienne caméra

Cela dit, il ne faudrait pas qu'on puisse se voir ! Il va donc falloir "purger" le  Player pour qu'il n'en reste plus rien. Pour cela, nous allons détruire un tas d'objets :

  • la  playerBox doit être détruite ;

  • la caméra doit être détruite ;

  • l'arme doit être détruite ;

  • le  Player doit être enlevé de la liste des  players ;

  • la variable  isAlive doit passer à  false.

Une longue liste de courses dites donc ! Ajoutons tout cela au code en dessous de l'instanciation de la  deadCamera.

// Suppression de la playerBox
this.camera.playerBox.dispose();

// Suppression de la camera
this.camera.dispose();   

// Suppression de l'arme
this.camera.weapons.rocketLauncher.dispose();

// On signale à Weapons que le joueur est mort
this.isAlive=false;

Et voilà ! Si vous relancez l'application, vous verrez que… le joueur n’apparaît plus ! 

De la mort... à la vie!

Maintenant que le joueur est mort, il est temps de mettre un petit décompte et de le faire revenir parmi les vivants ! On va utiliser un simple  timeOut ici.

var newPlayer = this;
var canvas = this.game.scene.getEngine().getRenderingCanvas();
setTimeout(function(){ 
    newPlayer._initCamera(newPlayer.game.scene, canvas);
}, 4000);

Au bout de 4000 millisecondes, on rappelle  _initCamera  qui va recréer le joueur proprement, et la partie continue !

Félicitations, vous êtes revenu d'entre les morts ! :lol: À nous maintenant de définir un point de resurrection (ou respawn) aléatoire sur le terrain !

Revenir en jeu, là où on ne s'y attend pas !

Pour créer des  spawnPoints, on va ajouter dans  Game.js plusieurs valeurs qui définiront où les utilisateurs pourront réapparaître sur la carte. Nous allons définir ce tableau juste en dessous de  _this.actualTime().

this.allSpawnPoints = [
    new BABYLON.Vector3(-20, 5, 0),
    new BABYLON.Vector3(0, 5, 0),
    new BABYLON.Vector3(20, 5, 0),
    new BABYLON.Vector3(-40, 5, 0)
];

Ensuite, allez tout au début de la fonction  _initCamera pour faire un random sur toutes ces positions, puis les affecter à la  playerBox.

// Math.random nous donne un nombre entre 0 et 1
let randomPoint = Math.random();

// randomPoint fait un arrondi de ce chiffre et du nombre de spawnPoints
randomPoint = Math.round(randomPoint * (this.game.allSpawnPoints.length - 1));

// On dit que le spawnPoint est celui choisi selon le random plus haut
this.spawnPoint = this.game.allSpawnPoints[randomPoint];

var playerBox = BABYLON.Mesh.CreateBox("headMainPlayer", 3, scene);
// On donne le spawnPoint avec clone() pour que celui-ci ne soit pas affecté par le déplacement du joueur
playerBox.position = this.spawnPoint.clone();
playerBox.ellipsoid = new BABYLON.Vector3(2, 2, 2);

Actualisez la page, et voilà ! Votre joueur apparaît sur une position aléatoire sur le terrain de jeu !

  

On y est presque ! On a presque un jeu jouable ! Mais avant de passer à la connexion entre les joueurs ou encore à l'ajout de mesh, nous allons devoir étoffer notre arsenal d'armes‌. En route ! ^^

Exemple de certificat de réussite
Exemple de certificat de réussite