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! We recently gave the multiplayer WebGL game Stac…

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!

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

A Massively Multiplayer WebGL Game Mashup of PubNub and Three.js

With the emergence of HTML5, WebGL, and other browser-based 3D technologies, the way we think about browsers is quickly shifting. To me, making bad ass graphics in a browser is a rad prospect, but a hard one. Unless you have a pretty deep understanding of shader, vertex buffers, matrix transformations and the like, it can be overwhelming just to approach it.

Personally, I took an extremely-difficult but poorly-taught graphics course in college back in 2009, and while I pulled out a decent grade, I’ve already forgotten a lot of it. Nowadays I consider myself more of a generalist. But I remembered a speaker in a WebGL camp conference I attended back in June recommending a certain library called three.js, which he said was great for beginners. So I looked into mrdoob‘s library three.js.

Three.js

Three.js is essentially an abstraction layer for a variety of web 3D technologies. Creating shapes and setting up cameras is as easy as defining JavaScript objects. It’s renderer-agnostic, but out of the box, it supports canvas, svg and WebGL. When I first saw mrdoob’s project voxels I was dying to mash it up with PubNub and turned into a massively-multiplayer game. And as it turned out, he had an open-source, MIT-licensed version of it in the examples directory called Canvas Interactive Voxelpainter. But it uses the Canvas renderer, as opposed to WebGL. That’s ok for the purposes of this experiment.

StackHack is the result of that idea. Go play with it and come back when and if you want to delve into the proverbial details.

The Deets

Let’s start with the server. I used Node.js with express to serve up our HTML, CSS, JavaScript et all. When a client connects, we generate a UUID, append some stuff and listen on that channel. Why do it this way? Why not just use a generic PubNub channel? Excellent question. I wanted what’s known as an authoritative server.

Authoritative vs Non-Authoritative Server

In an non-authoritative server, a player would say “hey everyone, I’m making a block here.” And everyone else would have to accept it. A player could ignore it, but then their environment would be different than everyone else’s. Or a player could say “hey, I’m making (or deleting) a thousand blocks. Your creation is now gone.” There’s no referee to stop that type of behavior. Conversely, with an authoritative server, a player would instead say “hey, I would like to create a block here” and then wait a few milliseconds for the server to authorize it.

I once listened to Mozilla Evangelist Rob Hawkes give a talk on his project Rawkets (essentially a massively-multiplayer version of Asteroids) wherein he outlined his early, non-authoritative version of his server. Within hours, players had hacked the game to make giant ships, turbo-speed weaponry, and even cluster bombs. For StackHack, even though there are essentially no rules, I wanted this same sort of protection from cheating.

Here’s some server-side code where I initialize the pubnub bits using our npm module:

? 1 2 3 4 5 6 7 8 9 var pubnub = require( 'pubnub' ); var network = pubnub.init({ publish_key : "MY_PUBLISH_KEY" , subscribe_key : "MY_SUBSCRIBE_KEY" , secret_key : "MY_SECRET_KEY" , ssl : false , origin : "pubsub.pubnub.com" });

Now let’s load that into a Node’s event system:

? 1 2 3 4 5 6 7 8 9 10 11 var events = new (require( 'events' ).EventEmitter); network.subscribe({ channel : "stackhack_from_client_" + client.uuid, callback : function (message) { events.emit(client.uuid + "_" + message.name, message); }, error : function (e) { console.log(e); } });

So we can catch incoming messages of type “status” like this:

? 1 2 3 4 5 6 7 app.get( '/' , function (req, res) { events.on(client.uuid + "_status" , function (message) { }; }

On the server, if we want to send something to a particular player, we use the following functions:

? 1 2 3 4 5 6 7 8 9 10 11 12 function messageClient(client_uuid, name, data, cb) { var message = { "name" : name, "data" : data }; network.publish({ channel : "stackhack_from_server_" + client_uuid, message : message, callback : function (info) { if (cb != undefined) cb(info); } }); }

Then we render the page to the client and provide them the UUID. Here we render the page with Express and node-jqtpl:

? 1 2 3 4 5 6 7 8 9 10 11 12 var app = require( 'express' ).createServer(); app.configure( function () { app.use(app.router); app.set( "view engine" , "html" ); app.set( 'view cache' , false ); app.register( ".html" , require( "jqtpl" ).express); }); app.get( '/' , function (req, res) { res.render( 'main' , { 'layout' : true , 'uuid' : new_uuid, 'time_til_wipe' : getTimeTilWipe() }); }

Jumping to the client-side JavaScript, the client takes this UUID, listens on the right channel, and loads it into the pubnub event system:

? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 PUBNUB.subscribe({ channel : "stackhack_from_server_${uuid}" , callback : function (message) { console.log( "got from server: " + JSON.stringify(message)); PUBNUB.events.fire( "got_from_server_" + message.name, message); }, error : function (e) { console.log(e); } }); PUBNUB.events.bind( 'send_to_server' , function (message) { PUBNUB.publish({ channel : "stackhack_from_client_${uuid}" , message : message, error : function (e) { console.log(e); } }); console.log( "sent to server: " + JSON.stringify(message)); });

That’s how clients will communicate with our authoritative node server. In this way, we prevent other users from pretending to be a client they’re not. I perpended a “from_server” and “from_client” to keep things from getting confusing. In essence, I’m turning a bidirectional connection into two separate one-directional sockets. Perhaps this isn’t the ideal way to do it, but it works for this use case regardless and it keeps things clean.

Let’s dive into some three.js code. Here’s how we set up the grid. This is taken completely from mrdoobs’ original “voxels”.

? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var geometry = new THREE.Geometry(); geometry.vertices.push( new THREE.Vertex( new THREE.Vector3( - 500, 0, 0 ) ) ); geometry.vertices.push( new THREE.Vertex( new THREE.Vector3( 500, 0, 0 ) ) ); var material = new THREE.LineBasicMaterial( { color: 0x000000, opacity: 0.2 } ); for ( var i = 0; i <= 20; i ++ ) { var line = new THREE.Line( geometry, material ); line.position.z = ( i * 50 ) - 500; scene.add( line ); var line = new THREE.Line( geometry, material ); line.position.x = ( i * 50 ) - 500; line.rotation.y = 90 * Math.PI / 180; scene.add( line ); }

In order to create a block, the client uses the following functions. Note that x, y, and z are the coordinates, while c is the color, and o is the opacity.

? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function addBlockToScene(x, y, z, c, o) { var material = new THREE.MeshLambertMaterial( { color: c, opacity: o, shading: THREE.FlatShading } ); var block = new THREE.Mesh( new THREE.CubeGeometry( 50, 50, 50 ), material ); block.material = material; block.position.x = x; block.position.y = y; block.position.z = z; block.matrixAutoUpdate = false ; block.updateMatrix(); block.overdraw = true ; scene.add( block ); return block; } function createBlock(x, y, z, c, o) { var place = blockHash(x, y, z); if (!block_index[place]) { var block = addBlockToScene(x, y, z, c, o); block_index[place] = block; } }

But we want to create a block only if the server authorizes it. So when we click, we’ll add a block with an opacity at .8 and set a setTimeout; if we don’t get a server acknowledgment within 5 seconds, we’ll remove the block. If we do get an acknowledgment, we’ll clear that timeout and set the block’s opacity to 100%.

? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 createBlock(cursor.position.x, cursor.position.y, cursor.position.z, color, .8); pending_blocks[place].removal_timeout = setTimeout( blockTimeout, 5000, place); function blockTimeout(place) { scene.remove(block_index[place]); delete block_index[place]; clearInterval(pending_blocks[place].recheck_interval); delete pending_blocks[place]; } PUBNUB.events.bind( "got_from_server_create" , function (message) { place = blockHash(message.data.loc[0], message.data.loc[1], message.data.loc[2]); if (pending_blocks[place] != undefined) { clearTimeout(pending_blocks[place].removal_timeout); clearInterval(pending_blocks[place].recheck_interval); delete pending_blocks[place]; block_index[place].material.opacity = 1; } else { createBlock(message.data.loc[0], message.data.loc[1], message.data.loc[2], message.data.color, 1); } });

We also have a server supplied-total wipeout every 10 minutes.

On the server:

? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var minutes_between_wipes = 10; function getTimeTilWipe() { var now = + new Date; time_til_wipe = (wipe - now); return time_til_wipe; } function wipeIt() { console.log( "wipe!" ); block_index = {}; wipe = + new Date + (minutes_between_wipes * 60 * 1000); messageAllClients( "wipe" , { "next" : getTimeTilWipe()}); } wipeIt(); setInterval( function () { wipeIt(); }, minutes_between_wipes * 1000 * 60);

On the client:

? 1 2 3 4 5 6 7 8 9 10 function removeAllBlocks() { for (block in block_index) { scene.remove(block_index[block]); delete block_index[block]; } } PUBNUB.events.bind( "got_from_server_wipe" , function (message) { removeAllBlocks(); });

And that’s the gist of it. Have fun!

Footnotes

Get Started

Sign up for free and use PubNub to power interactive three.js