IN THIS ARTICLE

    We recently gave the multiplayer WebGL StackHack a major facelift, and we recommend checking out that blog post here: Making Interactive WebGL Applications: StackHack 2.0

    What I learned and how I made it better

    Last month I released StackHack, a browser-based 3D block-building game, made multiplayer by PubNub. In the weeks that followed, I realized that a lot of my design choices were just plain stupid. It was time to ruthlessly refactor. In this process, I learned some stuff. I’m going to tell you some awesome facts about what I learned.

    multiplayer webgl

    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.

    When I first hacked together StackHack, I wanted to be in control. Working on an open platform like the web is awesome, but it comes with the caveat that the only thing you can ultimately control are your own entities. Regardless of what html you serve up, it’s just information, and internet users can do whatever they want with it. Because of this, you have to get into the mindset of your project being an API, for which you’re also working on the default interface for that API. Not the only interface, because people can always control what they see.

    Here’s the thing: this mindset is good, but I took it too far. I was a tyrant running a dictatorship, and my users were suffering as a result. What I realized is that I still get that initial page load; I can tell the clients who the boss is. However, once that is established, the boss can get out of the way until he needs to get involved. The boss always has the ability to cut someone out, but the rest of the time he doesn’t interfere. A referee is better than a dictator.

    This leads me to the fundamental change in the architecture. Rather than having every move go through the server, which would then pass on only good messages, now the clients can freely communicate over PubNub. The server listens to this chatter and has the ability to interfere if need be. As a result, everything is much faster.

    This was the reason for the previous version’s disconnects. They weren’t disconnects between the user and PubNub, but rather between the user and my server in the cloud (Nodejitsu’s cloud, actually). PubNub never disconnects. When it does, it auto-reconnects. It was time to embrace that. As such, I changed my server architecture so no reloading should ever be necessary again.

    multiplayer webgl

    Previously, the server was listening to too many channels and binding to too many events. It didn’t scale. I simplified things to have one global channel for global actions. So the server listens on only one channel, whereas the client listens on the global channel and it’s own private channel. The server can send private messages to the client, but the client can’t send private messages to the server. This is OK.

    Also, the server can go away and everyone is still fine. The server just makes things better. The ecosystem can survive without it, but it’s better for everyone if it’s there.

    The Critical Mass Problem

    I think the hardest part of making anything “realtime” is the critical mass problem. This affects all websites, not just realtime ones, but this effect is multiplied because if someone visits your app and they happen to be the only one there at that very second, it’s not very much fun. My solution to this problem: if no one is there, fake it.

    I don’t know much about artificial intelligence but I do know how to use a random number generator. So I started stitching together a function that waits around, and if things get boring, it starts making little patterns. It’s like being friends with a replicant: almost as good, but not quite real. This is about the degrading to something better than nothing: “fake it til you make it”. Here’s some code where I generate the starting block in a random location at grid level, with a random color.

    1
    2
    3
    4
    5
    6
    7
    8
    do {
    x = (Math.floor(Math.random()*20) - 10) * 50 + 25;
    y = 25;
    z = (Math.floor(Math.random()*20) - 10) * 50 + 25;
    c = Math.floor(Math.random()*16777215); // thanks: http://paulirish.com/2009/random-hex-color-code-snippets/
    }
    while (isBlockValid(x,y,z) == false);
    }

    Then I go on to generate a random amount of moves, going random directions, randomly changing in color, spaced apart over a random amount of time.

     

    Persistance

    In the first version of Stackhack, I had no database. Everything lived in memory on the server. Thus, when the process crashed and restarted, everything went back to zero. I decided to remedy this with a little MongoDB and mongoose. Here’s how I load and store the blocks:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var mongoose = require('mongoose');
    // here I define the schema
    var BlockSchema = new mongoose.Schema({
    x : { type: Number, default: 0 },
    y : { type: Number, default: 0 },
    z : { type: Number, default: 0 },
    color : { type: Number, default: 0 },
    created_by : { type: String, default: '' },
    hash : { type: String, default: '' }
    });
    // define the model based on that schema
    var Block = mongoose.model('Block', BlockSchema),
    // this gets run when the node process starts
    Block.find({}, function (err, blocks) {
    Object.keys(blocks).forEach(function(b) {
    block = blocks[b];
    console.log('block: starting with ' + block.x + ', ' + block.y + ', ' + block.z);
    block_index[block.hash] = block;
    });
    });

    When I want to save a block, I do it like this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var block = new Block();
    block.x = message.data.x;
    block.y = message.data.y;
    block.z = message.data.z;
    block.color = message.data.color;
    block.created_by = client.info.uuid;
    block.hash = hashBlock(message.data.x, message.data.y, message.data.z),
    block_index[block.hash] = block;
    block.save( function(err) {
    if (err) { console.log('err saving block: ' + err); }
    });

    So now when the process crashes, everything spins right back to life.

    Some other things I improved:

    • I used some basic jQuery UI buttons to make things look shinier.
    • Download StackHack Source: The whole thing is open source on githhub here.
    • You can only make a block in the actual grid. Anything outside those bounds will be ignored by the server and all clients (by default).
    • Before, block removal was a messy, buggy thing controlled by the shift key. Creation and removal is now a binary system. You’re either doing one or the other, determined by a global “mode” variable.
    • My presence detection was too chatty and too unforgiving. Now, every 30 seconds it says “hey, who’s there?” and the clients respond, or don’t.

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

    Try PubNub today!

    Build realtime applications that perform reliably and securely, at global scale.
    Try Our APIs
    Try PubNub today!
    More From PubNub