How To Build A Multiplayer Browser Game (Part 1)

Written by omgimanerd | Published 2016/12/20
Tech Story Tags: nodejs | javascript | game-development | games | software-development

TLDRvia the TL;DR App

Tank Anarchy

Back in 2014, I went to my first CodeDay in NYC. Though CodeDay isn’t exactly a hackathon, it was my first experience with hackathon-like events. At this event, I built a multiplayer tank battle game with my friend Kenneth Li. Since a few of my friends have expressed interest in how I built it, I thought I’d document the process here.

In this post, I’ll run through a quick overview of my thought process and give an walkthrough of how to replicate the architecture, as well as providing tips and tricks if you want to try doing this yourself. This post assumes you have knowledge of the basics of JavaScript and node.js. If you don’t, there are a lot of great online resources to learn the above.

I built this game on a node.js backend using WebSockets to provide real time communication between the clients and the server. The game itself was rendered on an HTML5 canvas on the client side. To get started, you’ll of course need node.js. For this post, I’ll be using node.js version 6.3.1, but you can follow along with almost any version (above 0.12) and it should be fine.

Let’s get started with dependencies first. Create a directory for your project and run the following inside it:

npm initnpm install --save express socket.io

We’ll be using the Express framework to quickly set up the server, and the socket.io package to handle WebSockets on the server. Put the following in a file named server.js:

// Dependenciesvar express = require('express');var http = require('http');var path = require('path');var socketIO = require('socket.io');

var app = express();var server = http.Server(app);var io = socketIO(server);

app.set('port', 5000);app.use('/static', express.static(__dirname + '/static'));

// Routingapp.get('/', function(request, response) {response.sendFile(path.join(__dirname, 'index.html'));});

// Starts the server.server.listen(5000, function() {console.log('Starting server on port 5000');});

The code above is a pretty standard node.js server using the Express framework. It sets up dependencies and the basic routing for the server. For this demo application, we’re only going to serve a single index.html and static directory. Create a directory named static and an index.html file in the root project folder. The HTML file is pretty basic so we’ll write that now:

<html><head><title>A Multiplayer Game</title><style>canvas {width: 800px;height: 600px;border: 5px solid black;}</style><script src="/socket.io/socket.io.js"></script></head><body><canvas id="canvas"></canvas></body><script src="/static/game.js"></script></html>

For larger scale projects, you should put CSS styles in a separate dedicated stylesheet. You might also have more UI and display elements. For simplicity, I’m going to keep the CSS in the HTML code. Note that I’ve also included a socket.io.js script. This is automatically be provided by the socket.io package when the server is hosted.

Now we’ll do some setup on the server for the WebSockets. Add this line to the end of server.js:

// Add the WebSocket handlersio.on('connection', function(socket) {});

We don’t have any functionality for it yet, so just leave this as is for now. For testing, add the following lines to the end of server.js:

setInterval(function() {io.sockets.emit('message', 'hi!');}, 1000);

This will send a message to all connected sockets with the name ‘message’ and the content ‘hi’. Remember to remove this snippet later since it’s just for testing.

Create a file called game.js in the static directory. We can write a quick function to log the messages from the server in order to verify that we’re receiving them. Put the following in static/game.js:

var socket = io();socket.on('message', function(data) {console.log(data);});

Run the server (using the command node server.js) and navigate to http://localhost:5000 in any web browser. You’ll notice a message appear every second if you open the developer console (Right Click -> Inspect).

In general, socket.emit(name,data) will send a message with the given name and data to the server if it is called on the client, and vice versa if it is called on the server. To listen for messages with a specific name, you need to create an event handler that looks like this:

socket.on('name', function(data) {// data is a parameter containing whatever data was sent});

You can send just about anything using socket.emit(). You can pass JSON objects as well into the data parameter, which is super handy for us. This allows us to send game information back and forth between the server and clients instantaneously, forming the backbone of most of the multiplayer functionality.

Let’s have the client send some keyboard states. Put the following code at the end of static/game.js:

var movement = {up: false,down: false,left: false,right: false}document.addEventListener('keydown', function(event) {switch (event.keyCode) {case 65: // Amovement.left = true;break;case 87: // Wmovement.up = true;break;case 68: // Dmovement.right = true;break;case 83: // Smovement.down = true;break;}});document.addEventListener('keyup', function(event) {switch (event.keyCode) {case 65: // Amovement.left = false;break;case 87: // Wmovement.up = false;break;case 68: // Dmovement.right = false;break;case 83: // Smovement.down = false;break;}});

This is some basic code for an input handler to track when the WASD keys are pressed. After this bit, we’ll add a message to alert the server that a new player has joined and create a loop to constantly send their keyboard input to the server.

socket.emit('new player');setInterval(function() {socket.emit('movement', movement);}, 1000 / 60);

This will send the keyboard state of this client 60 times a second to the server. Now we need to handle this input on the server. Add the following to the end of the server.js:

var players = {};io.on('connection', function(socket) {socket.on('new player', function() {players[socket.id] = {x: 300,y: 300};});socket.on('movement', function(data) {var player = players[socket.id] || {};if (data.left) {player.x -= 5;}if (data.up) {player.y -= 5;}if (data.right) {player.x += 5;}if (data.down) {player.y += 5;}});});

setInterval(function() {io.sockets.emit('state', players);}, 1000 / 60);

Let’s break this down. We’re going to store all connected players as JavaScript dictionaries (JSON objects). Since each socket connected to the server has a unique ID, we can use that ID to identify the players as well. Each key in the dictionary will be the socket ID of the connected player’s socket, and the value will be another dictionary containing an x and y position.

When the server receives a ‘new player’ message, it will add a new entry into the players object using the ID of the socket that sent that message. When the server receives a ‘movement’ message, it will update the player associated with that socket (if one exists).

io.sockets.emit() is a call that will send the given message and data out to ALL connected sockets. The server will be sending out its state to all connected clients 60 times a second.

At this point, the client doesn’t really do anything with this information, so lets add a handler on the client side to draw the data on the server to the HTML5 canvas. Add this code to the end of static/game.js:

var canvas = document.getElementById('canvas');canvas.width = 800;canvas.height = 600;var context = canvas.getContext('2d');socket.on('state', function(players) {context.clearRect(0, 0, 800, 600);context.fillStyle = 'green';for (var id in players) {var player = players[id];context.beginPath();context.arc(player.x, player.y, 10, 0, 2 * Math.PI);context.fill();}});

This code accesses the canvas and draws to it. Each time a ‘state’ message is received from the server, the client will clear the canvas and redraw all the players as a green circle on the canvas.

Any client that connects will now be able to draw the state of all connected players onto the canvas. Run the server (again using the command node server.js) and open two tabs in your web browser. If you navigate to http://localhost:5000, you should see behavior similar to this:

That’s pretty much it! If you had trouble following along, here’s a link to a repository containing this minimal implementation.

Tips and Tricks

If you were making a real game, it would be a much better idea to refactor a lot of the code used in this demonstration into their own files.

These multiplayer games are pretty good examples of MVC architecture. All the game logic should be handled on the server, and the only thing the client should do is send user input to the server and render the information the server sends.

There are a few flaws with this demo project though. The game updating is tied to the socket listener. If I wanted to mess with the game state, I could type the following into the inspector:

while (true) {socket.emit('movement', { left: true });}

Depending on the computer, movement data is now being sent to the server much more than 60 times a second, causing the player to start moving insanely fast. This leads me to another point known as the concept of authoritative server determination.

At no point should the client have control over any data on the server. For example, you should never have code on the server that allows the client to set their position/health from data passed through the socket since a user can easily falsify a message emitted from a socket as demonstrated above.

When I built my first multiplayer game, I coded it so that a player would shoot whenever a ‘shoot’ message was sent, which was tied to a mouse down event on the client side. A clever player was able to exploit this by injecting a line of JavaScript very similar to the one above to gain near-infinite shooting speed.

The best analogy I can draw is that the clients should only send intents to the server, which are then processed and used to modify the state of the players if they are valid.

Ideally, the update loop on both the client and the server should be independent of the sockets. Try not to have your game update inside of a socket.on() block because you can have a lot of wonky inconsistent behavior since your game updating will be tied to your socket updating.

Also, try to avoid code like this:

setInterval(function() {// code ...player.x += 5;// code ...}, 1000 / 60);

The player’s x position update is tied to the frame rate of the game in this code snippet. setInterval() is not always guaranteed to run at the given interval, especially if the function running is computationally intensive and takes more than a 60th of a second to run. Rather, you should do something like this:

var lastUpdateTime = (new Date()).getTime();setInterval(function() {// code ...var currentTime = (new Date()).getTime();var timeDifference = currentTime - lastUpdateTime;player.x += 5 * timeDifference;lastUpdateTime = currentTime;}, 1000 / 60);

This is a lot clunkier, but will guarantee smoother and more consistent behavior by calculating the time between the last update and the current time to figure out the proper distance to move the player. Fork the demo project and try to implement the code above. Put some functionality into and try to make a full-fledged game.

Another thing to implement might be removing disconnected players. When a socket disconnects, a message named ‘disconnect’ is automatically sent, so you can listen for it using:

io.on('connection', function(socket) {// other handlers ...socket.on('disconnect', function() {// remove disconnected player});});

Try writing your own physics engine on the server as well, that’s a lot of fun and it’s a great challenge. If you want to try this, I highly recommend reading The Nature of Code, since it provides a lot of useful insight.

If you want to see a much more high level example, here’s a multiplayer game I’ve made, as well as a link to its source code if you want to peruse how I did it. That’s all I have for now. Thanks for reading! Please hit the clap button down below if you enjoyed this article :)

EDIT: I’ve published part 2 to this article!

Follow me on Twitter: @omgimanerd


Published by HackerNoon on 2016/12/20