In this tutorial, we will take a look at how to build a multiplayer game using Javascript and NodeJS. My goal is to take you through the basic setup so that you can begin focusing on the actual gameplay. When building a full-scale MMO, there are a lot of things to consider like load balancing, network contingencies, etc. For now, we are going to focus on the bare minimum just to help you get an understanding of how you can creatively and effectively use the available tools of NodeJS to get started.
Overview
We are first going to put together a NodeJS server. This server will host our application (index.html) and also communicate with the web page via web sockets. Then we will create a simple HTML5 page with a canvas. The canvas will draw all the players (as squares) on the map. When the player connects, he will receive spawn on the map at a random position with a random color. The player will also be able to move freely around the map using the WASD keys. Finally, when a player disconnects we must make sure that we remove him from the map. So let’s build a Multiplayer game using Javascript!
Putting Together the Server
Begin by creating a new NodeJS project, and a file called app.js. Afterward, install the following dependencies:
npm install express --save
npm install socket.io --save
We will be using express to server our web application and socket io for its web sockets functionality. Another reason that I chose socket.io is that it’s able to fall back to long-polling if your browser doesn’t support web sockets. It is also capable of gracefully handling network reconnects on signal disruption. Put the following in app.js:
const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
const socketClients = require('./controllers/client')(io);
app.use(express.static('public'));
server.listen(3000, () => console.log('Game running'));
The code above is creating an express server and binding it together with socket.io. We are also exposing a folder called public. Inside of that folder, we will put all the files of our web application. Also, note my use of the following line:
const socketClients = require('./controllers/client')(io);
To keep everything clean, I am going to put all of my socket.io event handlers inside of another file that called client.js. This file will be located in a folder called controllers.
Webpage
Inside of the public folder, create a file called index.html. This will be the HTML for our main application. Put the following in that file:
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js">
<!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>MMORPG Game</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="./styles/games.css" type="text/css" />
</head>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<canvas id="canvas"></canvas>
<script src="http://192.168.50.48:3000/socket.io/socket.io.js"></script>
<script src="./scripts/player.js"></script>
<script src="./scripts/game.js"></script>
</body>
</html>
Most of the above is simple HTML5 boilerplate code. You will notice that I created a canvas in the body but I did not give it a height or width. We will define that in our javascript code. Also, the following line loads the socket.io client library:
<script src="http://192.168.50.48:3000/socket.io/socket.io.js"></script>
I am only planning on running this locally, so I am not worried about the local IP address. Keep in mind that if you want to test this on other devices, you will not be able to use localhost because it will be local to the client’s device. Therefore, for testing on another device on my network, I need to define the IP of the computer that’s running NodeJS.
I also load two additional scripts. One for the player and another one for the game.
Finally, I have a simple **game.css **which contains the following:
canvas {
background-color: #000;
position: absolute;
top:0;
bottom: 0;
left: 0;
right: 0;
margin:auto;
}
Player Class
Let’s create a simple player class. This will go in the player.js file.We will put this file under **public –> scripts –> player.js. **Because we are not going to use any transpiling tools, we are going to stick with constructor functions. Here are the contents of this file:
function Player() {
this.id = '';
this.x = 0;
this.y = 0;
this.width = 50;
this.height = 50;
this.color = '#666';
}
The properties of each player will get filled by the server.
Game Class
The final file that we need for our application is the game class. We will put this file under **public –> scripts –> game.js. **Let’s break this file down into parts:
const socket = io.connect('http://192.168.50.48:3000');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 0;
canvas.height = 0;
const myPlayer = new Player();
let otherPlayers = [];
The first line connects to socket.io running on our NodeJS server. The next couple of lines define the canvas. Finally, we initialize a new player along with an array of other players. Let’s add some more:
(function () {
gameLoop();// Start the cycle
}());
function gameLoop() {
window.requestAnimationFrame(gameLoop);
drawRect();
}
This is the recommended way of initiating a game loop in Javascript. You might have seen many bad examples of using while(true), but don’t do that. We call a self-invoking function which then calls our gameLoop function. Inside of the gameLoop() we call this cool thing called requestAnimationFrame. Basically, this hooks into the browser’s refresh cycle and runs on every refresh. This is better because it’s non-UI blocking and doesn’t drain your system’s resources. We also call this function drawRect() which does that actual painting.
Here’s the drawRect() function:
function drawRect() {
ctx.clearRect(0, 0, 800, 600);
ctx.fillStyle = myPlayer.color; // Fill color of rectangle drawn
ctx.fillRect(myPlayer.x, myPlayer.y, myPlayer.width, myPlayer.height); // draw main player
otherPlayers.forEach((p) => {
ctx.fillStyle = p.color;
ctx.fillRect(p.x, p.y, 50, 50);
});
}
This function clear the canvas and draws the main player along with all the other players in the otherPlayers array. Now, let’s add in some movement:
function move(direction) {
socket.emit('requestmove', direction);
}
// move rectangle inside the canvas using arrow keys
window.onkeydown = function (event) {
const keyPr = event.keyCode; // Key code of key pressed
if (keyPr === 68) {
move(0);
} else if (keyPr === 65) {
move(180);
} else if (keyPr === 87) {
move(90);
} else if (keyPr === 83) {
move(270);
}
};
Let’s focus first on the onKeydown function. On the key press, it’s sending in a direction to the move function. This direction correspodings to left, up, right or down (0,180,90,270). The move function sends a request to our server that we want to move, along with the direction. When developing a game (or any web application really) you need to remember that the client is not a trusted source. So you should take information from the client (like increase the x coordinate) and just pass it to the server. It’s better if you just pass the server the intended input, and let the server decide whether it will grant your request. That allows the server to also be able to validate the coordinates before moving and checking for collision if you wanted.
Finally, let’s add the Socket.IO event listeners:
socket.on('create', (data) => {
canvas.width = data.canvasWidth;
canvas.height = data.canvasHeight;
myPlayer.id = data.id;
});
socket.on('gamedata', (data) => {
console.log(data);
otherPlayers = [];
data.forEach((p) => {
if (p.id == myPlayer.id) {
myPlayer.x = p.x;
myPlayer.y = p.y;
myPlayer.width = p.width;
myPlayer.height = p.height;
myPlayer.color = p.color;
} else {
otherPlayers.push(p);
}
});
});
The oncreate event listener sets the initial height and width of the canvas along with the player id. We will trigger this event in our server after a client successfully connects. The game data listener gets information for all players and puts them an array. If the player.id is equal to your player, it will update your player’s coordinates. We also get the color and dimensions from the server.
Game Settings:
Now let’s move back to the server side. We are going to create a simple file which will hold the universal game settings. Create a folder called controllers and create a file called gameSettings.js in that folder.** **Put the following in it:
module.exports = {
playerWidth: 50,
playerHeight: 50,
gameWidth: 800,
gameHeight: 600,
fps: 16, //16.6 ms for about 60 fps
};
Player Controller
Now create a file called player.js and put it in controllers (controllers –> player.js). The player controller will contain all the functions that we will need to interact with the player from the server-side:
const gameSettings = require('./gameSettings');
const players = [];
module.exports = {
getRandomColor() {
const color = [0, 0, 0];
for (let i = 0; i < 3; i += 1) {
color[i] = Math.floor(Math.random() * (256 - 0)) + 0;
}
return color;
},
getRandomPosition() {
const x = Math.floor(Math.random() * (gameSettings.gameWidth - gameSettings.playerWidth)) + gameSettings.playerWidth;
const y = Math.floor(Math.random() * (gameSettings.gameHeight - gameSettings.playerHeight)) + gameSettings.playerHeight;
return [x, y];
},
We first import the gameSettings controller and create two functions. getRandomColor() and getRandomPosition(). The getRandomColor() returns an array of 3 random values for rgb. The getRandomPosition() returns a random x,y array that the player will spawn at. Let’s add some more functionality:
addPlayer(id) {
const xyCoordinates = this.getRandomPosition();
const rgbArray = this.getRandomColor();
if (!this.exisitingPlayer(id)) {
players.push({
id,
width: gameSettings.playerWidth,
height: gameSettings.playerHeight,
x: xyCoordinates[0],
y: xyCoordinates[1],
color: `rgba(${rgbArray[0]},${rgbArray[1]},${rgbArray[2]},1)`,
});
}
},
move(id, direction) {
const player = this.getPlayerById(id);
if (player) {
switch (direction) {
case 0:
if ((player.x + 20) < (gameSettings.gameWidth - (gameSettings.playerWidth / 2))) { player.x += 20; }
break;
case 90:
if ((player.y - 20) > 0) { player.y -= 20; }
break;
case 180:
if ((player.x - 20) > 0) { player.x -= 20; }
break;
case 270:
if ((player.y + 20) < (gameSettings.gameHeight - (gameSettings.playerWidth / 2))) { player.y += 20; }
break;
default:
console.log('no action');
}
}
},
getPlayerById(id) {
let player;
players.forEach(((element, index, array) => {
if (element.id == id) {
player = array[index];
}
}));
return player;
},
exisitingPlayer(id) {
let existing = false;
players.forEach(((element) => {
if (element.id == id) {
existing = true;
}
}));
return existing;
},
getAllplayers() {
return players;
},
removePlayer(id) {
let index = -1;
for (let i = 0; i < players.length; i++) {
if (players[i].id == id) {
index = i;
}
}
players.splice(index, 1);
},
};
The addPlayer() checks if the id already exists, if it doesn’t, it adds a new player to the array of players. The move() function detects the direction that the player wants to and updates the coordinates if it’s valid. The getPlayerById gets the player by the specified Id and returns it (as an object reference) to the move() function to use. The removePlayer() removes a player from the array.
Client Controller
Finally, create that file called **client.js **(which we specified in the beginning of this tutorial) inside of the controller folder. In this file, we will put all the events for Socket.IO. Begin by writing the following:
const playerController = require('./player');
const gameSettings = require('./gameSettings');
module.exports = function (io) {
setInterval(() => {
io.sockets.emit('gamedata', playerController.getAllplayers());
}, gameSettings.fps);
//more to come......
Like before, we import the game settings. We also import the player controller. Inside of the module.exports we create a setInterval function so that we can send game updates each on a defined interval to all clients. This will be the game loop for our server. You will notiice that gameSettings.fps is defined for 16 ms, which roughly corresponds to 60 fps. This post will explain why.
Finally, let’s add the rest of the event handlers:
io.on('connection', (socket) => {
console.log(`New connection ${socket.id}`);
socket.emit(
'create',
{
canvasWidth: gameSettings.gameWidth,
canvasHeight: gameSettings.gameHeight,
id: socket.id
}
);
playerController.addPlayer(socket.id);
socket.on('requestmove', (direction) => {
playerController.move(socket.id, direction);
});
socket.on('disconnect', () => {
console.log('Client left');
playerController.removePlayer(socket.id);
});
});
};
It might look a little messy, but we only define two event handlers. When the client connects, we emit out a create event with the specified canvas height and width along with the player id (socket id in this case). Next, when the client called requestmove we send the ID and direction to our player controller. Remeber that every 16ms game data is submitting all the players’ updated coordinates. Finally, we handle the client disconnect event.
That’s it! When you run the project, you will have a working MMO Game in Javascript and NodeJS. Now all you need to do is add in some actual gameplay and objectives.
You can find the full project on GitHub.