Lightweight Multiplayer HTML5 Games with PubNub and melonJS

We’ve updated our SDKs, and this code is now deprecated.

Good news is we’ve written a comprehensive guide to building a multiplayer game. Check it out!

In my quest to change the world, I’ve been experimenting with an HTML5 game engine to push the limits of the browser as a platform for serious gaming. melonJS has been my tool of choice because it’s lightweight, runs well on mobile, and is very easy to use. Starting last year, I became co-developer on the project. One question that always comes up on the melonJS forum is the best way to use Node.js/socket.io to build multiplayer games. In this article, I’ll be using PubNub, but many of the techniques can be applied to socket.io as well.

For this experiment, I’ll start with the platformer demo that ships with the melonJS 0.9.9 source code, and transform it into a multiplayer HTML5 game with just a few extra lines of code. And all without any servers! This is only possible with the power provided by PubNub.

Want to see the end result? Check it out here. I’ll walk you through how to add multiplayer support to your own game below:

Download melonJS

First step, clone the git repository and checkout the 0.9.9 tag:

$ git clone https://github.com/melonjs/melonJS.git
$ cd melonJS
$ git checkout 0.9.9

Next, you’ll want to follow the build instructions to build the library. And you can test the vanilla platformer demo by launching an HTTP server with Python:

$ python -m SimpleHTTPServer

Now visit the URL in your favorite web browser:

http://localhost:8000/examples/platformer/

It’s a very simple game demo, with a handful of enemies and two maps. However, we want multiplayer support as well. What I started with is a simple module to handle the multiplayer communications.

You’ll first need to sign up for a PubNub account. Once you sign up, you can get your unique PubNub keys in the PubNub Developer Portal. Once you have, clone the GitHub repository, and enter your unique PubNub keys on the PubNub initialization.

mp.js

var Multiplayer = Object.extend({
    init : function (new_player) {
        this.pubnub = PUBNUB.init({
            publish_key   : "PUBLISH KEY HERE",
            subscribe_key : "SUBSCRIBE KEY HERE"
        });

        this.new_player = new_player;

        // Record my UUID, so I don't process my own messages
        this.UUID = this.pubnub.uuid();

        // Listen for incoming messages
        this.pubnub.subscribe({
            channel : "PubNub-melonJS-demo",
            message : this.handleMessage.bind(this)
        });
    },

    handleMessage : function (msg) {
        // Did I send this message?
        if (msg.UUID === this.UUID)
            return;

        // Get a reference to the object for the player that sent 
        // this message
        var obj = me.game.getEntityByName(msg.UUID);
        if (obj.length) {
            obj = obj[0];
        }
        else {
            var x = obj.pos && obj.pos.x || 50;
            var y = obj.pos && obj.pos.y || 50;
            obj = this.new_player(x, y);
            obj.name = msg.UUID;
        }

        // Route message
        switch (msg.action) {
        case "update":
            // Position update
            obj.pos.setV(msg.pos);
            obj.vel.setV(msg.vel);
            break;

        // TODO: Define more actions here
        }
    },

    sendMessage : function (msg) {
        msg.UUID = this.UUID;

        this.pubnub.publish({
            channel : "PubNub-melonJS-demo",
            message : msg
        });
    }
});

This class has a constructor and two methods; the constructor takes one callback, and the sendMessage() method is the one we’ll be using to send game state updates. This module also does some useful things like creating new player objects, and handling player position updates. I placed this file (mp.js) into the platformer directory, and included it within index.html (along with pubnub-3.4.5-min.js)

Creating a new Multiplayer object

To initialize the Multiplayer object, I added a few lines after the level has been loaded, around line 104:

    // Instantiate the Multiplayer object
    game.mp = new Multiplayer(function (x, y) {
        // Create a new player object
        var obj = me.entityPool.newInstanceOf("mainplayer", x, y, {
            spritewidth : 72,
            spriteheight : 98,
            isMP : true
        });

        me.game.add(obj, 4);
        me.game.sort();

        return obj;
    });

This creates the object, placing a reference into the game namespace as game.mp, and passes a callback function that will create new player objects when we receive messages from other players we haven’t seen before.

That isMP : true line is important! It will be used later to determine whether the player object is Keyboard-controlled, or controlled by messages from the network.

Side note: to make testing easier, you can disable the “automatic pause” feature when navigating away from the browser window. I added the following line just before the call to me.video.init() in main.js:

        me.sys.pauseOnBlur = false;

Turning the PlayerEntity object into a Multi-PlayerEntity Object

Now we’re ready to hack the PlayerEntity object to work in a multiplayer environment, sending position updates, and ignoring the keyboard input for the isMP entities. Starting in entities.js at line 25, I added two new properties:

        this.isMP = settings.isMP;
        this.step = 0;

Then I changed the following lines to be conditional on the value of the isMP property. The viewport follow and key bindings should be skipped if the entity is a multiplayer object:

        if (!this.isMP) {
            // set the display around our position
            /* ... snip */
            // enable keyboard
            /* ... snip */        }

The original code has been snipped from the example above, but it should be pretty obvious what needs to be changed here.

In the PlayerEntity.update() method, there are a few things that also need to be made conditional on the value of isMP. This first checks the key status:

        if (!this.isMP) {
            if (me.input.isKeyPressed('left')) {
                /* ... snip */            }

            if (me.input.isKeyPressed('jump')) {
                /* ... snip */            }
        }

There’s also a call to me.game.viewport.fadeIn() that reloads the level when the player falls into a hole. We could make that conditional too, if we don’t want to reload the level when other players fall in.

And finally, there’s a comment at the end of the method about checking if the player has moved. This is the perfect hook for sending out player position updates to other players! I added the following code just before the call to this.parent():

            if (this.vel.x !== 0)
                this.flipX(this.vel.x < 0);

            if (!this.isMP) {
                // Check if it's time to send a message
                if (this.step == 0) {
                    game.mp.sendMessage({
                        action : "update",
                        pos : {
                            x : this.pos.x,
                            y : this.pos.y
                        },
                        vel : {
                            x : this.vel.x,
                            y : this.vel.y
                        }
                    });
                }

                if (this.step++ > 3)
                    this.step = 0;
            }

The first two lines will fix the “direction” of the player object when it is updated by a message from the network.

The rest contains a basic counter to prevent sending messages too fast, and the final message publish that other players will receive.

Play Our Multiplayer HTML5 Games Online!

The final demo can be played online now! And you can also have a peek at the full patch here. A much better approach would be separating control logic entirely from the entity. But in this case, the demo serves its purpose. Maybe next time, we can work on synchronizing more of the game state, like enemy positions and individual score counters!

Get Started
Sign up for free and use PubNub to power multiplayer games!

Try PubNub Today

Connect up to 100 devices for Free