How to Build a Multiplayer Web App [Code Lab with Mad Genius Escapes]

The hardest part of a project is often just getting started. In my interview with Peter Lewis, co-creator of The Truth About Edith and Boobano Farm, he ended with some advice to fellow creators: even as no-code platforms for multiplayer escape games become increasingly available, there is still much to be gained from building this sort of interactivity from scratch. Not only will you learn something new, you’ll also inject your personality into the game through the hands-on development process.

With this in mind, Peter and I put together this step-by-step walkthrough for coding a simple web app which uses the same tech stack as The Truth About Edith and Boobano Farm. If you have some coding experience and want to learn about developing collaborative or asymmetrical web-based gameplay, this code lab is for you. 

We hope this serves as a jumping-off point for you to build your own games, and we’d love to hear about your future creations!

Digital Boobano Farm jigsaw puzzle scrambled.

Goals

  • Make a simple web app which allows multiple players to solve a jigsaw together
  • Learn how to use Socket.IO and Node.js to implement server-synced interactions

Prerequisites

Stage 0: Getting started

  1. Make a folder for the app, and cd there. Add two new empty files called index.html and server.js.
  2. Initialize a Node package for the folder with npm init. You can hit “enter” to accept the default options. When asked for an “entry point”, make sure server.js is specified.
  3. Install the dependencies needed for this app. You’ll see how these are used later.
npm install express
npm install socket.io
  1. Initialize the folder as a Github repository. This allows us to directly deploy to Heroku. If this is your first time using Git, you’ll be prompted to configure your GitHub email and username.
git init
git add .
git commit -m "first commit"
  1. Create a new empty Heroku app with heroku create <projectname>. If this is your first time using Heroku, you’ll be prompted to log in here.
  2. Deploy our (empty) app to Heroku to check that all this initial config worked properly with git push heroku master. If this is your first time running this, you may be prompted to log into your Heroku account online.

Your app will now be available at <projectname>.herokuapp.com. However, you’ll just get an “Application error” page since index.html is currently empty. (You’ll be able to redeploy the app anytime later once we’ve started building it!)

Stage 1: App structure and puzzle piece initialization

  1. In server.js, add the following boilerplate code setting up Express (a helpful framework for Node.js) and Socket.IO.
'use strict';
const express = require('express');
const socketIO = require('socket.io');
const PORT = process.env.PORT || 3000;
const INDEX = __dirname + '/index.html';
const server = express();
server.get('/', (req, res) => {
    res.sendFile(INDEX);
});
server.use(express.static('public'));
const listener = server.listen(PORT, () => console.log('Listening on ${PORT}'));
const io = socketIO(listener, {
    pingTimeout: 120000
});
  1. In server.js, add on the following code. We’ll store an array of the coordinates of each piece server-side, initialized randomly within a 600px x 600px canvas. Then using socket.emit(), we’ll send this position data to a client-side function which we’ll call placePieces().
var piecePositions = [];
for(var i = 0; i < 20; i++){
    piecePositions.push([600 * Math.random(), 600 * Math.random()]);
}
io.on('connection', (socket) => {    // Any functions we want to listen for go in here
    socket.emit('placePieces', piecePositions);
});
  1. Now let’s define that client-side function placePieces. In index.html, add a div playArea as a container for all the jigsaw pieces. Then, add some JavaScript which places the pieces using the position data from the server. Once we add in the ability to move the pieces around, this function will update the piece locations essentially continuously.

For the purposes of this demo, Mad Genius Escapes has provided a set of 20 Boobano Farm-themed puzzle pieces, hosted on their server and named “piece1” through “piece20”. To ensure the correct sizing, we also explicitly set the correct width for each piece. (There are more elegant ways to do this if we were actually building a jigsaw app.)

<!DOCTYPE html>
<html>
<body>
<div id='playArea'></div>
<script src="/socket.io/socket.io.js"></script>

<script>
let socket = io();
var pieces = [];
const sizes = [150, 221, 209, 316, 217, 138, 233, 234, 318, 183, 145, 216, 290, 257, 181, 151, 213, 279, 270, 194];

function placePieces(positions){
    var thisPiece;
    let playArea = document.getElementById('playArea');
    for(var i = 0; i < 20; i++){
        thisPiece = document.createElement('img');
        thisPiece.src = 'https://v-escape.com/img/demo/piece' + (i + 1) + '.png';
        thisPiece.style.left = positions[i][0] + 'px';
        thisPiece.style.top = positions[i][1] + 'px';
        thisPiece.style.width = sizes[i] + 'px';
        thisPiece.style.height = 'auto';
        thisPiece.classList.add('piece');
        thisPiece.pieceNum = i;
        pieces.push(thisPiece);
        playArea.appendChild(thisPiece);
    }
}

socket.on('placePieces', placePieces);
</script>

</body>
</html>
  1. In index.html, add some styling to get rid of the default image drag effect, change our mouse to a pointer hand when hovering over a piece, and set a transformation so we’ll eventually be dragging from the center of each piece.
<style>
.piece {
    position: absolute;
    cursor: pointer;
    transform: translate(-50%, -50%);
}
img {
    user-drag: none; 
    user-select: none;
    -moz-user-select: none;
    -webkit-user-drag: none;
    -webkit-user-select: none;
    -ms-user-select: none;
}
</style>

Now is a great time to check in on what this code is doing. After pushing the new code to Heroku, you should see the 20 pieces randomly scattered around the screen. Next up: making the pieces moveable!

git add .
git commit -m "place pieces"
git push heroku master

Stage 2: Make the pieces moveable

  1. In index.html, add the following functions in the script block. We’ll use selectedPiece to keep track of which piece, if any, is currently being clicked by the user. This is set back to null onMouseUp. In onMouseMove(), if the user has a piece selected, we send the coordinates for that piece back to the server.
var selectedPiece = null;

function pieceClicked(){
 selectedPiece = this.pieceNum;
}

function onMouseMove(e){
 if(selectedPiece == null)
 return;
 socket.emit('movePiece', [selectedPiece, e.clientX, e.clientY])
}

function onMouseUp(e){
 selectedPiece = null;
}

Now let’s use these new functions. In index.html, add the following 3 lines to the existing placePieces() function:

function placePieces(positions){
...
    for(var i = 0; i < 20; i++){
        ...
        thisPiece.onmousedown = pieceClicked;
        ...
    }
    document.addEventListener('mouseup', onMouseUp);
    document.addEventListener('mousemove', onMouseMove);
}
  1. Socket.IO allows for ongoing two-way communication between servers and clients. For placePieces, we have a socket.emit() on the server and a socket.on() on the client, sending data from the server which is received by each connected client.

For movePiece(), we’ll be sharing data in both directions, from the client to the server and back from the server to the client.

We already added socket.emit('movePiece', [selectedPiece, e.clientX, e.clientY]) in onMouseMove. Add the following code in server.js to receive this position data and update the piecePositions array for that piece. Then, we’ll send the updated position data back to the client.

io.on('connection', (socket) => {
    socket.emit('placePieces', piecePositions);

    socket.on('movePiece', (data) => {
        let pieceNum = data[0];
        let piecePos = piecePositions[pieceNum];
        piecePos[0] = data[1];
        piecePos[1] = data[2];
        io.emit('movePiece', data);
    });
});

Back in index.html, we’ll receive this data and update the position of the moved piece accordingly.

socket.on('movePiece', movePiece);

function movePiece(data){
    let piece = pieces[data[0]];
    piece.style.left = data[1] + 'px';
    piece.style.top = data[2] + 'px';
}

While this back-and-forth would be redundant if there were just one client connected to our app, it suddenly becomes really powerful when multiple clients are connected. Each client sends data on which piece they’re moving, and the server sends updated piece positions aggregated from all the clients back to each client. Thus, in real time each client sees their own pieces moving as well as any pieces that are being moved by other clients.

To test the final version of the app, git commit your most recent changes and git push heroku master.

What’s next?

This code lab demonstrates how to use Socket.IO, not how to make the perfect jigsaw app. The jigsaw app we built is admittedly quite janky, and some simple improvements might include:

  • Add a “claimed” property to pieces so multiple players can’t try to move the same piece at the same time.
  • Snap together correctly placed pieces.
  • Drag pieces from the actual mouse position rather than the center of the piece.

More importantly, the simple code pattern you’ve now seen can be used to share any sort of data between clients and a server. Players might all see the same interface and have the same controls, as in this jigsaw example, or players might each have a different view and different controls for asymmetrical interactions. To accomplish this, we could give players unique login codenames, as in The Truth About Edith, or dynamically assign players roles based on the order in which they arrive on a page.

Players might each have their own button, control their own character, or be presented with a unique subset of puzzle data. The possible interactions you can build using Socket.IO are endless, and we look forward to seeing what you create on your own. This cheat sheet for Socket.IO emits is useful, and there are numerous other tutorials and resources available online.

If you’ve made it this far, you probably like learning about escape game design and technology. We invite you to the Reality Escape Convention, August 22-23, 2021. This digital event brings the global escape room player and creator communities together for 2 days of connection, learning, and sharing.

2 thoughts on “How to Build a Multiplayer Web App [Code Lab with Mad Genius Escapes]

  1. In my experience, the Firebase Realtime Datastore (https://firebase.google.com/products/realtime-database) is a good alternative (instead of Socket.io and Node.js) for realtime game development.

    The big advantage is that it mains a _synchronized state_ instead of just relaying messages. (Basically you get a tree of JSON, which you can edit from anywhere, and it tells you whenever it changes, and it keeps track of robust sync.) This is a big deal because otherwise it’s very, very easy for things to get out of sync if a message is dropped (absolutely possible with Node.js/Socket.io, especially with flaky customer networks) or there’s a buglet somewhere.

    The big disadvantage of the Firebase Realtime Datastore is that it’s a proprietary service by Google, and a little bit of a backwater one, so it’s always subject to getting yanked (though there’s no sign of that at present).

    Finally: These days, it’s a lot less important to use Socket.io as a layer — it adds a few niceties but not a lot on top of regular websockets. It used to be absolutely necessary since a lot of browsers didn’t implement websockets (and Socket.io would find other ways to do real time communication) but now they all do. Not that there’s anything _wrong_ with Socket.io as a layer at that level.

  2. Thanks for sharing these insights, Dan! It’s particularly interesting to hear the background on how the need for Socket.io has developed/eroded. It also seems a bit bloated for what it’s currently doing.

    I might check out Firebase for a project I’m currently working. The Realtime Datastore looks awesome.

    Regarding the specific stack used in this article, we’re in no way claiming it’s the best or most optimal way for creating all real-time games. We had 2 main goals:
    1) Demonstrate how easy/quick it can be to get started building something from scratch.
    2) For other creators, pull back the curtain on how specifically Edith and Boobano Farm were coded, which Peter iteratively developed with no real prior game dev background.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.