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 !

Préparez le décor du jeu

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

Nous avons une arme, une jolie arène et un personnage qui se déplace avec les touches du clavier. Super ! Mais il nous manque deux ou trois choses. Comme quoi ? Eh bien, pour l'instant, nous n'avons aucune ombre et pas de collisions avec l'environnement (on ne peut pas rentrer dans un mur ou un poteau !). Nous allons créer ces deux choses dans ce chapitre. ;)

Il est temps de se prendre les murs !

L'ajout des collisions est techniquement très simple, mais il va nous demander l'ajout de pas mal de lignes de code ! Que ce soit dans Game, Player ou Arena, il y a des modifications à apporter. C'est parti !

Game.js, activez la gravité

La première étape est d'activer les collisions et la gravité. Cela va nous permettre pour l'instant de détecter les murs, et plus tard de permettre à notre personnage de sauter en l'air ! Pour cela, allez dans Game.js ajouter deux petites lignes dans _initScene().

// Prototype d'initialisation de la scène
_initScene : function(engine) {
    var scene = new BABYLON.Scene(engine);
    scene.clearColor=new BABYLON.Color3(0.9,0.9,0.9);
    scene.gravity = new BABYLON.Vector3(0, -9.81, 0);
    scene.collisionsEnabled = true;
    return scene;
}
  • gravity définit le sens dans lequel les personnages vont chuter. Ici, j'ai mis -9.81 sur l'axe Y, car c'est la valeur communément acquise pour la Terre.

  • collisionsEnabled dit au moteur physique de calculer les collisions.

Maintenant, il faut indiquer aux objets qu'ils doivent vérifier les contacts avec les autres meshes.

Arena.js, les murs et les poteaux !

C'est ce que nous allons faire à partir de Arena.js. Vous allez pouvoir dire à notre arène d'activer les collisions pour les meshes qui se trouvent sur le chemin du personnage (ici, les meshes en question seront les colonnes et les murs). Il vous suffit d'ajouter  checkCollisions = true aux meshes concernés.

var boxArena = BABYLON.Mesh.CreateBox("box1", 100, scene, false, BABYLON.Mesh.BACKSIDE);
boxArena.material = materialGround;
boxArena.position.y = 50 * 0.3;
boxArena.scaling.y = 0.3;
boxArena.scaling.z = 0.8;
boxArena.scaling.x = 3.5;

boxArena.checkCollisions = true;

Ici, je l'ai fait pour boxArena à la ligne 8. Les collisions sont donc activées sur les murs ! À vous de faire de même avec tous les objets de la boucle for pour que les colonnes soit aussi prises en compte dans le moteur physique. ^^

Player.js, c'est là que les choses se compliquent un peu

Maintenant que l'on a activé la collision sur les objets de la scène, il est temps de dire au player de rentrer en contact avec son environnement. Cependant, pour le faire proprement, il va nous falloir retravailler une bonne partie du code.

Pourquoi ? On ne pourrait pas simplement ajouter les collisions sur la caméra ?

Nous pourrions, mais nous voulons pouvoir avoir un contrôle total sur le Player ! L'objectif est d'explorer les possibilités de la gravité et de paramétrer nous même le maximum de choses possibles ;) 

Dans l'ordre, voici ce que nous allons faire :

  • Ajouter une box qui sera le parent de la caméra

  • Activer les collisions sur ce mesh

  • Bouger cette box à la place de la caméra

  • Faire constamment tomber cette box vers le bas pour simuler la gravité 

On va donc réécrire pas mal de choses !

Ajouter une box qui sera le parent de la caméra

Procédons par ordre et attaquons-nous à  _initCamera. Pour commencer, nous devons créer un objet que nous allons appeler  playerBox qui servira à déplacer le joueur.

var playerBox = BABYLON.Mesh.CreateBox("headMainPlayer", 3, scene);
playerBox.position = new BABYLON.Vector3(-20, 5, 0);
playerBox.ellipsoid = new BABYLON.Vector3(2, 2, 2);

ellipsoid est une valeur qui nous intéresse ici pour déterminer à quelle hauteur le regard sera et quelle sera la taille de la box autour du joueur pour détecter les collisions.

Activer les collisions sur ce mesh

Maintenant, on re-crée la caméra juste en dessous.

// On crée la caméra
this.camera = new BABYLON.FreeCamera("camera", new BABYLON.Vector3(0, 0, 0), scene);
this.camera.playerBox = playerBox
this.camera.parent = this.camera.playerBox;

// Ajout des collisions avec playerBox
this.camera.playerBox.checkCollisions = true;
this.camera.playerBox.applyGravity = true;

// Si le joueur est en vie ou non
this.isAlive = true;

// Pour savoir que c'est le joueur principal
this.camera.isMain = true;

// On crée les armes !
this.camera.weapons = new Weapons(this);

// On ajoute l'axe de mouvement
this.camera.axisMovement = [false,false,false,false];

var hitBoxPlayer = BABYLON.Mesh.CreateBox("hitBoxPlayer", 3, scene);
hitBoxPlayer.parent = this.camera.playerBox;
hitBoxPlayer.scaling.y = 2;
hitBoxPlayer.isPickable = true;
hitBoxPlayer.isMain = true;

Appliquez checkCollisions mais aussi applyGravity : cela va permettre de calculer plus tard le déplacement suivant la gravité définie dans la scène. On laisse le setTarget déjà présent ainsi que new  Weapons qui ne changent pas.

Bouger cette box à la place de la caméra

Nous en avons aussi profité pour ajouter un élément essentiel pour la détection des dégâts : la hitBoxPlayer. Elle fait la hauteur du joueur et permet de capter tous les événements aux alentours du joueur.

Ensuite, il faut dire à l'event mousemove dans Player de tourner non plus la caméra mais bien playerBox. Pour cela, vous devez simplement remplacer _this.camera.rotation par _this.camera.playerBox.rotation.

// 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);
        }
    }
}, false);

Il nous reste une dernière étape dans ce fichier : changer _checkMove.

Cette fonction se base pour l'instant sur un déplacement normal, sans tenir compte des collisions. Nous allons donc utiliser moveWithCollisions qui fonctionne de la même façon que ce que vous avez fait précédemment. Notre fonction va finalement ressembler à ça :

_checkMove : function(ratioFps) {
    let relativeSpeed = this.speed / ratioFps;
    if(this.camera.axisMovement[0]){
        forward = new BABYLON.Vector3(
            parseFloat(Math.sin(parseFloat(this.camera.playerBox.rotation.y))) * relativeSpeed, 
            0, 
            parseFloat(Math.cos(parseFloat(this.camera.playerBox.rotation.y))) * relativeSpeed
        );
        this.camera.playerBox.moveWithCollisions(forward);
    }
    if(this.camera.axisMovement[1]){
        backward = new BABYLON.Vector3(
            parseFloat(-Math.sin(parseFloat(this.camera.playerBox.rotation.y))) * relativeSpeed, 
            0, 
            parseFloat(-Math.cos(parseFloat(this.camera.playerBox.rotation.y))) * relativeSpeed
        );
        this.camera.playerBox.moveWithCollisions(backward);
    }
    if(this.camera.axisMovement[2]){
        left = new BABYLON.Vector3(
            parseFloat(Math.sin(parseFloat(this.camera.playerBox.rotation.y) + degToRad(-90))) * relativeSpeed, 
            0, 
            parseFloat(Math.cos(parseFloat(this.camera.playerBox.rotation.y) + degToRad(-90))) * relativeSpeed
        );
        this.camera.playerBox.moveWithCollisions(left);
    }
    if(this.camera.axisMovement[3]){
        right = new BABYLON.Vector3(
            parseFloat(-Math.sin(parseFloat(this.camera.playerBox.rotation.y) + degToRad(-90))) * relativeSpeed, 
            0, 
            parseFloat(-Math.cos(parseFloat(this.camera.playerBox.rotation.y) + degToRad(-90))) * relativeSpeed
        );
        this.camera.playerBox.moveWithCollisions(right);
    }
    this.camera.playerBox.moveWithCollisions(new BABYLON.Vector3(0,(-1.5) * relativeSpeed ,0));
}

 Vous reconnaissez la forme déjà utilisée précédemment ? Il n'y a rien de nouveau, vous avez juste appelé une fonction différente pour définir le déplacement. ;)

Et le moveWithCollisions à la fin ?

Il correspond à notre chute due à la gravité ! Comme vous le voyez, il dirige toutes les frames vers le bas.

Il nous manque plus qu'un fichier à changer et notre code sera bon !

Weapons.js, vérifier la trajectoire des roquettes

Les trajectoires des roquettes sont centrées sur la caméra. Cependant, maintenant, ce n'est plus la caméra mais playerBox qui bouge ! Vous n'avez qu'une toute petite modification à faire dans les fonctions launchFire() et createRocket().

Dans launchFire, changez l'appel à  createRocket pour qu'il récupère  this.Player.camera.playerBox à la place de this.Player.camera. Ainsi, vous envoyez bien les positions de playerBox.

Dans createRocket, on ne renvoie qu'une valeur que nous appellerons playerPosition. Et nous réaffecterons positionValue et  rotationValue de la manière suivante :

launchFire : function() {
    if (this.canFire) {
        // console.log('Pew !');
        
        this.createRocket(this.Player.camera.playerBox)
        this.canFire = false; 
    } else {
        // Nothing to do : cannot fire
    }
},
createRocket : function(playerPosition) {
    var positionValue = this.rocketLauncher.absolutePosition.clone();
    var rotationValue = playerPosition.rotation; 
    var Player = this.Player;
    var newRocket = BABYLON.Mesh.CreateBox("rocket", 1, Player.game.scene);
------------------------ SUITE DE CREATEROCKET ----------------

Enregistrez et actualisez votre navigateur. Et voila ! Vous vous prenez tous les murs ? Félicitations ! Avouez que vous ne pensiez jamais être aussi content de vous prendre tous les murs, non ? :lol:

De l'ombre je vous prie !

Bon, je vous préviens, on arrive dans un monde où on fait des calculs sacrément balèzes ! Enfin, pas nous, seulement l’ordinateur. :p

Pour pouvoir jouer avec les ombres, désactivez le dispose() présent dans Weapons.js pour le mesh  explosionRadius et le mesh newRocket. Je vous laisse faire !

Tamiser les lumières et générer des ombres

Pour ajouter des ombres et les rendre visible, nous allons tamiser un peu la lumière ambiante ! En route pour Arena.js  ! C'est dans ce fichier que tout va se passer. ^^ Nous allons écrire les lignes suivantes juste après avoir défini les lumières.

// Création de notre lumière principale
var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 10, 0), scene);
var light2 = new BABYLON.HemisphericLight("light2", new BABYLON.Vector3(0, -1, 0), scene);
light2.specular = new BABYLON.Color3(0,0,0);

light.intensity = 0.2;
light2.intensity = 0.2;

Après avoir diminué les lumières, nous allons ajouter une lumière de type PointLight. Ces lumières sont extrêmement pratiques pour faire des ombres très sympa.

var light3 = new BABYLON.PointLight("Spot0", new BABYLON.Vector3(-40, 10, -100), scene);
light3.intensity = 0.3;
light3.specular = new BABYLON.Color3(0,0,0);

Comme pour les lumières précédentes, diminuez l'intensité et enlevez la lumière spéculaire. Maintenant que nous avons nos lumières, il est temps de dire à l'arène de générer les ombres ! Et pour cela, il faut créer un Générateur d'ombres. Celui-ci va s'occuper de distribuer la lumière selon les objets présents sur le chemin.

var shadowGenerator1 = new BABYLON.ShadowGenerator(2048, light3);
shadowGenerator1.usePoissonSampling = true;
shadowGenerator1.bias = 0.0005;

Le premier chiffre en paramètre (2048) est la qualité des ombres. Ce chiffre n'a pas de maximum et son minimum est bien évidemment zéro. Mais vous devez comprendre que votre ordinateur ne va pas forcément apprécier si vous méttez une définitions d'ombres trop haute ;) . Puis nous affectons au shadowGenerator1 la lumière que nous venons de créer et nous lui demandons d'utiliser du poissonSampling, une méthode d'échantillonnage de la texture d'ombre. bias nous permet de définir la précision des ombres rendues.

Sélectionner les objets qui vont recevoir les ombres

Apres cela, il nous reste à dire au système quels sont les objets qui vont recevoir les ombres, lesquels vont en générer et augmenter le nombres de lumières maximales reçues pour les meshes.

Dans notre cas, le sol va recevoir des ombres ; les colonnes vont aussi en recevoir, mais surtout en générer !

Pour dire au mesh de recevoir les ombres, utilisez la formule suivante :

boxArena.receiveShadows = true;

Pour dire au mesh de générer des ombres, il faut le push dans la renderList de la shadowMap. Ce n'est pas clair ? Il faut utiliser la formule suivante dans la boucle for utilisée pour générer les colonnes :

for (var i = 0; i <= 1; i++) {
    if(numberColumn>0){
        columns[i] = [];
        let mainCylinder = BABYLON.Mesh.CreateCylinder("cyl0-"+i, 30, 5, 5, 20, 4, scene);
        mainCylinder.position = new BABYLON.Vector3(-sizeArena/2,30/2,-20 + (40 * i));
        mainCylinder.material = materialWall;
        mainCylinder.checkCollisions = true;
        
        
        // La formule pour recevoir plus de lumières
        mainCylinder.maxSimultaneousLights = 10;
        // La formule pour générer des ombres
        shadowGenerator1.getShadowMap().renderList.push(mainCylinder);
        // La formule pour recevoir des ombres
        mainCylinder.receiveShadows = true;
        
        
        columns[i].push(mainCylinder);

        if(numberColumn>1){
            for (let y = 1; y <= numberColumn - 1; y++) {
                let newCylinder = columns[i][0].clone("cyl"+y+"-"+i);
                newCylinder.position = new BABYLON.Vector3(-(sizeArena/2) + (ratio*y),30/2,columns[i][0].position.z);
                newCylinder.checkCollisions = true;
                newCylinder.maxSimultaneousLights = 10;
                shadowGenerator1.getShadowMap().renderList.push(newCylinder);
                newCylinder.receiveShadows = true;
                columns[i].push(newCylinder);
            }
        }
    }
}

 Nous y voilà ! Des ombres sont désormais générées dans notre scène : elle est plus que prête !

C'est quand même plutôt joli ce qu'on a fait, hein?
C'est quand même plutôt joli ce qu'on a fait, hein ?

… Mais il nous manque encore plusieurs choses ! Certes, nous avons vu plein de nouvelles fonctionnalités, mais notre personnage ne peut pas encore mourir ni vaincre d'ennemis, il n'a pas de vie et il n'y a qu'une seule arme... Et il y a un problème d'ordre majeur pour un FPS... Il n'y a pas d'ennemis !! 

Il va nous falloir remédier à tout cela, et nous ferons ça dans la prochaine partie ! ;) Félicitations à vous pour ce que vous avez déjà fait ! C'est un sacré morceau qu'on a vu ici. Il est temps pour un petit quiz avant de continuer. On se retrouve après ! ^^ ‌‌

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