Fil d'Ariane
Mis à jour le jeudi 31 août 2017
  • 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 !

Liez vos joueurs avec Node.js !

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

Nous y voilà ! Nous allons enfin mettre les joueurs en réseau ! L’intérêt de ce chapitre est de vous montrer comment fonctionne le serveur sans rentrer dans les détails : on pourrait créer un cours entier consacré aux réseaux dans les jeux vidéo ! Ils ont une façon de gérer la connexion relativement différente de ce que nous connaissons dans le développement web.

Pour faire fonctionner tout ça, j'utilise Node.js, mais vous pouvez suivre ce chapitre sans le maîtriser. Vous allez voir. :)

Téléchargez tous les fichiers nécessaires pour ce chapitre ici.

Petit cahier des charges de notre version multijoueur

Un gameplay simple

Je vous propose un jeu basé sur une mécanique empruntée à Agar.io et à des jeux similaires : plus vous restez longtemps dans le jeu, plus votre score augmente (et plus vous avez de chances de monter dans le leaderboard !). Le score est effacé si vous sortez du jeu. 

Un leaderboard doit donc être présent et recenser les 5 meilleurs résultats de la partie.

Un réseau léger

Au niveau du réseau, l'objectif est d'économiser au maximum la bande passante des joueurs pour que la connexion ne pose pas problème. 

Nous allons donc envoyer les données de façon un peu spéciale : au lieu d'envoyer les coordonnées des personnages toute les frames (60 fois par seconde et par joueur donc), nous allons plutôt envoyer les touches pressées par le joueur. Nous saurons ainsi dans quel sens le joueur bouge et nous n'aurons plus qu'à faire bouger le fantôme dans le même sens que celui-ci. Pour information, le fantôme représente le mesh du joueur affiché sur l'écran des adversaires ;) 

En plus de cela, nous enverrons toutes les 4 secondes un ping à tous les joueurs pour vérifier les positions et l'orientation actuelle. L'objectif est de re-calibrer les déplacements s'il y a un souci de connexion.

Des graphismes épurés

Au niveau UI, nous allons simplement proposer une interface en teintes de gris pour mettre en avant tous les objets interactifs, qui sont eux en couleur. 

Des fichiers HTML et CSS déjà prêts

J'ai préparé un HTML et un CSS rien pour vous, étant donné que notre cours porte exclusivement sur le WebGL (et cela fait déjà pas mal de choses à voir ;)).

Comme vous pouvez le noter, la plupart des éléments sont en pointer-event : none dans le CSS pour les rendre inactifs par rapport au canvas en dessous (c'est-dire qu'ils ne captent pas la souris). 

Quant au HTML en lui même, vous voyez deux grands blocs dans lesquels avec le tableau des scores, les points de vie, d'armure et enfin les munitions (cet élément sera le sujet d'un chapitre dans la quatrième et dernière partie de ce cours).

Notre objectif est de rendre ces champs dynamiques selon ce que le serveur nous enverra.

Notre liste de tâches

Pour réaliser tout ça, voici ce que vous allez devoir faire :

  • Comprendre le fonctionnement basique de NetworkManager

  • Créer les GhostPlayer

  • Créer les ponts avec Player, Weapon et Arena

  • Rendre le HTML dynamique

Et maintenant, il ne nous reste plus qu'à passer à l'action ! Première étape : récupérer les fichiers (si vous ne l'avez pas déjà fait) qui vont vous servir à développer la version multijoueur. Copiez-collez ensuite les nouveaux fichiers JavaScript que vous venez de télécharger (NetworkManager.js et GhostPlayer.js) dans votre dossier javascript, avec ceux qui contiennent votre code Babylon.

NetworkManager, la fonction qui gère le dialogue serveur !

Actuellement, presque tout le contenu du fichier NetworkManager.js est commenté. Ce fichier gère la connexion avec le serveur : nous allons décommenter ce fichier pas à pas pour activer les ponts.

L'intérêt de NetworkManager.js et de ses quelques 200 lignes de code est d'envoyer et de recevoir les messages que le serveur va envoyer.

Le lancement du jeu déménage !

Comme vous le voyez, Game n'est plus appelé dans Game.js anonymement, mais directement dans ce fichier. L'intérêt est de lancer le jeu uniquement quand nous avons reçu toutes les données serveur. Il faut connaître la position et l'état actuel de tous les joueurs pour pouvoir lancer le jeu.

Vous allez donc devoir enlever toute la première partie du code présent dans Game.js qui lance le jeu avec DOMContentLoaded, puisque que tout est désormais lancé depuis le NetworkManager (vous pouvez voir qu'il est appelé dans la page index). ^^ 

updatePlayer et newPlayer, les deux socket principaux

Pendant que newPlayer crée le joueur et envoie un tableau de données sur les joueurs présents, updatePlayer s'occupe de modifier les données de chaque ghost selon ce que les joueurs vont envoyer au serveur.

Cette fonction va servir aussi bien à actualiser toutes les données qu'à en actualiser un lot spécifique (si le joueur change simplement son orientation, il n'enverra que cette information et pas plus). L'objectif est, comme je vous le disais plus haut, d'économiser les performances au maximum ! ^^ 

Maintenant que vous avez vu les grandes lignes de NetworkManager.js, je vous conseille de regarder tous les messages transférés qui correspondent à tous les appels dont nous allons avoir besoin ici.

Ça y est ? Vous êtes familiarisé avec tous les messages ? Nous allons passer à l'instanciation du GhostPlayer. ;)

GhostPlayer, un modèle similaire à _initCamera

Actuellement, le fichier GhostPlayer.js est vide. Nous allons devoir créer deux fonctions : une qui crée le joueur et une qui le supprime. Ces deux fonctions seront appelées dès que les joueurs ennemis seront tués et qu'ils devront réapparaître.

GhostPlayer = function(game,ghostData,idRoom) {
    // On dit que game est acessible dans l'objet
    this.game = game;
    var fakePlayer = {};

    // On donne à notre ghost une rotation et position envoyées par le serveur.
    var positionSpawn = new BABYLON.Vector3(ghostData.position.x,
        ghostData.position.y,
        ghostData.position.z);

    var rotationSpawn = new BABYLON.Vector3(ghostData.rotation.x,
        ghostData.rotation.y,
        ghostData.rotation.z);
}

À la création de l'objet, la fonction GhostPlayer reçoit trois valeurs :

  • game qui se trouve être notre objet Game ;

  • ghostData qui comprend toutes les informations nécessaires quant au jeu pour chaque ghost ;

  • idRoom qui est l'id généré par Socket.IO, que nous allons attribuer au ghost pour qu'il puisse envoyer facilement au serveur les dégâts aux bonnes personnes.

Nous allons créer maintenant le corps, la tête et la hitBox de notre ghost. Attention, je vous parle de tête et de corps… mais ce sera simplement deux carrés l'un sur l'autre ! ;)

this.game = game;
var fakePlayer = {};

var positionSpawn = new BABYLON.Vector3(ghostData.position.x,
    ghostData.position.y,
    ghostData.position.z);

var rotationSpawn = new BABYLON.Vector3(ghostData.rotation.x,
    ghostData.rotation.y,
    ghostData.rotation.z);

fakePlayer.playerBox = BABYLON.Mesh.CreateBox(ghostData.id, 5, this.game.scene);
fakePlayer.playerBox.scaling = new BABYLON.Vector3(0.5,1.2,0.5)
fakePlayer.playerBox.position = positionSpawn;
fakePlayer.playerBox.isPlayer = true;
fakePlayer.playerBox.isPickable = true;

fakePlayer.playerBox.material = new BABYLON.StandardMaterial("textureGhost", this.game.scene);
fakePlayer.playerBox.material.alpha = 0;

fakePlayer.playerBox.checkCollisions = true;
fakePlayer.playerBox.applyGravity = true;
fakePlayer.playerBox.ellipsoid = new BABYLON.Vector3(1.5, 1, 1.5);

fakePlayer.head = BABYLON.Mesh.CreateBox('headGhost', 2.2, this.game.scene);
fakePlayer.head.parent = fakePlayer.playerBox;
fakePlayer.head.scaling = new BABYLON.Vector3(2,0.8,2)
fakePlayer.head.position.y+=1.6;
fakePlayer.head.isPickable = false;

fakePlayer.bodyChar = BABYLON.Mesh.CreateBox('bodyGhost', 2.2, this.game.scene);
fakePlayer.bodyChar.parent = fakePlayer.playerBox;
fakePlayer.bodyChar.scaling = new BABYLON.Vector3(2,0.8,2)
fakePlayer.bodyChar.position.y-=0.6;
fakePlayer.bodyChar.isPickable = false;

Vous pouvez voir qu'on passe le corps et la tête en isPickable false : en effet, la hitbox fait déjà le travail pour nous. On donne comme nom à notre playerBox l'id qui est présent dans Socket.IO pour pouvoir le rapeller facilement plus tard.

Il nous reste plus qu'à définir en dessous de tout ça toutes les spécificités de cet objet.

// Les datas de vie et d'armure du joueur
fakePlayer.health = ghostData.life;
fakePlayer.armor  = ghostData.armor;

// Une variable en prévision de la fonction de saut
fakePlayer.jumpNeed = false;

// La place du joueur dans le tableau des joueurs, gérée par le serveur
fakePlayer.idRoom = idRoom;

// L'axe de mouvement. C'est lui qui recevra les informations de touches pressées envoyées par le joueur
fakePlayer.axisMovement = ghostData.axisMovement;

// Le nom réel du joueur
fakePlayer.namePlayer = ghostData.name;

// A nouveau l'id du joueur
fakePlayer.uniqueId = ghostData.uniqueId;

// La rotation. Comme pour le mouvement, elle sert à déterminer le sens de déplacement
fakePlayer.rotation = rotationSpawn;

// Les materials qui définissent la couleur du joueur
fakePlayer.head.material = new BABYLON.StandardMaterial("textureGhost", this.game.scene);
fakePlayer.head.material.diffuseColor = new BABYLON.Color3(0, 1, 1);

fakePlayer.bodyChar.material = new BABYLON.StandardMaterial("textureGhost", this.game.scene);
fakePlayer.bodyChar.material.diffuseColor = new BABYLON.Color3(0, 0.6, 0.6);

return fakePlayer;

Beaucoup de ces données vous évitent de recourir à des boucles pour envoyer des données au serveur. Avec ce code, le GhostPlayer a les valeurs essentielles immédiatement à disposition.

Maintenant que nous avons notre fonction pour le GhostPlayer, nous allons rapidement faire un tour dans Player pour ajouter une ligne juste en dessous de weaponShoot.

this.ghostPlayers=[];

C'est ici qu'on va lister tous les joueurs affichés à l'écran. Et c'est dans cette boucle qu'on va supprimer les joueurs si jamais ils disparaissent du jeu ou s'ils sont simplement éliminés par quelqu'un d'autre.

À nous de gérer maintenant l'aspect suppression ! Retournez dans GhostPlayer.js pour travailler la fonction deleteGameGhost.  

En premier lieu, on doit chercher à travers tous les joueurs présents le joueur qui a l'id du joueur à supprimer envoyé par le serveur. Pour cela, on récupère tous les joueurs de ghostPlayers et on regarde si l'un d'eux est celui qu'on cherche.

deleteGameGhost = function(game,deletedIndex){
    ghostPlayers = game._PlayerData.ghostPlayers;
    for (var i = 0; i < ghostPlayers.length; i++) {
        if(ghostPlayers[i].idRoom === deletedIndex){
        }
    }
}

 Quand on le trouve, il nous reste plus qu'à le supprimer.

deleteGameGhost = function(game,deletedIndex){
    ghostPlayers = game._PlayerData.ghostPlayers;
    for (var i = 0; i < ghostPlayers.length; i++) {
        console.log(ghostPlayers[i].idRoom);
        console.log(deletedIndex)
        if(ghostPlayers[i].idRoom === deletedIndex){
            ghostPlayers[i].playerBox.dispose();
            ghostPlayers[i].head.dispose();
            ghostPlayers[i].bodyChar.dispose();
            ghostPlayers[i] = false;

            ghostPlayers.splice(i,1);
            break;
        }
        
    }
}

On supprime ainsi tous les objets présents dans ghostPlayer et on efface le joueur concerné du tableau des joueurs. 

Et voilà ! Quand vous vous connectez à plusieurs, vous pouvez vous voir ! Cependant, vous ne pouvez pas encore vous déplacer ni interagir. Alors, en route pour connecter le NetworkManager avec le reste de l'application ! ^^ 

Connecter le réseau avec le reste de l'application

L'objectif du NetworkManager est de regrouper tous les appels réseau dans un même fichier. Dans le reste du code, on ne va laisser que des appels à ce fichier. Nous allons donc créer ces appels dans l'ordre de nos fichiers JavaScript, de haut en bas !

Player.js

On commence par un élément essentiel dans Player : l'envoi des données de déplacement.

Quelles que soit les données que vous allez envoyer ici, vous les enverrez à sendNewData qui va s'occuper d'actualiser intelligemment les données modifiées. Elles doivent cependant être organisées d'une façon bien particulière :

  • Si vous voulez envoyer des changements de touche, envoyez data.axisMovement.

  • Si vous voulez envoyer une rotation, envoyez data.rotation.

  • Si vous voulez envoyer des positions en dur, envoyez data.position.

Pour envoyer les données au serveur pour la position, il vous suffit désormais d'ajouter les lignes suivantes :

window.addEventListener("keyup", function(evt) {
    if(evt.keyCode == 90 || evt.keyCode == 83 || evt.keyCode == 81 || evt.keyCode == 68 ){
        switch(evt.keyCode){
            case 90:
            _this.camera.axisMovement[0] = false;
            break;
            case 83:
            _this.camera.axisMovement[1] = false;
            break;
            case 81:
            _this.camera.axisMovement[2] = false;
            break;
            case 68:
            _this.camera.axisMovement[3] = false;
            break;
        }
        var data={
            axisMovement : _this.camera.axisMovement
        };
        _this.sendNewData(data)
        
    }
}, false);

// Quand les touches sont relachées
window.addEventListener("keydown", function(evt) {
    if(evt.keyCode == 90 || evt.keyCode == 83 || evt.keyCode == 81 || evt.keyCode == 68 ){
        switch(evt.keyCode){
            case 90:
            _this.camera.axisMovement[0] = true;
            break;
            case 83:
            _this.camera.axisMovement[1] = true;
            break;
            case 81:
            _this.camera.axisMovement[2] = true;
            break;
            case 68:
            _this.camera.axisMovement[3] = true;
            break;
        }
        var data={
            axisMovement : _this.camera.axisMovement
        };
        _this.sendNewData(data)
    }
    
}, false);

On appelle la fonction sendNewData qui est présente dans Player.prototype. Elle sert simplement à envoyer les données à NetworkManager.

On va d'ailleurs dès à présent se rendre dans le prototype de Player pour la créer !

sendNewData : function(data){
    updateGhost(data);
},

C'est tout ? Pourquoi on n'a pas directement renvoyé les données à NetworkManager si c'est pour les envoyer à une fonction qui ne fait rien de plus ?

Nous avons besoin de créer cette fonction pour une raison simple. Quand nous utilisons une fonction utilisée avec les addEventListener, sans lui donner de nom, on dit que c'est une fonction anonyme. Il devient alors compliqué d'appeler des fonctions externes. Nous appelons donc _this qui représente Player pour qu'il puisse gérer les données et les renvoyer proprement ! ^^ 

Maintenant, il nous suffit de faire de même avec la rotation présente avec mousemove.

// Quand la souris bouge dans la scène
window.addEventListener("mousemove", function(evt) {
    if(_this.rotEngaged === true){
        _this.camera.playerBox.rotation.y+=evt.movementX * 0.001 * (_this.angularSensibility / 250);
        var nextRotationX = _this.camera.playerBox.rotation.x + (evt.movementY * 0.001 * (_this.angularSensibility / 250));
        if( nextRotationX < degToRad(90) && nextRotationX > degToRad(-90)){
            _this.camera.playerBox.rotation.x+=evt.movementY * 0.001 * (_this.angularSensibility / 250);
        }
        var data={
            rotation : _this.camera.playerBox.rotation
        };
        _this.sendNewData(data)
    }
}, false);

La forme est la même que précédemment, rien de nouveau ici. Avec trois events, nous envoyons notre déplacement et celui-ci pourra être traité.

Nous allons nous attaquer à une section en lien avec celle que nous venons de voir : le déplacement des autres joueurs par rapport à soi.

_checkMove et _checkUniqueMove

Pour l'instant, Game lance à chaque frame la fonction _checkMove. Il serait logique que cette fonction soit lancée pour tous les joueurs, pour le ghost comme pour l'utilisateur. Pour cela, nous allons réécrire la fonction _checkMove. Rien de grave ou d'important, je vous rassure ! :p 

Premièrement, vous allez renommer cette fonction en _checkUniqueMove. Elle nous servira à vérifier chaque joueur présent dans la scène. Pour cela, nous allons re-créer la fonction _checkMove juste au-dessus. Elle aura pour travail de rendre tous les déplacements de tous les personnages.

_checkMove : function(ratioFps){
    // On bouge le player en lui attribuant la caméra
    this._checkUniqueMove(ratioFps,this.camera);
    for (var i = 0; i < this.ghostPlayers.length; i++) {
        // On bouge chaque ghost présent dans ghostPlayers
        this._checkUniqueMove(ratioFps,this.ghostPlayers[i]);
    }
},

Avec cette boucle, vous récupérez tout les ghostPlayer et vous allez pouvoir les bouger. Il va simplement falloir changer notre nouvellement renommée _checkUniqueMove pour faire en sorte que tous les player.camera soient changés pour prendre la valeur de notre deuxième paramètre passé.

_checkUniqueMove : function(ratioFps, player) {
        let relativeSpeed = this.speed / ratioFps;
        var playerSelected = player
        if(playerSelected.axisMovement){
            if(playerSelected.head){
                var rotationPoint = playerSelected.head.rotation;
            }else{
                var rotationPoint = playerSelected.playerBox.rotation;
            }
            if(playerSelected.axisMovement[0]){
                
                forward = new BABYLON.Vector3(
                    parseFloat(Math.sin(parseFloat(rotationPoint.y))) * relativeSpeed, 
                    0, 
                    parseFloat(Math.cos(parseFloat(rotationPoint.y))) * relativeSpeed
                );
                playerSelected.playerBox.moveWithCollisions(forward);
            }
------------------------ FAIRE DE MEME POUR LE RESTE ------------------------
    

Vous voyez aussi qu'on donne un paramètre en plus pour les ghostPlayer (on vérifie que c'en est effectivement en regardant si la variable player a une tête :p) qui est le déplacement de la "tête" du personnage. Ensuite, on change tous les player.camera par playerSelected qui aura pris la valeur de player ainsi que tous les playerSelected.playerBox.rotation par rotationPoint.

Voilà, notre fonction de mouvement est ajoutée ! Nous allons désormais passer à la fonction getDamage, qui va se voir offrir quelques modifications ! :) 

getDamage et playerDead

Actuellement, la fonction getDamage prend uniquement en compte les dommages subis. Nous devons faire en sorte de prévenir le réseau que notre personnage a été tué, et par qui ! Nous allons donc indiquer en deuxième paramètre de la fonction une variable whoDamage qui indiquera au réseau qui a touché le joueur. Cela va être particulièrement utile pour calculer le score total.

getDamage : function(damage, whoDamage){
    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;
    }
    // Prise des dégâts avec le tampon de l'armure
    if(this.camera.health>damageTaken){
        this.camera.health-=damageTaken;
    }else{
        // Envoi de la mort par le joueur
        this.playerDead(whoDamage)
    }
},

Dans le même temps, il va falloir rajouter deux lignes à la fonction playerDead pour demander à tous les joueurs d’effacer le joueur touché puis de le ré-afficher après un laps de temps défini. Vous pouvez d'ailleurs voir qu'on passe désormais à cette fonction le nom du personnage qui a touché le joueur.

Nous allons devoir ajouter tout en haut de la fonction un appel à une fonction dans NetworkManager qui porte le nom de sendPostMortem().

playerDead : function(whoKilled) {
        // Fonction appelée pour annoncer la destruction du joueur
        sendPostMortem(whoKilled);
---------------------------------------------------------------

Après voir annoncé la disparition du joueur, il faut annoncer son retour ! Vous allez créer une fonction qui va ressusciter le joueur chez les autres participants.

-----------------------------------------------------------------------
    setTimeout(function(){ 
        newPlayer._initCamera(newPlayer.game.scene, canvas, newPlayer.spawnPoint);
        newPlayer.launchRessurection();
    }, 4000);
},

La fonction, définie plus bas et que nous allons ajouter tout de suite, est similaire à sendNewData : elle ne sert qu'à transférer les données.

launchRessurection : function(){
    ressurectMe();
},

Avec cette fonction, NetworkManager va récupérer la position actuelle du joueur et faire réapparaître un ghost pour que les autres joueurs le voient au bon endroit.

Maintenant que nous pouvons régénérer le joueur, il va falloir ajouter une avant-dernière feature à ce fichier. Nous devons, comme je vous l'ai dit plus tôt, actualiser toutes les positions du joueur toutes les 4 secondes. Nous devons donc compiler les informations nécessaires dans un fichier et les envoyer au serveur, grâce à une fonction que l'on appellera sendActualData.

sendActualData

Cette fonction se résume en un simple envoi des données actuelles du joueur : un grand return avec les données triées dans un objet, organisées comme nous l'avons vu plus tôt.

sendActualData : function(){
    return {
        actualTypeWeapon : this.camera.weapons.actualWeapon,
        armor : this.camera.armor,
        life : this.camera.health,
        position  : this.camera.playerBox.position,
        rotation : this.camera.playerBox.rotation,
        axisMovement : this.camera.axisMovement
    }
},

Il nous reste une dernière fonction à créer. Nous envoyons et recevons les données des déplacements de tous les joueurs… mais nous les les traitons pas encore. Qu'à cela ne tienne, créons updateLocalGhost ! ^^ 

updateLocalGhost

Cette fonction a pour objectif de traiter les données reçues pour les affecter aux ghosts présents sur la scène. Pour cela, il faudra une boucle for qui déterminera le ghost à actualiser et lui assignera les données nécessaires.

updateLocalGhost : function(data){
    ghostPlayers = this.ghostPlayers;
    
    for (var i = 0; i < ghostPlayers.length; i++) {
        if(ghostPlayers[i].idRoom === data.id){
            var boxModified = ghostPlayers[i].playerBox;
            // On applique un correctif sur Y, qui semble être au mauvais endroit
            if(data.position){
                boxModified.position = new BABYLON.Vector3(data.position.x,data.position.y-2.76,data.position.z);
            }
            if(data.axisMovement){
                ghostPlayers[i].axisMovement = data.axisMovement;
            }
            if(data.rotation){
                ghostPlayers[i].head.rotation.y = data.rotation.y;
            }
            if(data.axisMovement){
                ghostPlayers[i].axisMovement = data.axisMovement;
            }
        }
        
    }
}

Au niveau du NetworkManager, vous allez pouvoir réactiver plusieurs fonctions ! 

socket.on('requestPosition', function(room){
    var dataToSend = [game._PlayerData.sendActualData(),personalRoomId];
    socket.emit('updateData',dataToSend);
});

socket.on ('updatePlayer', function (arrayData) {
    if(arrayData.id != personalRoomId){
        if(arrayData.ghostCreationNeeded){
            var newGhostPlayer = GhostPlayer(game,arrayData,arrayData.id);
            game._PlayerData.ghostPlayers.push(newGhostPlayer);
        }else{
            game._PlayerData.updateLocalGhost(arrayData);
        }
    }
});

// socket.on ('createGhostRocket', function (arrayData) {
//     if(arrayData[3] != personalRoomId){
//         game.createGhostRocket(arrayData);
//     }
// });

// socket.on ('createGhostLaser', function (arrayData) {
//     console.log(arrayData)
//     if(arrayData[2] != personalRoomId){
//         game.createGhostLaser(arrayData);
//     }
// });

socket.on ('giveDamage', function (arrayData) {
    if(arrayData[1] == personalRoomId){
        console.log('receive damage')
        game._PlayerData.getDamage(arrayData[0],arrayData[2]);
    }
    
});
socket.on ('killGhostPlayer', function (arrayData) {
    var idArray = arrayData[0];
    var roomScore = arrayData[1];
    if(idArray[0] != personalRoomId){
        deleteGameGhost(game,idArray[0]);
    }
    if(idArray[1] == personalRoomId){
        // game._PlayerData.newDeadEnnemy(idArray[2]);
    }
    // game.displayScore(roomScore);
});
socket.on ('ressurectGhostPlayer', function (idPlayer) {
    if(idPlayer != personalRoomId){
        deleteGameGhost(game,idPlayer);
    }
});
// socket.on ('deleteProps', function (deleteProp) {
//     game._ArenaData.deletePropFromServer(deleteProp)
// });
// socket.on ('recreateProps', function (createdProp) {
//     game._ArenaData.recreatePropFromServer(createdProp)
//     // console.log('Props re-created!' + createdProp);
// });

Avec tout cela, vous avez créé toutes les fonctions dont nous avons besoin dans Player ! Il nous reste une ou deux choses à voir dans Weapons.js et vous pourrez passer à l’intégration du HTML ! :)

Weapons.js 

L'objectif maintenant est d'envoyer les dégâts infligés au joueur touché. Cela se traduit par l'envoi des données de dégâts à notre cher ami NetworkManager, ainsi que la génération de roquettes et de tirs de laser pour tous les joueurs.

Autant le système de roquettes gère lui-même les dégâts, autant les trois autres types d'armes infligent des dégâts immédiatement. Nous allons donc dire au serveur qu'un joueur est touché si un joueur dit l'avoir touché, tout simplement.

Il vous faut donc aller dans les fonctions qui infligent des dégâts et que nous avions laissé vides pour y ajouter un appel à NetworkManager. Dans shootBullet, quand le tir semble avoir touché un joueur, nous allons ajouter :

// Cette condition if existe déjà
if(meshFound.hit && meshFound.pickedMesh.isPlayer){
    var damages = this.Armory.weapons[idWeapon].setup.damage;
    // On envoie les dégâts ainsi que l'ennemi trouvé grâce à son name
    sendDamages(damages,meshFound.pickedMesh.name)
}else{
    // L'arme ne touche pas de joueur
    console.log('Not Hit Bullet')
}

La forme est la même pour hitHand et createLaser. Vous devez simplement ajouter sendDamages avec les dégâts et le name du mesh trouvé. Pour hitHand par exemple :

if(meshFound.hit && meshFound.pickedMesh.isPlayer){
    var damages = this.Armory.weapons[idWeapon].setup.damage;
    sendDamages(damages,meshFound.pickedMesh.name)
}else{
    // L'arme frappe dans le vide
    console.log('Not Hit CaC')
}

Et pour createLaser :

if(meshFound.pickedMesh.isPlayer){
    var damages = this.Armory.weapons[idWeapon].setup.damage;
	sendDamages(damages,meshFound.pickedMesh.name)
}

Maintenant que les coups ont été répartis, il nous reste une dernière étape avant de rendre notre interface dynamique : sendGhostLaser et sendGhostRocket.

Envoi des ghosts des armes

Pour l'instant, les roquettes se créent dans Player. Cela signifie que l'on peut recevoir des roquettes dans l'interface sans même que le joueur soit vivant (le combat continue quand le joueur a été tué :p). Il va donc falloir envoyer les roquettes et les traits des lasers et les re-générer du côté des autres joueurs.

Premièrement, appelez la fonction sendGhostRocket dans createRocket, juste avant de push dans _rockets.

// On a besoin de la position, la rotation et la direction
sendGhostRocket(newRocket.position,newRocket.rotation,newRocket.direction);

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

C'est la même chose du coté de createLaser : on appelle la fonction sendGhostLaser juste avant le push dans _lasers.

// On envoie le point de départ et le point d'arrivée
sendGhostLaser(laserPosition,directionPoint.pickedPoint);

this.Player.game._lasers.push(line);

Maintenant que les deux fonctions sont appelées, il ne nous reste plus qu'à définir tout ça dans Game et c'est bon ! ^^ 

Game.js

Dans Game.js, nous allons créer les deux fonctions qui vont générer les ghosts des armes. Dès qu'ils seront créés, ils seront envoyés à rockets et à _lasers. Dans tous les cas, ces deux fonctions seront sensiblement les mêmes que celles que vous avez pu voir précédemment.

createGhostRocket : function(dataRocket) {
    var positionRocket = dataRocket[0];
    var rotationRocket = dataRocket[1];
    var directionRocket = dataRocket[2];
    var idPlayer = dataRocket[3];

    newRocket = BABYLON.Mesh.CreateBox('rocket', 0.5, this.scene);
    
    newRocket.scaling = new BABYLON.Vector3(1,0.7,2);

    newRocket.direction = new BABYLON.Vector3(directionRocket.x,directionRocket.y,directionRocket.z);

    newRocket.position = new BABYLON.Vector3(
        positionRocket.x + (newRocket.direction.x * 1) , 
        positionRocket.y + (newRocket.direction.y * 1) ,
        positionRocket.z + (newRocket.direction.z * 1));
    newRocket.rotation = new BABYLON.Vector3(rotationRocket.x,rotationRocket.y,rotationRocket.z);

    newRocket.scaling = new BABYLON.Vector3(0.5,0.5,1);
    newRocket.isPickable = false;
    newRocket.owner = idPlayer;

    newRocket.material = new BABYLON.StandardMaterial("textureWeapon", this.scene, false, BABYLON.Mesh.DOUBLESIDE);
    newRocket.material.diffuseColor = this.armory.weapons[2].setup.colorMesh;
    newRocket.paramsRocket = this.armory.weapons[2].setup;
    
    game._rockets.push(newRocket);
},
createGhostLaser : function(dataRocket){
    var position1 = dataRocket[0];
    var position2 = dataRocket[1];
    var idPlayer = dataRocket[2];

    let line = BABYLON.Mesh.CreateLines("lines", [
                position1,
                position2
            ], this.scene);
    var colorLine = new BABYLON.Color3(Math.random(), Math.random(), Math.random());
    line.color = colorLine;
    line.enableEdgesRendering();
    line.isPickable = false;
    line.edgesWidth = 40.0;
    line.edgesColor = new BABYLON.Color4(colorLine.r, colorLine.g, colorLine.b, 1);
    this._lasers.push(line);
},

La fonction createGhostRocket a un réel intérêt par sa variable owner. Il est facile de déterminer la propriété d'un tir effectué sur un joueur, mais il est plus complexe de définir celle d'un projectile qui se déplace dans le temps. C'est pour cela que nous donnons à ces roquettes l'id de leur propriétaire.

Du côté de createGhostLaser, il n'y a aucune différence puisque que les dégâts ont déjà été infligés précédemment.

Ah oui ! Je pensais que nous en avions fini, mais il nous reste maintenant une étape cruciale ! Il faut détecter qui a tiré a la roquette et qui a infligé les dégâts ! Direction le if de renderRockets dans lequel on vérifie si le joueur a été touché.

if (this._PlayerData.isAlive && this._PlayerData.camera.playerBox && explosionRadius.intersectsMesh(this._PlayerData.camera.playerBox)) {
    // Envoi à la fonction d'affectation des dégâts
    if(this._rockets[i].owner){
        var whoDamage = this._rockets[i].owner;
    }else{
        var whoDamage = false;
    }
    this._PlayerData.getDamage(paramsRocket.damage,whoDamage);
}

Et enfin, ré-activez les deux  socket.on createGhostRocket et createGhostLaser dans NetworkManager.

socket.on('requestPosition', function(room){
    var dataToSend = [game._PlayerData.sendActualData(),personalRoomId];
    socket.emit('updateData',dataToSend);
});

socket.on ('updatePlayer', function (arrayData) {
    if(arrayData.id != personalRoomId){
        if(arrayData.ghostCreationNeeded){
            var newGhostPlayer = GhostPlayer(game,arrayData,arrayData.id);
            game._PlayerData.ghostPlayers.push(newGhostPlayer);
        }else{
            game._PlayerData.updateLocalGhost(arrayData);
        }
    }
});

socket.on ('createGhostRocket', function (arrayData) {
    if(arrayData[3] != personalRoomId){
        game.createGhostRocket(arrayData);
    }
});

socket.on ('createGhostLaser', function (arrayData) {
    console.log(arrayData)
    if(arrayData[2] != personalRoomId){
        game.createGhostLaser(arrayData);
    }
});

socket.on ('giveDamage', function (arrayData) {
    if(arrayData[1] == personalRoomId){
        console.log('receive damage')
        game._PlayerData.getDamage(arrayData[0],arrayData[2]);
    }
    
});
socket.on ('killGhostPlayer', function (arrayData) {
    var idArray = arrayData[0];
    var roomScore = arrayData[1];
    if(idArray[0] != personalRoomId){
        deleteGameGhost(game,idArray[0]);
    }
    if(idArray[1] == personalRoomId){
        // game._PlayerData.newDeadEnnemy(idArray[2]);
    }
    // game.displayScore(roomScore);
});
socket.on ('ressurectGhostPlayer', function (idPlayer) {
    if(idPlayer != personalRoomId){
        deleteGameGhost(game,idPlayer);
    }
});
// socket.on ('deleteProps', function (deleteProp) {
//     game._ArenaData.deletePropFromServer(deleteProp)
// });
// socket.on ('recreateProps', function (createdProp) {
//     game._ArenaData.recreatePropFromServer(createdProp)
//     // console.log('Props re-created!' + createdProp);
// });

Et voilà ! Le jeu est officiellement connecté entre les joueurs ! Nous avons terminé l'aspect connexion !

  

Il ne nous reste plus qu'à gérer l'affichage des informations à l'écran ! Nous verrons cependant cela dans le chapitre suivant… À tout de suite ! ^^‌‌

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