Creating a real-time multiplayer game with WebSockets and Node.js
This past weekend I had the pleasure of putting on workshop at the Mozilla Festival in London. During the workshop I explained exactly how to take a single player HTML5 game and turn it into a multiplayer one using Node.js and WebSockets.
This past weekend I had the pleasure of putting on workshop at the Mozilla Festival in London. During the workshop I explained exactly how to take a single player HTML5 game and turn it into a multiplayer one using Node.js and WebSockets.
I'm looking to put on more workshops like this and write other tutorials about HTML5 gaming, so make sure to leave your feedback in the comments or on Twitter.
Downloading the single-player code
Before we get started you'll need to download the single-player game code from Github.
Unpack the game code into a directory and open the index.html
file contained within the public
directory.
If everything worked correctly then you'll see a little black square inside the browser. Use the arrow keys to move it around.
Optional: Bask in the glory of this amazing game.
Setting up Node.js
If you haven't guessed already, all the Node.js server files will exist outside of the public
directory, which only contains client-side files.
To run the game server you'll need to have Node.js installed.
An easy way to install on the Mac is to use Homebrew. It's as simple as one line in the terminal:
brew install node
Node.js can be tricky on Windows but there is a package manager called Chocolatey which may help. Chocolately also lets you install Node.js with one line in the terminal:
cinst nodejs
You can check if Node.js was installed by running the version command in the terminal:
node -v
It should output the current version number of Node.js, which for me was:
v0.4.12
Installing NPM and Socket.IO
Once Node.js is installed you'll need to install the Node Package Manager (NPM). This will allow you to add extra functionality to Node.js to achieve things like WebSockets communication.
You can install NPM on Mac with one line in the terminal:
curl https://npmjs.org/install.sh | sh
NPM is still fairly unsupported on Windows so you'll need to follow the manual instructions to get it working.
Make sure you're terminal is in the unpacked game directory:
cd /path/to/unpacked/game
Then install Socket.IO using NPM with the following command:
npm install socket.io
It installed successfully if you see no errors and a line that starts with something like:
socket.io@0.8.6
Setting up the game server file
The game server is run from a single file called game.js
that sits outside of the public
directory.
Open game.js
in your favourite editor, it should have a single line in it that explains that it's a placeholder file.
Remove that line and add some Node.js requirements in its place:
var util = require("util"),
io = require("socket.io");
Then add the core game variables that you'll need at a later stage:
var socket,
players;
Underneath that create the initialisation function and set the players variable to an empty array:
function init() {
players = [];
};
Finally, run the initialisation function at the bottom of the file:
init();
Configure Socket.IO
Getting Socket.IO running is really straightforward.
The first step is to get the socket server listening on a port. Add the following to the bottom of the init
function:
socket = io.listen(8000);
The second step is optional and limits Socket.IO to using WebSockets and not falling back to anything else. This step also cuts down the amount of output that Socket.IO spits into the terminal.
Add the following at the end of the init function:
socket.configure(function() {
socket.set("transports", ["websocket"]);
socket.set("log level", 2);
});
Testing the game server
You're now at a stage to test the game server.
Navigate in the terminal to the directory containing the game.js
server file, then run the following command:
node game.js
If all went well then you should see something like this:
socket.io started
If you get an error about Node.js not being able to find a module then there may be a couple of problems;
- You haven't installed Socket.IO properly
- Node.js can't find
game.js
because you're in the wrong directory
Listen for socket events
Now Socket.IO is set up you need to start listening for related events.
First, add the following call to the bottom of the init
function:
setEventHandlers();
This won't do anything yet so create the setEventHandlers
function underneath the init
function:
var setEventHandlers = function() {
socket.sockets.on("connection", onSocketConnection);
};
The inside line listens for new connections to Socket.IO, passing them off to the onSocketConnection
function which doesn't exist yet.
Create the onSocketConnection
function below setEventHandlers
and define some event listeners inside it:
function onSocketConnection(client) {
util.log("New player has connected: "+client.id);
client.on("disconnect", onClientDisconnect);
client.on("new player", onNewPlayer);
client.on("move player", onMovePlayer);
};
The client variable is passed to onSocketConnection
and is used to identify the player that just connected to the game.
Each player is identified using a unique client.id
number. We'll use this later when communicating game updates to other players. For now the util.log
line will output the client identification number to the terminal on connection.
The client.on
event listeners are used to detect when a player has either disconnected or sent a message to the server. Right now they point to handler functions that don't exist.
Add the placeholder handler functions below onSocketConnection
:
function onClientDisconnect() {
util.log("Player has disconnected: "+this.id);
};
function onNewPlayer(data) {
};
function onMovePlayer(data) {
};
In the onClientDisconnect
handler function, the this object refers to the client variable from the onSocketConnection
function. Slightly confusing but bear with it.
Create the server Player class
Before we can move on you need to create the Player
class on the server. It is much like the Player
class on the client.
Create a new file called Player.js
in the same directory as the game.js
server file.
Add the following to it:
var Player = function(startX, startY) {
var x = startX,
y = startY,
id;
var getX = function() {
return x;
};
var getY = function() {
return y;
};
var setX = function(newX) {
x = newX;
};
var setY = function(newY) {
y = newY;
};
return {
getX: getX,
getY: getY,
setX: setX,
setY: setY,
id: id
}
};
exports.Player = Player;
The only things different to the client Player
class are a couple of getter and setter functions, an id
variable, and the exports
object at the bottom.
You'll need to require the server Player
class within the game.js
file, so add the following after requiring Socket.IO:
io = require("socket.io"),
Player = require("./Player").Player;
Remember to change the semi-colon after the Socket.IO line to a comma.
Flesh out the new player listener
There's not much left to do on the server. The next task is to flesh out the logic for the new player message listener.
Add the following within the onNewPlayer
function:
var newPlayer = new Player(data.x, data.y);
newPlayer.id = this.id;
This creates a new player instance using position data sent by the connected client. The identification number is stored for future reference.
Now the new player is created you can send it to the other connected players by adding the following below the previous code:
this.broadcast.emit("new player", {id: newPlayer.id, x: newPlayer.getX(), y: newPlayer.getY()});
Now you need to send the existing players to the new player by adding the following below the previous code:
var i, existingPlayer;
for (i = 0; i < players.length; i++) {
existingPlayer = players[i];
this.emit("new player", {id: existingPlayer.id, x: existingPlayer.getX(), y: existingPlayer.getY()});
};
It's important to point out that this.broadcast.emit
sends a messages to all clients bar the one we're dealing with, while this.emit
send a message only to the client we're dealing with.
The last step is to add the new player to the players
array so we can access it later.
Add the following below the previous code:
players.push(newPlayer);
Adding Socket.IO to the client
Before we finish the server code, let's add the code to the client that creates new players. This way we can be sure things are working.
The first step is to open the index.html
file that sits within the public
directory.
Embed the Socket.IO script that is generated automatically by the Node.js server by adding the following line after the canvas
element:
<script src="https://localhost:8000/socket.io/socket.io.js"></script>
Notice how you need to include the port number that you defined on the server.
The second step is to open the game.js
file that sits within the js
directory.
Add the socket variable to the top of the file, underneath localPlayer
:
socket;
Remember to replace the semi-colon after localPlayer
with a comma after adding this line.
The next step is to initialise the socket connection to the server.
Add the following line to the end of the init
function:
socket = io.connect("https://localhost", {port: 8000, transports: ["websocket"]});
io.connect
will connect you to a Socket.IO server by using the first parameter as the server address.
The second parameter is an options
object that lets you define some specific settings. In this case you're defining the port number and limiting the connection to just WebSockets.
The final step is to listen for socket events and set up the handler functions to deal with them.
Add the following to the bottom of the setEventHandlers
function:
socket.on("connect", onSocketConnected);
socket.on("disconnect", onSocketDisconnect);
socket.on("new player", onNewPlayer);
socket.on("move player", onMovePlayer);
socket.on("remove player", onRemovePlayer);
These should make sense to you by now, they're effectively the same events that you're listening for on the server. The only difference is that the connect event is called when the client is successfully connected, rather than when a different client connects.
Likewise with the disconnect event.
Add the empty socket event handler functions below the onResize
function:
function onSocketConnected() {
console.log("Connected to socket server");
};
function onSocketDisconnect() {
console.log("Disconnected from socket server");
};
function onNewPlayer(data) {
console.log("New player connected: "+data.id);
};
function onMovePlayer(data) {
};
function onRemovePlayer(data) {
};
Make sure the game server is running by typing the following in the terminal:
node game.js
Remember, the terminal needs to be pointed to the main directory of the game for this to work.
Once the game server is running, open the index.html
file within the browser and view the JavaScript console (cmd+shift+k in Firefox on the Mac).
Refresh the page and you should see a message in the console stating that you're now connected to the socket server.
Creating a new player on the client
Before we finish the server code, let's add the code to the client that creates new players. This way we can be sure things are working.
First off you'll need to open up the Player.js
file within the public/js
directory.
Add an id
variable to the top of the file, just above moveAmount
:
id,
Next, add the same getters and setters that you used on the server. Place them below the moveAmount
variable:
var getX = function() {
return x;
};
var getY = function() {
return y;
};
var setX = function(newX) {
x = newX;
};
var setY = function(newY) {
y = newY;
};
Add then declare the getters and setters to the top of the return statement at the end of the file so we can access them:
getX: getX,
getY: getY,
setX: setX,
setY: setY,
Jump back into the game.js
file within the public
directory and let's tie everything together.
Add the remotePlayers
variable to the top of the file, just below the localPlayer
variable:
remotePlayers,
Then define remotePlayers
as an empty array at the end of the init
function, above the setEventHandlers
call:
remotePlayers = [];
Add the following to the bottom of the onNewPlayer
handler function:
var newPlayer = new Player(data.x, data.y);
newPlayer.id = data.id;
remotePlayers.push(newPlayer);
This will create a new player object based on the position data sent from the server. It then sets the identification number of the player and adds it to the remotePlayers
array so we can access it later.
Nothing will happen yet as you haven't told the server that you want it to create a new player when you connect.
To do that you need to add the following code to the onSocketConnected
handler function:
socket.emit("new player", {x: localPlayer.getX(), y: localPlayer.getY()});
You still won't be able to see anything exciting yet, so add the following code to the bottom of the draw
function:
var i;
for (i = 0; i < remotePlayers.length; i++) {
remotePlayers[i].draw(ctx);
};
This will loop through every remote player and draw it to the canvas.
Make sure the server is running and refresh the browser a few times. You should see multiple black squares appearing on the screen. If not, check to see if the JavaScript console is outputting a message that says that new players have connected.
The multiple black squares are actually a bug, but the fact that they're showing means that things are working correctly. We'll fix the bug now.
Removing players
The multiple squares were appearing because you're not removing disconnected players from the game. This is relatively easy to fix.
Jump into the game.js
server file that's located within the main directory of the game.
To remove players you need a quick way to find them in the players
array, so add the following helper function to the end of the file:
function playerById(id) {
var i;
for (i = 0; i < players.length; i++) {
if (players[i].id == id)
return players[i];
};
return false;
};
All it does is take a player identification number and loop through the players
array until it finds a matching player. That player is then returned so you can use it.
Add the following code to the onRemovePlayer
handler function:
var removePlayer = playerById(this.id);
if (!removePlayer) {
util.log("Player not found: "+this.id);
return;
};
players.splice(players.indexOf(removePlayer), 1);
this.broadcast.emit("remove player", {id: this.id});
It may look complicated but all this is doing is finding the disconnected player using its id number, then removing that player from the players
array with help from the indexOf
function.
The last line lets all the other players know that someone has disconnected and to remove that player. Let's do that part now.
Move back into the game.js
file within the public/js
directory and add a similar helper function to the bottom so you can search for players:
function playerById(id) {
var i;
for (i = 0; i < remotePlayers.length; i++) {
if (remotePlayers[i].id == id)
return remotePlayers[i];
};
return false;
};
Note that this is identical apart from that you're looking in the remotePlayers
array instead of the players array.
Add the following code to the onRemovePlayer
handler function:
var removePlayer = playerById(data.id);
if (!removePlayer) {
console.log("Player not found: "+data.id);
return;
};
remotePlayers.splice(remotePlayers.indexOf(removePlayer), 1);
This is practically identical to the onClientDisconnect
handler function on the server; it finds a player based on the id number and removes it from the array using the indexOf
function.
Restart the game server by going to the terminal and pressing ctrl+c on Mac to stop Node.js, then running the game.js
file in the same way that you've done before.
Refresh the browser a few times and you'll notice that only one black square is showing up. You've fixed the bug!
For added awesomeness, open the index.html
file in another tab or browser window. See that there are two squares now? That's multiple players in your game. Cool, ey?
But we're not done yet. If you move the players around the screen you'll notice that the position doesn't change in the other browser window. That's not what we want.
Updating player positions
The first step is to know when the local player has moved. This allows you to cut down the amount of data that's sent back and forth.
Open the Player.js
file within public/js
and add the following to the top of the update
function:
var prevX = x,
prevY = y;
And add the following code to the end of the same update
function:
return (prevX != x || prevY != y) ? true : false;
This will return true
if the position has changed, or false
if it hasn't.
Open the game.js
file in public/js
and change the update
function to the following:
if (localPlayer.update(keys)) {
socket.emit("move player", {x: localPlayer.getX(), y: localPlayer.getY()});
};
This will send the player position to the server after every update only if the player position has changed.
While you're in the client-side game.js
file, add the following to the onMovePlayer
handler function:
var movePlayer = playerById(data.id);
if (!movePlayer) {
console.log("Player not found: "+data.id);
return;
};
movePlayer.setX(data.x);
movePlayer.setY(data.y);
This will search for the player that is being moved and will update its x and y position using the setter methods you defined in the Player.js
file.
Right now nothing will happen as the server isn't transmitting the move player messages.
Open up the game.js
server file and add the following to the onMovePlayer
handler function:
var movePlayer = playerById(this.id);
if (!movePlayer) {
util.log("Player not found: "+this.id);
return;
};
movePlayer.setX(data.x);
movePlayer.setY(data.y);
this.broadcast.emit("move player", {id: movePlayer.id, x: movePlayer.getX(), y: movePlayer.getY()});
This is practically identical to the code on the client-side except that you're broadcasting the updated position to all the other players.
Restart the game server and refresh the browser windows.
Try moving around. See how the position updates on the other window? You've just completed your first multiplayer game. Congratulations!
Finished game code
Feel free to download the finished game code if you'd like to see all the comments, or even if you've just been having a few issues and want to see how it all works.
Feedback
Please let me know what other workshops and tutorials you'd like to see related to creating games with HTML5.