I have a new open-source software project to share today. It is an idea that has been bouncing around my head for a while: a working, real-time, multiplayer Ouija board game that runs in a web browser. I decided to go ahead and make it last week because there are some amazing new JavaScript tools maturing out of the NodeJS madness, and this kind of project has suddenly become much easier to complete than it would have been just a few years ago.
Specifically, I’m using MeteorJS, a brand-new full-stack NodeJS/MongoDB framework which abstracts away the intimidating problem of integrating a client program with its server resources. To oversimplify, a fast web app can’t wait for every bit of program logic to download from the server, and a centralized server can’t keep every bit of data organized on every client without its own reference copies, so a lot of work gets duplicated. MeteorJS attempts to minimize this issue by setting up a framework where application data can be accessed from either the client or the server at the same logical address or “collection,” using the same JavaScript semantics on either end. When it is possible to synchronize each client’s copy of its application data with the server’s version, MeteorJS automatically passes updates to the server and fetches fresh data, algorithmically eliminating conflicts that the other clients might have introduced.
It’s all wonderfully intricate stuff, but the point is that somebody else is building it and we don’t have to. So let’s cut the techno-babble and make a multiplayer Ouija game! Start by installing MeteorJS. Then, to create a new project and populate it with basic HTML, CSS, and JS files, type this at a command line:
meteor create worldwideouija
To turn off “easy mode” and better control the data that gets shared, disable autopublish:
meteor remove autopublish
Our game needs to store three kinds of “things,” so we’ll start by initializing three collections. Open the “worldwideouija.js” file in the “worldwideouija” folder and add this at the top:
Rooms = new Meteor.Collection("rooms");
Forces = new Meteor.Collection("forces");
Messages = new Meteor.Collection("messages");
The Rooms
collection will store metadata about each game room, the Forces
collection will store temporary impulse values that push the Ouija marker around the board, and the Messages
collection will store chat messages.
We can access these collections from either client or server, but the distinction is still important for the purposes of our application, as we need our Ouija server to manage the data for each room, and update each player’s client as necessary. MeteorJS allows us to conditionally execute JavaScript depending on whether the current environment is the client or the server, which allows all our application logic to be contained in one script. Let’s declare what the client should do now:
if (Meteor.is_client) {
Meteor.startup(function() {
Meteor.subscribe("allrooms");
Meteor.subscribe("allmessages");
//...
});
//...
}
Meteor.startup()
takes a callback function that should contain all the code we want to run when the application is ready to start execution. This callback will contain our game loop.
Inside, the Meteor.subscribe()
methods tell the client that it should ask the server for access to “allrooms” and “allmessages” that the server hopefully has available. While the Rooms
variable always refers to the collection, this “allrooms” subscription tells the client which records to actually synchronize with the server. Because autopublish is disabled, we will have to make these subscriptions available to the client. At the end of the file, add a block that tells the server to publish all Rooms
to “allrooms” subscribers and all Messages
to “allmessages” subscribers. Security is not a concern when consulting with spirits:
if (Meteor.is_server) {
Meteor.startup(function () {
Meteor.publish("allrooms", function() {
return Rooms.find();
});
Meteor.publish("allmessages", function() {
return Messages.find();
});
//...
});
}
I’ve neglected to publish the Forces
collection, but this is on purpose. At first, I imagined that each client could just update the server’s marker position for the current room at any time and the server would sort everything out, but whenever the delay between page updates got worse, the movement became jerky and weird. The Forces
collection is how I solved this. It acts as a buffer where each client can write control data. When the server is asked to update the marker position, it reads from this collection, updates the position according to all the available Forces
, and then deletes them, before sending the new position back. This means that no client ever has to read directly from this collection. Each client can update its own Forces
collection and send new records to the server without subscribing to any published objects. It’s probably better not to waste bandwidth synchronizing this data if the clients don’t need it.
So let’s write some code! Define a game loop that executes every 500 milliseconds by adding a Meteor.setInterval()
method to the client block, inside the startup callback function:
if (Meteor.is_client) {
Meteor.startup(function() {
Meteor.subscribe("allrooms");
Meteor.subscribe("allmessages");
Meteor.setInterval(function() {
if (Session.get("room")) {
if (isNaN(Session.get("dx"))) Session.set("dx", 0);
if (isNaN(Session.get("dy"))) Session.set("dy", 0);
Session.set("dx", Session.get("dx") * 0.9);
Session.set("dy", Session.get("dy") * 0.9);
Forces.insert({
room: Session.get("room"),
x: Session.get("dx"),
y: Session.get("dy")
});
Meteor.call("updateMarker", Session.get("room"), function(e,r) {
if (r.x && r.y) {
Session.set("posX", r.x);
Session.set("posY", r.y);
}
});
}
}, 500);
});
//...
}
This loop uses Session.set()
and Session.get()
to manage temporary session variables, used to save the client’s current impulse value, along with the client’s copy of the marker position. Meteor.call()
tells the server to execute its “updateMarker” method that we will have to define, and the callback updates the client’s position using whatever response data comes back.
The rest of the client application uses HTML templates integrated with JavaScript, like this function which returns the current Room ID:
Template.main.currentRoom = function() {
return Session.get("room") || false;
};
This return object can be accessed from the “main” template by including {{currentRoom}}
anywhere in context. We won’t go into detail about the rest of the templates but they are all included with the source code and should make enough sense if you can follow this logic. Look in the .html file for all the template HTML.
Templates can also handle jQuery events, and we can use this functionality to read the mouse position and update the client’s impulse values:
Template.room.events = {
"mousemove .gameBoard": function(e) {
var theRoom = Rooms.findOne(Session.get("room"));
var trueX = e.pageX - parseInt($('.gameBoard').css('margin-left'));
Session.set("dx", (trueX - Session.get("posX"))/25);
Session.set("dy", ((e.pageY - 50) - Session.get("posY"))/25);
}
//...
};
This is the events object for the “room” template, with keys that are strings containing a jQuery event type and often a CSS selector that declares which DOM element(s) to bind. Inside, we use a little hackery to compute an impulse vector from the mouse and marker positions, and then we save the session variables that the game loop will read from.
Finally, we have to implement an “updateMarker” method on the server:
if (Meteor.is_server) {
Meteor.startup(function () {
Meteor.publish("rooms", function() {
return Rooms.find({});
});
Meteor.publish("messages", function() {
return Messages.find({});
});
Meteor.methods({
updateMarker: function(id) {
var theRoom = Rooms.findOne(id);
var position = {};
if (theRoom) {
if (isNaN(theRoom.x)) theRoom.x = 480;
if (isNaN(theRoom.y)) theRoom.y = 320;
var dx = 0;
var dy = 0;
var numForces = 0;
var theForces = Forces.find({room: id});
theForces.forEach(function(force) {
dx += parseInt(force.x);
dy += parseInt(force.y);
numForces++;
});
Forces.remove({room: id});
Rooms.update(id, {$set: {players: numForces}});
if (numForces > 0) {
var newX = theRoom.x + dx/numForces;
var newY = theRoom.y + dy/numForces;
if (newX < 100) newX = 100;
if (newX > 860) newX = 860;
if (newY < 100) newY = 100;
if (newY > 540) newY = 540;
Rooms.update(id, {$set: {x: newX}});
Rooms.update(id, {$set: {y: newY}});
position.x = newX;
position.y = newY;
}
}
return position;
}
});
});
}
And that’s (almost) all!
Visit http://github.com/guscost/worldwideouija for the complete source code, and try it out at worldwideouija.meteor.com or worldwideouija.com! I’m still tinkering around so uptime and code quality are not 100% guaranteed.
And before I forget, many thanks to AVGP for the missing chatroom example, which helped a lot.
Awesome wicked fun! I had all my darkest questions answered by the demons of javascript. Fortunately, they told me exactly what I wanted to hear! Seriously, very cool work, and thanks for posting the code and walkthrough so we can all see how magic is brewed. 🙂
Always great to hear about another convert to JavaScript!