Skip to content

matiasbeckerle/doom-lgs

Repository files navigation

DoomLGS

A multiplayer Node.js light gun shooter inspired on Doom. This is a proof of concept for a server/client game.

Play

Currently available for play in heroku.

Server

Runs on a Node.js server and uses socket.io library for communication. Sends snapshots of the game state to all the connected players several times per second, using a small structure to identify uniquely each instance of the game objects.

Insights

Server scripts are placed in /server folder and the main or bootstraper server.js file is located at root. All dependencies are defined at package.json and managed by NPM.

Game session is managed by the gameManager wich starts a cycle when the game is deployed to the server.

From server.js script:

gameManager.start();

From gameManager.js script:

/**
 * Starts a game iteration keeping two different ticks:
 *  - Snapshot will notify our players about the current game state.
 *  - Game will keep alive our game creating enemies, etc.
 */
var start = function () {
    snapshot = {
        dirty: false,
        players: 0,
        enemies: {}
    };
    tickSnapshot = setInterval(updateSnapshot, 100);
    tickGame = setInterval(updateGame, 3000);
};

With that code in mind the players will receive updates each 100ms and after 3s an enemy could be spawned too. Simple, right? Perhaps you are wondering how you send that snapshot to the players. The answer is listening for a game update.

From server.js script:

gameManager.onUpdate = function (snapshot) {
    socketManager.sendUpdate(snapshot);
};

What about socketManager? Is necessary too! That class stablishes a communication with players and sometimes it also emits a message (our snapshot).

From socketManager.js script:

/**
 * Sends an update to connected clients.
 * @param {Snapshot} snapshot The current game status.
 */
var sendUpdate = function (snapshot) {
    io.emit("update", snapshot);
};

But wait... what about listening? If someone enters our game the server is ready to listen for connections, disconnections and also player events like hitting an enemy!

From socketManager.js script:

/**
 * Starts socket communication.
 * @param {Http} http The recently created server.
 */
var listen = function (http) {
    io = socketio.listen(http);

    io.sockets.on("connection", function (socket) {
        // Add a new client
        addClient(socket);

        // Someone kills an enemy
        socket.on("enemyHit", function (enemyId) {
            SocketManager.onEnemyHit(enemyId);
        });

        // Someone goes offline
        socket.on("disconnect", function () {
            removeClient(socket);
        });
    });
};

Client

Uses Pixi.js as a game engine and socket.io library for communication. Consumes the snapshots provided by the server and handles all the game objects (populated with the game engine information) in the scene.

Insights

All client source files are placed inside /public. The dependencies are defined at bower.json file and managed by Bower.

  • /assets contains images and sound.
  • /build is used for building only, no files should be modified inside this folder.
  • /css contains the styles (not much to see here).
  • /lib contains the 3rd party libraries.
  • /scripts contains our game scripts.
  • index.html the only HTML required file.

I chose RequireJS for handling module loading because I'm familiar with. The boostrap file is main.js that defines some dependencies and starts the game.

From main.js script:

requirejs(["game"], function (Game) {
    Game.start();
});

Starting the game involves several things to consider. First of all there is the assets loading.

From game.js script:

// Where the game begins!
var start = function () {
    PIXI.loader
        .add("background", "/assets/e2m2.png")
        .add("soldier", "/assets/enemy.png")
        .load(onAssetsLoaded);

    ...
};

When the assets are loaded I proceed with mounting the scene and binding with networking in order to receive updates.

From game.js script:

function onAssetsLoaded(loader, resources) {
    // Add scene background
    scene.addChild(new PIXI.Sprite(resources.background.texture));

    // Append the view
    document.getElementById("game").appendChild(renderer.view);

    // Everything ready, force the first update
    Networking.forceAnUpdate();

    // Listen for updates
    Networking.update = function (updatedServerSnapshot) {
        update(updatedServerSnapshot);
    };

    UI.showGame();

    // Start animating
    animate();
}

There is the Pixi.js cycle too, similar to other game engines.

From game.js script:

/**
 * Pixi.js cycle.
 */
function animate() {
    requestAnimationFrame(animate);

    // Render the container
    renderer.render(scene);
}

Perhaps you are wondering what Networking does. That's right. Manages the communication with the server. An example is when the server sends a snapshot to the players.

From networking.js script:

// The server send us an update of the current game state
socket.on("update", function (updatedServerSnapshot) {
    // Check for changes
    if (updatedServerSnapshot.dirty || forceUpdate) {
        Networking.update(updatedServerSnapshot);
        forceUpdate = false;
    }
});

Another responsability of the Networking class is to notify about certain events. Suppose that a player hits an enemy, that needs to be reported to the server to validate because we are based on the authoritative server concept.

From networking.js script:

/**
 * Communicate to the server that an enemy has been hit.
 */
function enemyHit(enemyId) {
    socket.emit("enemyHit", enemyId);
}

Networking

Shared data between server and clients needs to be minimal. In order to achieve that goal I've created a networking EnemyN class to provide basic information about each enemy instance. Clients takes that basic EnemyN information and creates their own instances with more complex stuff like the sprite information. In this game minimal data consists only in: position and id.

Build

To build and run this game you will need to install Node.js and Gulp. Once the two are finished you only have to run the following command:

$ gulp

Task runner

Automating was done through Gulp because I wanted to learn about it. Yes, this projects has a lot of wanted-to-learn technologies and tools. Tasks are defined in gulpfile.js at root.

Testing

You don't want to take this as an example. The project started without testing in mind so it isn't so strong in that regarding. Anyways, there are some tests inside /test folder and runs thanks to Mocha, Chai and Sinon.

Disclaimer

Assets being used on this proof of concept are property of their owners: id Software. The only purpose is to learn about networking, I'm not trying to sell anything. Thanks for all the inspiration, id Software: John Romero, John Carmack, Adrian Carmack, Kevin Cloud, Tom Hall, Sandy Petersen, Shawn Green, Robert Prince.