Simple Multiplayer Game with Socket.io Tutorial — Part One: Setup and Movement

Jason Yang
7 min readMay 10, 2018

--

I’ve been an avid gamer throughout my entire life, from consoles to the browser. So when I began learning javascript, building my own game was high on the list of personal projects I wanted to attempt.

I’m by no means an expert on creating HTML5 games or working with socket.io, but I’d like to share what I’ve learned so far to hopefully help out others looking to get some familiarity with working with socket.io in the context of a game.

This tutorial assumes you know the basics of javascript and Node.js/express, along with how the canvas API works (p.s. here’s a great beginner friendly guide on canvas by Mozilla).

I’d also like to give credit to Alvin Lin, who’s own guide on multiplayer games has served as a great reference for when I was learning.

What is socket.io and why do we need it?

Socket.io is a library that contains an API for Websockets and also Node.js. Think of it as a library that somewhat simplifies and adds convenience towards interacting with Websockets.

Ok, so what are Websockets? Websockets is an API that allows for persistent connections between clients and servers. Both parties can send and receive data at any time until the connection is closed (versus a traditional HTTP connection which closes after the response is sent).

An example you can use to compare HTTP versus Websockets, is imagine a scenario where you and a friend are working remotely on a project together:

  • HTTP would be like if every time you or your friend wanted to share information, you’d have to call each other and then hang up. This is great if you just need to share something every once in a while. This becomes rather tedious if the required frequency is much higher however.
  • Websockets would be like if you or your friend still called each other, but just didn’t hang up and left the line open until the project was done.

Here’s the game we’re eventually going to make:

It’s a simple 2D game where the player controls a blue box across the screen and tries to collect gold ‘coins’. Each coin is worth 1 point on the scoreboard.

Now, let’s begin!

Setup

Click here for a convenient starting boilerplate. It includes everything you need to build a game with Canvas, Socket.io and Express. It also comes with webpack set up as well.

Let’s review some of the code:

index.html:

Everything here should be pretty self-explanatory. Take note of the script tag in the header for socket.io, which allows our client side code to access an io global.

<script src="/socket.io/socket.io.js"></script>

server/app.js:

Again, most of the code is a standard server set up with express. The only extra addition is near the bottom. The io here represents the socket.io server instance. It then listens for a connection event, where it will proceed to console log a message along with something called a socket.id which represents an id given to each connecting socket.

The next line of code displays a similar pattern, except it’s using the socket instead of io. It’s listening for a ‘disconnect’ event which will then trigger a callback that sends another message.

io.on('connection', (socket) => {
console.log('a user connected:', socket.id);
socket.on('disconnect', function() {
console.log('user disconnected');
});
});

client/index.js

There’s not much code here yet. Simply some convenient constants for canvas and socket.io.

Go ahead and try ‘npm start’ on the console to get everything up and running. If you go check out your localhost:3000, you should see a connection message printed on the console along with a disconnect message if you close the browser window.

Before we go any further with this code, we first need to carefully think about the multiplayer nature of our game. What type of data is going to be involved? Is it static (e.g. ‘one and done’?) or does it need to have some sort of persistent state (e.g. player location, score counter).

For a simple game, the architectural logic you want for your code would be:

  • Have the server side store the game ‘state’, be able to modify the state, and also receive updates from the players
  • Have the client side be able to receive and render the ‘game’ state sent by the server and also send out player actions to the server

For part one, let’s set a goal of being able to render a player ‘box’ when a player joins and also be able to move it around.

Creating a game state

First, we need to create something to store our game state. Since we don’t need much data right now for our game, we can simply use something like this:

const gameState = {
players: {}
}

The players object will serve as a hash to store our player objects. Please add this to the server/app.js file (remember, we want our server to be ‘responsible’ for state).

Rendering a player

Next, we need to figure out how and when do we add in a new player. This is where socket.io comes in handy. Take a moment to think about what kind of data we need to receive from the game state in order to ‘draw’ a player.

Next, go to your client/index.js file and add in this line:

socket.emit('newPlayer');

The emit function behaves what you would expect from it’s namesake. It ‘emits’ a custom event that a server can listen for. For the code above, all we’re simply doing is emitting an event called ‘newPlayer’ once at the start of our client code.

Now back to our server code again, let’s build something that’ll react to this event. Inside your io code block, insert:

socket.on('newPlayer', () => {
gameState.players[socket.id] = {
x: 250,
y: 250,
width: 25,
height: 25
}
}

The above code will now listen for a ‘newPlayer’ event and upon receiving it, will leverage our player hash and create a unique player entry based on their id. But wait a second…are we missing something?

Of course, we also need to make sure to keep proper order of our gameState by deleting players when we leave. This will help prevent ‘ghost’ players that may be rendered even though they’re no longer there. Add in the below code inside the ‘disconnect’ callback function:

delete gameState.players[socket.id]

As for the data we send along inside that object, it contains all the necessary information we need for canvas to render it out (coordinates + dimensions).

After the io code block, add in a small function:

setInterval(() => {
io.sockets.emit('state', gameState);
}, 1000 / 60);

This function basically ‘emits’ the gameState object to all sockets at a rate of 60 times per second.

Now, let’s go back to our client code and create a function that’ll help with drawing the player onto the canvas.

const drawPlayer = (player) => {
ctx.beginPath();
ctx.rect(player.x, player.y, player.width, player.height);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
};

Once we have that, we can now create something like:

socket.on('state', (gameState) => {
for (let player in gameState.players) {
rawPlayer(gameState.players[player])
}
}

Adding movement

How can we add movement to the game? Well, let’s think about it. The current game state stores the player’s x and y coordinates which determine where it appears on the canvas right? So, in order to ‘move’ a player, we need to be able to send updates to the server telling it to adjust the x and y coordinate.

This will be a similar pattern to what we’ve done before using socket.on and socket.emit. Here we can leverage the document event listener on our client side to listen for key strokes (such as pressing the arrow buttons).

If we can listen in on that, then we can send data to the server telling it when something like an up arrow has been pressed. The server can then take that data and perhaps change the player’s y coordinate.

Here’s how the code will look. First, a keyDown and keyUp handler along with a data value to represent the ‘key’ states. This will go in our client/index.js file.

const playerMovement = {
up: false,
down: false,
left: false,
right: false
};
const keyDownHandler = (e) => {
if (e.keyCode == 39) {
playerMovement.right = true;
} else if (e.keyCode == 37) {
playerMovement.left = true;
} else if (e.keyCode == 38) {
playerMovement.up = true;
} else if (e.keyCode == 40) {
playerMovement.down = true;
}
};
const keyUpHandler = (e) => {
if (e.keyCode == 39) {
playerMovement.right = false;
} else if (e.keyCode == 37) {
playerMovement.left = false;
} else if (e.keyCode == 38) {
playerMovement.up = false;
} else if (e.keyCode == 40) {
playerMovement.down = false;
}
};

Next, we’ll add in the listeners and emit function:

setInterval(() => {
socket.emit('playerMovement', playerMovement);
}, 1000 / 60);
document.addEventListener('keydown', keyDownHandler, false);
document.addEventListener('keyup', keyUpHandler, false);

Finally, we’ll finish with the logic on the back end to receive the movement data and manipulate the corresponding player coordinates:

socket.on('playerMovement', (playerMovement) => {
const player = gameState.players[socket.id]
const canvasWidth = 480
const canvasHeight = 320

if (playerMovement.left && player.x > 0) {
player.x -= 4
}
if (playerMovement.right && player.x < canvasWidth - player.width) {
player.x += 4
}

if (playerMovement.up && player.y > 0) {
player.y -= 4
}
if (playerMovement.down && player.y < canvasHeight - player.height) {
player.y += 4
}
})

Let’s see it all in action now! Do another ‘npm start’ to compile our bundle.js and start the server. We should now (hopefully if everything went right) be able to load the browser and have a player join along with it being able to move around using the arrow keys.

Woohoo! Stay tuned for part two where we delve more into some basic game mechanics like collision and scoring.

--

--