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 !

Codez une première arme !

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

Maintenant que notre caméra se déplace dans la scène, il est temps de donner au joueur la possibilité d'avoir une arme bien à lui ! Nous allons commencer par créer un lance-roquettes, une arme emblématique des vieux jeux tels que Quake ou Doom ! Nous allons devoir :

  • Créer un modèle d'arme et l'attacher au joueur

  • Détecter le clic pour lancer une roquette

  • Déplacer la roquette à travers le temps

  • La faire exploser si elle rencontre un élément

Créons Weapons.js !

Aujourd'hui, notre code JavaScript est réparti en trois parties :  Game.js  qui gère les interactions principales,  Player.js qui s'occupe du joueur et de ses déplacements et enfin Arena.js  qui crée la zone de jeu.

Mais où gérer les armes ? Eh bien nous allons ajouter un fichier que nous allons nommer  Weapons.js ! Ajoutez-le dans le dossier  js et liez le script dans  index.html.

<!-- SCRIPTS BABYLON -->
<script src="js/Game.js"></script>
<script src="js/Player.js"></script>
<script src="js/Arena.js"></script>
<script src="js/Weapons.js"></script>
Weapons = function() {

};

Weapons.prototype = {

};

Instancions une première arme

Maintenant que tout ça est prêt, il est temps de se lancer dans les choses sérieuses ! Vous allez devoir créer une arme et l’attacher au joueur pour qu'elle le suive et qu'elle soit présente dans le bord bas-droit de l'écran, convention présente depuis très longtemps ! ^^

Pour commencer, nous allons appeler  Weapons à l'initialisation de la caméra et lui envoyer comme paramètre le  Player. Avec cet objet, nous disposons de tout ce dont nous pouvons avoir besoin à la création de l'arme !

_initCamera : function(scene, canvas) {
    // On crée la caméra
    this.camera = new BABYLON.FreeCamera("camera", new BABYLON.Vector3(-20, 5, 0), scene);
    
    // On demande a la caméra de regarder au point zéro de la scène
    this.camera.setTarget(BABYLON.Vector3.Zero());
    
    // Axe de mouvement X et Z
    this.camera.axisMovement = [false,false,false,false];

    // Appel de la création des armes
    this.camera.weapons = new Weapons(this);
},

Avec ces lignes, vous allez pouvoir récupérer l'objet  Player du côté de  Weapons.js et nous allons pouvoir nous plonger réellement dans la création de notre lance-roquettes !

Weapons = function(Player) {
    
};

Weapons.prototype = {

};

Dans  Weapons, nous allons définir un  Vector3 qui marquera la position de notre arme quand le joueur l'aura dans son inventaire (l'arme n'est pas active à ce stade) et une valeur qui changera le Y de ce Vector 3 quand l'arme sera prise en main (cela va nous être utile plus tard quand on changera d'arme ;)).

Weapons = function(Player) {
    // On permet d'accéder à Player n'importe où dans Weapons
    this.Player = Player;
    
    // Positions selon l'arme non utilisée
    this.bottomPosition = new BABYLON.Vector3(0.5,-2.5,1);

    // Changement de Y quand l'arme est séléctionnée
    this.topPositionY = -0.5;

    // Créons notre arme
    this.rocketLauncher = this.newWeapon(Player);

};

Puis nous appelons une fonction que nous créerons dans  prototype pour créer le mesh.

Pour la création de l'arme que nous allons faire, rien de bien incroyable pour l'instant. Nous allons ajouter un cube et l'attacher à la caméra. Pour cela, nous allons ajouter parent.

Weapons.prototype = {
    newWeapon : function(Player) {
        var newWeapon;
        newWeapon = BABYLON.Mesh.CreateBox('rocketLauncher', 0.5, Player.game.scene);

        // Nous faisons en sorte d'avoir une arme d'apparence plus longue que large
        newWeapon.scaling = new BABYLON.Vector3(1,0.7,2);

        // On l'associe à la caméra pour qu'il bouge de la même facon
        newWeapon.parent = Player.camera;

        // On positionne le mesh APRES l'avoir attaché à la caméra
        newWeapon.position = this.bottomPosition.clone();
        newWeapon.position.y = this.topPositionY;

        // Ajoutons un material Rouge pour le rendre plus visible
        var materialWeapon = new BABYLON.StandardMaterial('rocketLauncherMat', Player.game.scene);
        materialWeapon.diffuseColor=new BABYLON.Color3(1,0,0);

        newWeapon.material = materialWeapon;

        return newWeapon
    }
};

Nous voilà donc avec un cube rouge en bas de notre écran qui bouge en même temps que nous ! Et maintenant ? C'est le moment où on va le plus s'amuser, on va faire tirer notre lance-roquettes ! 

Prêt ? En joue, feu !

Il va nous falloir faire plusieurs choses différentes avant de pouvoir tirer réellement avec notre lance-roquettes ! Il va falloir détecter le clic de la souris, mais aussi affecter une cadence à notre arme (cette arme n'est pas une mitrailleuse, eh oui ! ;)) et donc autoriser le lancement d'une roquette uniquement quand c'est possible. 

Ajoutons la détection des clics dans la scène

Direction  Player.js  ! Nous allons ajouter ici la détection de nos clics dans la scène et vérifier s'ils sont bien associés au tir. Première chose : ajoutez une variable  weaponShoot au tout début de l'objet  Player qui déterminera si le joueur peut tirer ou non.

// Si le tir est activée ou non
this.weponShoot = false;

Maintenant, il faut faire la différence entre le tir de l'arme et le joueur qui veut cliquer dans la scène pour verrouiller sa souris. Quand il sera possible de tirer, nous appellerons donc les deux fonctions  handleUserMouseDown et  handleUserMouseUp.

// On récupère le canvas de la scène 
var canvas = this.game.scene.getEngine().getRenderingCanvas();

// On affecte le clic et on vérifie qu'il est bien utilisé dans la scène (_this.controlEnabled)
canvas.addEventListener("mousedown", function(evt) {
    if (_this.controlEnabled && !_this.weponShoot) {
        _this.weponShoot = true;
        _this.handleUserMouseDown();
    }
}, false);

// On fait pareil quand l'utilisateur relache le clic de la souris
canvas.addEventListener("mouseup", function(evt) {
    if (_this.controlEnabled && _this.weponShoot) {
        _this.weponShoot = false;
        _this.handleUserMouseUp();
    }
}, false);

Avec cela, nous allons ajouter les fonctions à la fin du prototype de  Player qui vont appeler les fonctions nécessaires pour tirer directement dans  Weapons.js.

handleUserMouseDown : function() {
    if(this.isAlive === true){
        this.camera.weapons.fire();
    }
},
handleUserMouseUp : function() {
    if(this.isAlive === true){
        this.camera.weapons.stopFire();
    }
},

On détecte si le joueur est en vie. Si c'est le cas, le tir va s'activer.

Comme vous le voyez, on regarde si le joueur est en vie. Il faut donc définir cela depuis  _initCamera. :) 

_initCamera : function(scene, canvas) {
    // On crée la caméra
    this.camera = new BABYLON.FreeCamera("camera", new BABYLON.Vector3(-20, 5, 0), scene);
    
    // On demande a la caméra de regarder au point zéro de la scène
    this.camera.setTarget(BABYLON.Vector3.Zero());
    
    // Axe de mouvement X et Z
    this.camera.axisMovement = [false,false,false,false];

    // Appel de la création des armes
    this.camera.weapons = new Weapons(this);

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

Retournez maintenant dans  Weapons.js pour la suite ! 

Régler la cadence de tir et savoir quand une roquette est tirée

Pour pouvoir tirer, il faut savoir quand cela est possible selon les spécificités de l'arme.

Vous allez donc ajouter plusieurs lignes à l'objet  Weapons, juste après avoir appelé this.newWeapon().

// Cadence de tir
this.fireRate = 800;

// Delta de calcul pour savoir quand le tir est a nouveau disponible
this._deltaFireRate = this.fireRate;

// Variable qui va changer selon le temps
this.canFire = true;

// Variable qui changera à l'appel du tir depuis le Player
this.launchBullets = false;

// _this va nous permettre d'acceder à l'objet depuis des fonctions que nous utiliserons plus tard
var _this = this;

// Engine va nous être utile pour la cadence de tir
var engine = Player.game.scene.getEngine();

Toutes ces variables vont avoir une utilité pour connaître quand est-ce que l'utilisateur va pouvoir tirer. Dans un jeu, si vous maintenez le clic de la souris, l'arme va automatiquement tirer plusieurs coups. Il faut donc déterminer quand est-ce que l'arme est prête à tirer à nouveau, ce que nous allons déterminer quand un certain temps s'est écoulé.

En dessous de toutes ces nouvelles variables, nous allons donc ajouter les lignes suivantes.

Player.game.scene.registerBeforeRender(function() {
    if (!_this.canFire) {
        _this._deltaFireRate -= engine.getDeltaTime();
        if (_this._deltaFireRate <= 0  && _this.Player.isAlive) {
            _this.canFire = true;
            _this._deltaFireRate = _this.fireRate;
        }
    }
});

En premier lieu, on vérifie si le joueur peut tirer. Si ce n'est pas le cas, on va utiliser  _deltaFireRate qui détermine le temps à attendre avant le prochain coup. On va décrémenter cette valeur à chaque frame. Quand la valeur arrive à zéro, c'est que l'arme est de nouveau prête à tirer.

Il vous faut maintenant changer cette fameuse variable  canFire pour prendre en compte la  cadence. Nous allons d'abord ajouter les fonctions à prototype pour lier  Player et  Weapons quand un clic s'effectue. Vous vous souvenez, nous appelons  fire et  stopFire dans  Player, alors créons-les ! ^^

fire : function(pickInfo) {
    this.launchBullets = true;
},
stopFire : function(pickInfo) {
    this.launchBullets = false;
},

C'est tout ? Il n'y a que cela dans les fonctions ? 

Eh oui, cela nous permet une découpe plus propre des événements et des appels de fonctions dans les différents fichiers. Dans l'ordre, un clic est enregistré. On l'envoie à  Weapons, qui change la variable  lauchBullets. S'il est possible de tirer, nous tirons. Sinon, le _deltFireRate va diminuer jusqu’à arriver à zéro et nous permettre de tirer.

Il nous manque deux choses ici pour finaliser la détection du tir :

  • détecter toutes les frames si le tir est actif ou non et s'il est possible de tirer ;

  • une fonction qui tire et crée la roquette.

Nous allons commencer par la fonction qui crée la fameuse roquette. Dans le prototype de  Weapons, nous allons mettre juste après  stopFire()  la fonction suivante :

launchFire : function() {
    if (this.canFire) {
        console.log('Pew !');
        this.canFire = false; 
    } else {
        // Nothing to do : cannot fire
    }
}

Cette fonction va pour l'instant simplement envoyer à la console du navigateur le texte "Pew !" à chaque fois qu'on pourra tirer. 

Cependant, il nous manque une ultime chose à faire : ajouter la détection du tir à la boucle de rendu de  Game.js. Allez donc dans cet objet pour modifier  engine.runRenderLoop. La boucle devra finalement ressembler à ceci :

// 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);

    _this.scene.render();
    
    // Si launchBullets est a true, on tire
    if(_player.camera.weapons.launchBullets === true){
        _player.camera.weapons.launchFire();
    }
});

Et voilà ! Si vous actualisez votre page et ouvrez votre console, vous verrez le message "Pew !" s'afficher dès que vous tirerez et qu'il était possible de le faire ! Bon, pour l'instant ça n'est que du texte dans la console, mais on approche de la fin ! Allez, il est temps de créer cette fameuse roquette !

Créer la roquette !

En premier lieu, nous allons créer une fonction que nous appellerons  createRocket. Je vais écrire ici la totalité du code pour créer la roquette et l'instancier. Et ensuite, je vais vous détailler tout cela pas à pas. ;)

launchFire : function() {
    if (this.canFire) {
        var renderWidth = this.Player.game.engine.getRenderWidth(true);
        var renderHeight = this.Player.game.engine.getRenderHeight(true);
        
        var direction = this.Player.game.scene.pick(renderWidth/2,renderHeight/2);
        direction = direction.pickedPoint.subtractInPlace(this.Player.camera.position);
        direction = direction.normalize();

        this.createRocket(this.Player.camera,direction)
        this.canFire = false; 
    } else {
        // Nothing to do : cannot fire
    }
},
createRocket : function(playerPosition, direction) {
    var positionValue = this.rocketLauncher.absolutePosition.clone();
    var rotationValue = playerPosition.rotation; 
    var newRocket = BABYLON.Mesh.CreateBox("rocket", 1, this.Player.game.scene);
    newRocket.direction = new BABYLON.Vector3(
        Math.sin(rotationValue.y) * Math.cos(rotationValue.x),
        Math.sin(-rotationValue.x),
        Math.cos(rotationValue.y) * Math.cos(rotationValue.x)
    )
    newRocket.position = new BABYLON.Vector3(
        positionValue.x + (newRocket.direction.x * 1) , 
        positionValue.y + (newRocket.direction.y * 1) ,
        positionValue.z + (newRocket.direction.z * 1));
    newRocket.rotation = new BABYLON.Vector3(rotationValue.x,rotationValue.y,rotationValue.z);
    newRocket.scaling = new BABYLON.Vector3(0.5,0.5,1);
    newRocket.isPickable = false;

    newRocket.material = new BABYLON.StandardMaterial("textureWeapon", this.Player.game.scene);
    newRocket.material.diffuseColor = new BABYLON.Color3(1, 0, 0);
},

À la création de la roquette, nous lui passons comme paramètre la caméra qui va donner à notre objet la rotation désirée. Pour connaître le sens de déplacement, nous déterminons une direction en effectuant 4 étapes.

  • On capte le centre de l'écran avec  getRenderWidth et getRenderHeight.

  • On détermine le lieu pointé par  pick, fonction qui nous permet de définir depuis l'écran un endroit à pointer.

  • On soustrait au point ciblé par  pick notre position actuelle (ce qui nous donne un vecteur de déplacement).

  • On normalise ce vecteur (c'est-à-dire qu'on oblige à faire un ratio des valeurs pour que le maximum sur chaque axe soit 1). Cela nous donne notre direction de déplacement.

On donne en plus de cela un  material rouge, et notre roquette est créée ! Elle est pour l'instant fixe, mais nous allons la faire bouger juste après ! ^^

Faire avancer les roquettes

Maintenant que nos roquettes sont créées, nous avons fait le plus dur ! Les faire bouger est une histoire d'une dizaine de lignes. Tout d’abord, nous allons définir à l'intérieur même de  createRocket la fonction registerAfterRender, qui va agir après le mouvement de tous les éléments de la scène. 

// On donne accès à Player dans registerBeforeRender
var Player = this.Player;
	    
newRocket.registerAfterRender(function(){
    // On bouge la roquette vers l'avant
    newRocket.translate(new BABYLON.Vector3(0,0,1),1,0);
    
    // On crée un rayon qui part de la base de la roquette vers l'avant
    var rayRocket = new BABYLON.Ray(newRocket.position,newRocket.direction);
    
    // On regarde quel est le premier objet qu'on touche
    var meshFound = newRocket.getScene().pickWithRay(rayRocket);
    
    // Si la distance au premier objet touché est inférieure a 10, on détruit la roquette
    if(!meshFound || meshFound.distance < 10){
        newRocket.dispose();
    }
})

Mais ? Avant on a placé ce mesh avec une formule mathématique imbuvable alors qu'on peut le placer aussi facilement que ce qui est indiqué dans la fonction  translate ?

Oui, et non. Évidemment, ce n'est pas si simple que ça. ;)

translate nous permet uniquement de déplacer l'objet et de choisir dans quel repère le faire (le vecteur indique le sens, le deuxième paramètre la quantité de déplacement et le dernier paramètre si le repère est local ou global).

Local et Global ? Qu'est-ce que ça veut dire ?

Les repères local et global sont deux types de repères. Vous vous souvenez quand je vous ai dit que quand on bougeait un objet sur Y, ça le faisait monter ? Eh bien c'est le cas, mais dans le repère Global. Le repère Local est le repère associé à l'objet. Si l'objet est tourné à 90 degrés, son axe de déplacement aussi. Et tous les axes locaux seront tournés dans la même direction. 

Pour reprendre donc, avec ce rayon nous déterminons la distance restant à la roquette pour exploser contre quelque chose. Si la distance atteint la valeur requise, elle est détruite avec  dispose. Nous vérifions aussi si un objet est trouvé. Si ce n'est pas le cas, la roquette disparaît de la même façon. Cela permet d'éviter à des roquettes tirées depuis l'arrière de murs de faire planter l'application.

Et nous voilà avec des roquettes qui avancent toutes seules ! Modifiez la vitesse de celles-ci pour voir l'effet que cela leur donne, puis nous allons passons à la dernière partie : leurs explosions !

EXPLOSIONS !

Bien, nous avons fait de jolies roquettes qui avancent et se détruisent si elles rencontrent un obstacle. Et qu'est-ce qu'on fait maintenant ? Eh bah on les fait exploser ! :diable: 

Pour ajouter les explosions, il nous suffit d'ajouter un mesh quand la roquette touche un mur et coller ce mesh à notre élément rencontré. Nous allons donc ajouter un  registerAfterRender dans le  registerAfterRender de la roquette. ^^

if(!meshFound || meshFound.distance < 10){
    // On vérifie qu'on a bien touché quelque chose
    if(meshFound.pickedMesh){
        // On crée une sphere qui représentera la zone d'impact
        var explosionRadius = BABYLON.Mesh.CreateSphere("sphere", 5.0, 20, Player.game.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", Player.game.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();
            }
        });
    }
    newRocket.dispose();
}

Avec ces lignes, nos roquettes se déplacent et il y a bien un impact quand elles touchent quelque chose.

  

Maintenant que vous avons ceci, plus rien ne vous arrête pour la suite ! Vous avez passé les parties les plus complexes à comprendre. Il reste cependant encore beaucoup de choses à voir, et ce ne sera pas forcément simple, mais vous allez très bien vous en sortir ! Notre première arme est finie, nous pouvons nous concentrer désormais sur l’environnement ! À tout de suite ! :)‌ 

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