Shooter ! - A FPS prototype

Shooter ! - A FPS prototype

Objective

This tutorial will explain how to create a FPS game with Babylon.js. Here are several things (amongst others) we will talk about :

  • Asset loader
  • Camera viewport
  • Camera layer masks
  • Pointer lock API
Don't be afraid by all this, I will try to be as clearer as possible. And if you have any questions, my Twitter is at the bottom of the page ;)

What you will create

A fully working FPS ! With a gun and a gunsight, a camera you can move with ZQSD (or WASD), some targets and a minimap.
You can click on the image below or here to try it.

The class diagram

Our game will have several javascript classes, as described in the little diagram below :

  • The class Game will be the main class, and will be in charge of loading all models, creating the environment, the player and the arena.
  • The class Player will represent the player: the camera, and all users actions (click and movement.
  • The class Weapon will represent our weapon.
  • The class Arena will be the physical environment: the ground and the world our player will live in.
  • The class Target will be anything that can be shot :)

The starting file

I updated my starting project to make it more... classy. You can download it by clicking here. I use the last version of babylon (1.14), and several actions are already done in the Game class.

The assets loader

The version 1.14 of Babylon has been shipped with a new cool feature: the Assets Manager. Its behaviour is well explained in the Babylon wiki.
This feature can be used to load a complete scene, but we will use it to preload all of our 3D models, and store it in memory (like I explain it in my last tutorial).
Here is the code:


this.loader =  new BABYLON.AssetsManager(this.scene);

// An array containing the loaded assets
this.assets = {};

var _this = this;
var meshTask = this.loader.addMeshTask("gun", "", "./assets/", "gun.babylon");
meshTask.onSuccess = function(task) {
    _this._initMesh(task);
};

The loader object is first created. Then, all tasks are added to the loader (I only have one 3D model to load). The 'onSuccess' function precised in my loading task will add the loaded mesh in my array 'assets', like it is described below.


/**
* Initialize a mesh once it has been loaded. Store it in the asset array and set it not visible.
* @param task
* @private
*/
_initMesh : function(task) {
    this.assets[task.name] = task.loadedMeshes;
    for (var i=0; i<task.loadedMeshes.length; i++ ){
        var mesh = task.loadedMeshes[i];
        mesh.isVisible = false;
    }
}

Finally, a 'onFinish' function is added to the loader. This function will create the player object, the arena, and create the render loop.
Don't forget to start the loader with the 'load' method :)


this.loader.onFinish = function (tasks) {

    // Player and arena creation when the loading is finished
    var player = new Player(_this);
    var arena = new Arena(_this);

    engine.runRenderLoop(function () {
        _this.scene.render();
    });
};

this.loader.load();

The Player

In a classic FPS game, the player is only represented by a camera that can be moved with ZQSD(WASD) keys, and rotated with the mouse. Luckily, we have the exact same object in Babylon: a FreeCamera ! This camera needs to be tweaked a little bit, but nothing complicated.
All the code below is in the class constructor:


// The player eyes height
this.height = 2;
// The player speed
this.speed = 1;
// The player inertia
this.inertia = 0.9;
// The mouse sensibility (lower is most sensible)
this.angularSensibility = 1000;
// The player camera
this.camera = this._initCamera();
    ...
/**
* Init the player camera
* @returns {BABYLON.FreeCamera}
* @private
*/
_initCamera : function() {
    var cam = new BABYLON.FreeCamera("camera", this.spawnPoint, this.scene);
    cam.attachControl(this.scene.getEngine().getRenderingCanvas());
    cam.ellipsoid = new BABYLON.Vector3(2, this.height, 2);
    // Activate collisions
    cam.checkCollisions = true;
    // Activate gravity !
    cam.applyGravity = true;

    // Remap keys to move with ZQSD
    cam.keysUp = [90]; // Z
    cam.keysDown = [83]; // S
    cam.keysLeft = [81]; // Q
    cam.keysRight = [68]; // D
    cam.speed = this.speed;
    cam.inertia = this.inertia;
    cam.angularSensibility = this.angularSensibility;
    return cam;
}

If you run your game now, your player will fall... You need to create a ground the camera can move on... But we will create it later in the Arena class. Now, we need two things: a gunsight, and make the camera rotate without the user to click on the canvas.

The gunsight

The gunsight is only an image in the center of the screen. No javascript here :)


<img id="gunsight" src="assets/viseur.png" />

#gunsight {
    position:absolute;
    top:50%;
    left:50%;
    margin-top:-37px;
    margin-left:-37px;
}

The Pointer Lock API

In order to take control of the user mouse and remove his cursor, we must use the Pointer Lock API. This API provides some useful methods to lock the mouse, as described in the offical documentation: locks the target of mouse events to a single element, eliminates limits on how far mouse movement can go in a single direction, and removes the cursor from view. Exactly what we want to do !
Be aware the 'locked' state can be removed by pressing the Escape key, and we want to handle that too.

The only thing to pay attention to is that the user must activate himself the pointer lock. Our game cannot start with it. Let's try it on a 'click' event.


_initPointerLock : function() {
    var _this = this;
    // Request pointer lock
    var canvas = this.scene.getEngine().getRenderingCanvas();
    // On click event, request pointer lock
    canvas.addEventListener("click", function(evt) {
        canvas.requestPointerLock = canvas.requestPointerLock || canvas.msRequestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock;
        if (canvas.requestPointerLock) {
            canvas.requestPointerLock();
        }
    }, false);

    // Event listener when the pointerlock is updated (or removed by pressing ESC for example).
    var pointerlockchange = function (event) {
        _this.controlEnabled = (
                           document.mozPointerLockElement === canvas
                        || document.webkitPointerLockElement === canvas
                        || document.msPointerLockElement === canvas
                        || document.pointerLockElement === canvas);
        // If the user is alreday locked
        if (!_this.controlEnabled) {
            _this.camera.detachControl(canvas);
        } else {
            _this.camera.attachControl(canvas);
        }
    };

    // Attach events to the document
    document.addEventListener("pointerlockchange", pointerlockchange, false);
    document.addEventListener("mspointerlockchange", pointerlockchange, false);
    document.addEventListener("mozpointerlockchange", pointerlockchange, false);
    document.addEventListener("webkitpointerlockchange", pointerlockchange, false);
}

Kill it with fire !!

Last thing for the player class : fire ! But first, we need to give him a weapon... Let's create it in the constructor. Then, an action is added to a 'click' event on the game canvas to make it fire :)


// The player weapon
this.weapon = new Weapon(game, this);

// Event listener on click on the canvas
canvas.addEventListener("click", function(evt) {
var width = _this.scene.getEngine().getRenderWidth();
var height = _this.scene.getEngine().getRenderHeight();

if (_this.controlEnabled) {
    var pickInfo = _this.scene.pick(width/2, height/2, null, false, _this.camera);
    _this.weapon.fire(pickInfo);
}
}, false);

The function scene.pick is used to retrieved several information about where the player clicked: the picked point and the picked mesh for example. Useful if we want to destroy only enemies target :)

The (mighty) weapon

Remember the 3D model loaded in our Game class (thank you Jb) ? We will use it now ! The following code is part of the Weapon constructor:


// The weapon mesh
var wp = game.assets["gun"][0]; // The mesh at index 0 is the parent of the whole object
wp.isVisible = true;
wp.rotationQuaternion = null;
wp.rotation.x = -Math.PI/2;
wp.rotation.y = Math.PI;
wp.parent = player.camera; // The weapon will move with the player camera
wp.position = new BABYLON.Vector3(0.25,-0.4,1);
this.mesh = wp;

The method fire is below. The Weapon object only checks if the targetet mesh is a target, and if so destroy it. If it is not a target, an bullet impact is created (actually a small box).
Of course, we could have done a much more complicated code, with particles everywhere and so on.


/**
* Fire the weapon if possible.
* The mesh is animated and some particles are emitted.
*/
fire : function(pickInfo) {
    if (this.canFire) {
        if (pickInfo.hit && pickInfo.pickedMesh.name === "target") {
            pickInfo.pickedMesh.explode();
        } else {
            var b = BABYLON.Mesh.CreateBox("box", 0.1, this.game.scene);
            b.position = pickInfo.pickedPoint.clone();
        }
        this.animate();
        this.canFire = false;
    } else {
        // Nothing to do : cannot fire
    }
}

Right now, our weapon can fire several times by seconds. Let's change that with a controlled fire rate. Just add this in the Weapon controller:


var _this = this;
this.game.scene.registerBeforeRender(function() {
    if (!_this.canFire) {
        _this._currentFireRate -= BABYLON.Tools.GetDeltaTime();
        if (_this._currentFireRate <= 0) {
            _this.canFire = true;
            _this._currentFireRate = _this.fireRate;
        }
    }
});

The function registerBeforeRender called on the game scene takes a callback function as a parameter. It will be called before rendering the scene.
BABYLON.Tools.GetDeltaTime() is used to retrieved the time spent between two rendering. With this, we have a perfect fire rate, completly independent of the number of frames per seconds.

I'm sure you noticed the 'animate' function in the 'fire' method. This function is used to create a small weapon recoil, in order to add some dynamism to our game. Check it out:


/**
* Animate the weapon
*/
animate : function() {
    // The initial rotation is the initial mesh rotation
    var start = this._initialRotation.clone();
    var end = start.clone();
    // The actual rotation of the mesh
    end.x += Math.PI/10;

    // Create the Animation object
    var display = new BABYLON.Animation(
    "fire",
    "rotation",
    60,
    BABYLON.Animation.ANIMATIONTYPE_VECTOR3,
    BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);

    // Animations keys
    var keys = [{
        frame: 0,
        value: start
    },{
        frame: 10,
        value: end
    },{
        frame: 100,
        value: start
    }];

    // Add these keys to the animation
    display.setKeys(keys);

    // Link the animation to the mesh
    this.mesh.animations.push(display);

    // Run the animation !
    var _this = this;
    this.game.scene.beginAnimation(this.mesh, 0, 100, false, 10);
}

The arena

Our arena is very simple: it's only a ground ! But if you create your own shooter game, a ground won't be enough. In this class, you will surely add trees, walls, houses, and so on.


// The ground
var ground = BABYLON.Mesh.CreateGround("ground",  this.size,  this.size, 2, this.game.scene);
ground.checkCollisions = true;

We will now add a minimap on the screen. This minimap will be a new camera, located above the player head and showing everything happening in the arena.

The minimap

The minimap will use a cool feature of Babylon: multi-views and viewports. In a Babylon game, you can have many active cameras, each one looking at a different point of view.
But first, let's create a new FreeCamera. This camera will use the orthographic mode (whereas all created cameras use a perspective mode by default). If you don't know it yet, here is an image showing the same scene with 2 differents modes:

Orthographic mode is used to represent 3D objects in two dimensions, thus giving our scene a 2D aspect. Perfect for the minimap !


var mm = new BABYLON.FreeCamera("minimap", new BABYLON.Vector3(0,100,0), this.game.scene);
mm.setTarget(new BABYLON.Vector3(0.1,0.1,0.1));
// Activate the orthographic projection
mm.mode = BABYLON.Camera.ORTHOGRAPHIC_CAMERA;

//These values are required for using an orthographic mode,
// and represents the coordinates of the square containing all the camera view.
// this.size is the size of our arena
mm.orthoLeft = -this.size/2;
mm.orthoRight = this.size/2;
mm.orthoTop =  this.size/2;
mm.orthoBottom = -this.size/2;

mm.rotation.x = Math.PI/2;

Now our minimap is define, we must add it a viewport. A viewport is the part of the screen where our minimap will be shown. In our case, we want it in the top right of the screen.


var xstart = 0.8, // 80% from the left
    ystart = 0.75; // 75% from the bottom
var width = 0.99-xstart, // Almost until the right edge of the screen
    height = 1-ystart;  // Until the top edge of the screen

mm.viewport = new BABYLON.Viewport(
    xstart,
    ystart,
    width,
    height
    );

// Add the camera to the list of active cameras of the game
this.game.scene.activeCameras.push(mm);

Layer masks

Finally, we dont want to see the weapon mesh on our minimap. Instead, we want to see a red dot for the player. It will be easier for him to know where he is going. To implement this, we will use camera layers masks. They can be used to selectively filter game objects.

How do they work ? It's easy: it's a AND operation between two binary numbers. If the result number is 0, the mesh won't be seen for this camera. By default, the layer mask for all meshes and all cameras is 0xFFFFFFFF (1111111111111111111111111111 in binary). Let's take an example, and set a custom layer mask for our minimap:


mm.layerMask = 1; // 001 in binary

If a mesh has a layer mask equals to 2 (010 in binary), then 001 & 010 = 0 in binary : this mesh won't be displayed on this camera. It's exactly what we are going to do for our player.
In the minimap, the player will be represented by a red sphere. So, in the class Player:


this.camera.layerMask = 2; // 010 in binary

// The representation of player in the minimap
var s = BABYLON.Mesh.CreateSphere("player2", 16, 4, this.scene);
s.position.y = 10;
// The sphere position will be displayed accordingly to the player position
s.registerBeforeRender(function() {
    s.position.x = _this.camera.position.x;
    s.position.z = _this.camera.position.z;
});

var red = new BABYLON.StandardMaterial("red", this.scene);
red.diffuseColor = BABYLON.Color3.Red();
red.specularColor = BABYLON.Color3.Black();
s.material = red;

s.layerMask = 1; // 001 in binary : won't be displayed on the player camera, only in the minimap

Conclusion

Your game is now started ! Feel free to copy-paste the complete source code and start your own shooter game, with moving targets, several weapons, bonus,... And show me your finished game, I will be happy to play it :)
Click here to run the shooter demo, or click on the image below to get the final code source if you want to take a look at it.


If you have any questions about it, feel free to email me at temechon [at] pixelcodr [dot] com, or leave a comment below, I'll answer quickly.
You can also subscribe to the newsletter and you will receive an email when a new tutorial is out. No spam, and unsubscribe whenever you want.

Cheers !