Please enable JavaScript to view the comments powered by Disqus.

Part 1: Multiplayer game tutorial with Phaser.js, Node.js, Socket.io, and Express. Create a game like Agar.io, and Slither.io

Set up, Sync client movements.

Introudction

Repository available here: https://github.com/dci05049/Phaser-Multiplayer-Game-Tutorial

As the title suggests, this tutorial series will introduce the basic multiplayer game architecture, and run a complete tutorial on creating a browser-based multiplayer game similar to those already existing io games (agar.io, and slither.io) using Node.js for server side and Phaser.js for the client side. The Node.js server side will use Socket.io and Express.js for communication between client and server. The client side will specifically use the p2 physics for better collision detection. In this first part of the series we will setup the developing environment, connect multiple clients to the server, and implement player movements using mouse pointer. All player movements will be in sync at the end of this tutorial.

If you are a beginner at Node.js, don't worry, important lines are commented with explanation

I also made an io game with the same technology used in this tutorial. You can check out my game at slashing.io.

Setting Up

If you don’t have Node.Js installed, you can download it from here: https://nodejs.org/en/. After installing Node, run Node.js command prompt and navigate to your project folder. After that, type npm init to create package.json. My project folder is called multiplayerGame.

First thing we will need is socket.io. Socket.io allows us to implement a real time communication between the client and the server side. Socket.io is event-driven like Node.js. For example, let’s say a client sends a message called “attack”, then the server will listen to the message “attack”, and do something about it.

Type npm install socket.io --save. --save will include the package as dependency in package.json, which we will need for deployment later on.

The next thing we need is Express.js. Express.js is a Node.js framework that lets us create Web application more easily without having the need to use the Node.js http module, in which we will have to re-implement a lot of the stuff that Express.js already has. It will make developing our multiplayer game easier.

Just like Socket.io, to install Express.js, type npm install express --save

That's it!! Now you’re ready to code

index.html

 
	<body>
		<div id="gameDiv">
		</div>
	</body>
	<script src="client/lib/phaser.js"></script>
	<script src="/socket.io/socket.io.js"></script>
	<script src="client/player.js"></script>
	<script src="client/main.js"></script>
			

In index.html, set up a container for a phaser game. Here, it’s “gameDiv”. Reference the phaser game framework, socket.io library for the client, and your game file.

Step 1:

Client Side: main.js


var socket; // define a global variable called socket 
socket = io.connect(); // send a connection request to the server

//this is just configuring a screen size to fit the game properly
//to the browser
canvas_width = window.innerWidth * window.devicePixelRatio; 
canvas_height = window.innerHeight * window.devicePixelRatio;

//make a phaser game
game = new Phaser.Game(canvas_width,canvas_height, Phaser.CANVAS,
 'gameDiv');

var gameProperties = { 
	//this is the actual game size to determine the boundary of 
	//the world
	gameWidth: 4000, 
	gameHeight: 4000,
};

// this is the main game state
var main = function(game){
};
// add the 
main.prototype = {
	preload: function() {


    },
	//this function is fired once when we load the game
	create: function () {
		console.log("client started");
		//listen to the “connect” message from the server. The server 
		//automatically emit a “connect” message when the cleint connets.When 
		//the client connects, call onsocketConnected.  
		socket.on("connect", onsocketConnected); 

	}
}

// this function is fired when we connect
function onsocketConnected () {
	console.log("connected to server"); 
}

// wrap the game states.
var gameBootstrapper = {
    init: function(gameContainerElementId){
		game.state.add('main', main);
		game.state.start('main'); 
    }
};;

//call the init function in the wrapper and specifiy the division id 
gameBootstrapper.init("gameDiv");
			

The comment briefly explains how it works. Important line is io.connect(). It basically allows clients to request a connection to the server. The server will listen to this connection request, and emit a “connect” message back to the client when it successfully connects. That's why we have the line socket.on("connect", onsocketConnected). When the client receives the connect message, we call a onsocketConnected function in which we can initialize our game later.

Server: app.js:


//import express.js 
var express = require('express');
//assign it to variable app 
var app = express();
//create a server and pass in app as a request handler
var serv = require('http').Server(app); //Server-11

//send a index.html file when a get request is fired to the given 
//route, which is ‘/’ in this case
app.get('/',function(req, res) {
	res.sendFile(__dirname + '/client/index.html');
});
//this means when a get request is made to ‘/client’, put all the 
//static files inside the client folder 
Under ‘/client’. See for more details below

app.use('/client',express.static(__dirname + '/client'));

//listen on port 2000
serv.listen(process.env.PORT || 2000);
console.log("Server started.");

 // binds the serv object we created to socket.io
var io = require('socket.io')(serv,{});

// listen for a connection request from any client
io.sockets.on('connection', function(socket){
	console.log("socket connected"); 
	//output a unique socket.id 
	console.log(socket.id);
});

			

In the server side code, we’re firstly importing the express module, and assign it to a variable app. We then use this app to create a server.

Server11 : you might be wondering what this does. In Node Http module, require('http').Server(function(requestListener) means whenever we make any request to the server, it will fire a requestListener to handle the response and request. So we’re basically passing in express object (app) as a requestListener to handle response and request.

We then set up a static file called client. Without app.use('/client',express.static(__dirname + '/client')), Node.js by default will not know what to do when we try to reference files inside the client folder in index.html. Express.js provides us a convenient function called express.static, which lets us easily access static files.

For example, in index.html, we called <script src="client/main"></script> to access a javascript game file, which will be in localhost:2000/client/main.js. app.use('/client',express.static(__dirname + '/client')) puts all static files inside our client folder into localhost:2000/client, so that we can access them.

It's time to see our game in action!! Type in node app.js to run the server. Go to the browser and type in localhost:2000. If you see a socket connected message in the console and some long ass random characters (socket id) then it’s working fine!

Wow I can output some connection message but I want to make some real game! Well now is a good time to get into socket.io. In this client and server code, we’re going to create a player object whenever client connects to the server

Step 2:

Client main.js:


var socket; 
socket = io.connect();


canvas_width = window.innerWidth * window.devicePixelRatio;
canvas_height = window.innerHeight * window.devicePixelRatio;

game = new Phaser.Game(canvas_width,canvas_height, Phaser.CANVAS, 'gameDiv');

var gameProperties = { 
	gameWidth: 4000,
	gameHeight: 4000,
	game_elemnt: "gameDiv",
	in_game: false,
};

var main = function(game){
};

//call this function when the player connects to the server.
function onsocketConnected () {
	//create a main player object for the connected user to control
	createPlayer();
	gameProperties.in_game = true;
	// send to the server a "new_player" message so that the server knows
	// a new player object has been created
	socket.emit('new_player', {x: 0, y: 0, angle: 0});
}

//the “main” player class in the CLIENT. This player is what the user controls. 
//look at this example on how to draw using graphics https://phaser.io/examples/v2/display/graphics
// documenation here: https://phaser.io/docs/2.6.2/Phaser.Graphics.html

function createPlayer () {
	//uses Phaser’s graphics to draw a circle
	player = game.add.graphics(0, 0);
	player.radius = 100;

	// set a fill and line style
	player.beginFill(0xffd900);
	player.lineStyle(2, 0xffd900, 1);
	player.drawCircle(0, 0, player.radius * 2);
	player.endFill();
	player.anchor.setTo(0.5,0.5);
	player.body_size = player.radius; 

	// draw a shape
	game.physics.p2.enableBody(player, true);
	player.body.addCircle(player.body_size, 0 , 0); 
}

main.prototype = {
	preload: function() {
		game.scale.scaleMode = Phaser.ScaleManager.RESIZE;
		game.world.setBounds(0, 0, gameProperties.gameWidth, 
		gameProperties.gameHeight, false, false, false, false);
		//I’m using P2JS for physics system. You can choose others if you want
		game.physics.startSystem(Phaser.Physics.P2JS);
		game.physics.p2.setBoundsToWorld(false, false, false, false, false)
		//sets the y gravity to 0. This means players won’t fall down by gravity
		game.physics.p2.gravity.y = 0;
		// turn gravity off
		game.physics.p2.applyGravity = false; 
		game.physics.p2.enableBody(game.physics.p2.walls, false); 
		// turn on collision detection
		game.physics.p2.setImpactEvents(true);

    },
	
	create: function () {
		game.stage.backgroundColor = 0xE1A193;;
		console.log("client started");
		//listen if a client successfully makes a connection to the server,
		//and call onsocketConnected 
		socket.on("connect", onsocketConnected); 
	},
	
	update: function () {
		// emit the player input
		
		//move the player when he is in game
		if (gameProperties.in_game) {
			// we're using phaser's mouse pointer to keep track of 
			// user's mouse position
			var pointer = game.input.mousePointer;
			
			// distanceToPointer allows us to measure the distance between the 
			// mouse pointer and the player object
			if (distanceToPointer(player, pointer) <= 50) {
				//The player can move to mouse pointer at a certain speed. 
				//look at player.js on how this is implemented.
				movetoPointer(player, 0, pointer, 100);
			} else {
				movetoPointer(player, 500, pointer);
			}	
		}
	}
}

var gameBootstrapper = {
    init: function(gameContainerElementId){
		game.state.add('main', main);
		game.state.start('main'); 
    }
};;

gameBootstrapper.init("gameDiv");


			

An important line is socket.emit('new_player', {x: 0, y: 0, angle: 0});. This is how we send a message to the server using socket.io. First parameter is the message name, and the second is the data we want to send. We simply added a circular object for the player to control when the client connects to the server. Using movetoPointer allows us to move towards a mouse pointer at a certain speed. You can look at player.js for implementation.

Server: app.js


var express = require('express');

var app = express();
var serv = require('http').Server(app);


app.get('/',function(req, res) {
	res.sendFile(__dirname + '/client/index.html');
});
app.use('/client',express.static(__dirname + '/client'));

serv.listen(process.env.PORT || 2000);
console.log("Server started.");

//this is where we will store all the players in the client,
// which is connected to the server
var player_lst = [];

// A player “class”, which will be stored inside player list 
var Player = function (startX, startY, startAngle) {
  var x = startX
  var y = startY
  var angle = startAngle
}

//onNewplayer function is called whenever a server gets a message “new_player” from the client
function onNewplayer (data) {
	//form a new player object 
	var newPlayer = new Player(data.x, data.y, data.angle);
	console.log("created new player with id " + this.id);
	player_lst.push(newPlayer); 

}


 // io connection 
var io = require('socket.io')(serv,{});

io.sockets.on('connection', function(socket){
	console.log("socket connected"); 

	//Listen to the message “new_player’ from the client
	socket.on("new_player", onNewplayer);
});


			

An important function we added in our new server side code is socket.on("new_player", onNewplayer). Notice the line “this.id” inside the function, onNewPlayer. “this” in in the function refers to “socket” in io.sockets.on. “This.id” in a callback is equivalent to “socket.id” in io.sockets.on. Socket.id is unique, meaning every connection has a different id.

When you start your game, you can now control your circle!! Great! But... it's not a multiplayer game when you don't see other players. It's time to add enemies in real time!

Step3

Client: main.js


var socket; 
socket = io.connect();


canvas_width = window.innerWidth * window.devicePixelRatio;
canvas_height = window.innerHeight * window.devicePixelRatio;

game = new Phaser.Game(canvas_width,canvas_height, Phaser.CANVAS, 'gameDiv');

//the enemy player list 
var enemies = [];

var gameProperties = { 
	gameWidth: 4000,
	gameHeight: 4000,
	game_elemnt: "gameDiv",
	in_game: false,
};

var main = function(game){
};

function onsocketConnected () {
	console.log("connected to server"); 
	createPlayer();
	gameProperties.in_game = true;
	// send the server our initial position and tell it we are connected
	socket.emit('new_player', {x: 0, y: 0, angle: 0});
}

// When the server notifies us of client disconnection, we find the disconnected
// enemy and remove from our game
function onRemovePlayer (data) {
	var removePlayer = findplayerbyid(data.id);
	// Player not found
	if (!removePlayer) {
		console.log('Player not found: ', data.id)
		return;
	}
	
	removePlayer.player.destroy();
	enemies.splice(enemies.indexOf(removePlayer), 1);
}

function createPlayer () {
	player = game.add.graphics(0, 0);
	player.radius = 100;

	// set a fill and line style
	player.beginFill(0xffd900);
	player.lineStyle(2, 0xffd900, 1);
	player.drawCircle(0, 0, player.radius * 2);
	player.endFill();
	player.anchor.setTo(0.5,0.5);
	player.body_size = player.radius; 

	// draw a shape
	game.physics.p2.enableBody(player, true);
	player.body.clearShapes();
	player.body.addCircle(player.body_size, 0 , 0); 
	player.body.data.shapes[0].sensor = true;
}

// this is the enemy class. 
var remote_player = function (id, startx, starty, start_angle) {
	this.x = startx;
	this.y = starty;
	//this is the unique socket id. We use it as a unique name for enemy
	this.id = id;
	this.angle = start_angle;
	
	this.player = game.add.graphics(this.x , this.y);
	this.player.radius = 100;

	// set a fill and line style
	this.player.beginFill(0xffd900);
	this.player.lineStyle(2, 0xffd900, 1);
	this.player.drawCircle(0, 0, this.player.radius * 2);
	this.player.endFill();
	this.player.anchor.setTo(0.5,0.5);
	this.player.body_size = this.player.radius; 

	// draw a shape
	game.physics.p2.enableBody(this.player, true);
	this.player.body.clearShapes();
	this.player.body.addCircle(this.player.body_size, 0 , 0); 
	this.player.body.data.shapes[0].sensor = true;
}

//Server will tell us when a new enemy player connects to the server.
//We create a new enemy in our game.
function onNewPlayer (data) {
	console.log(data);
	//enemy object 
	var new_enemy = new remote_player(data.id, data.x, data.y, data.angle); 
	enemies.push(new_enemy);
}

//Server tells us there is a new enemy movement. We find the moved enemy
//and sync the enemy movement with the server
function onEnemyMove (data) {
	console.log(data.id);
	console.log(enemies);
	var movePlayer = findplayerbyid (data.id); 
	
	if (!movePlayer) {
		return;
	}
	movePlayer.player.body.x = data.x; 
	movePlayer.player.body.y = data.y; 
	movePlayer.player.angle = data.angle; 
}

//This is where we use the socket id. 
//Search through enemies list to find the right enemy of the id.
function findplayerbyid (id) {
	for (var i = 0; i < enemies.length; i++) {
		if (enemies[i].id == id) {
			return enemies[i]; 
		}
	}
}

main.prototype = {
	preload: function() {
		game.stage.disableVisibilityChange = true;
		game.scale.scaleMode = Phaser.ScaleManager.RESIZE;
		game.world.setBounds(0, 0, gameProperties.gameWidth, gameProperties.gameHeight, false, false, false, false);
		game.physics.startSystem(Phaser.Physics.P2JS);
		game.physics.p2.setBoundsToWorld(false, false, false, false, false)
		game.physics.p2.gravity.y = 0;
		game.physics.p2.applyGravity = false; 
		game.physics.p2.enableBody(game.physics.p2.walls, false); 
		// physics start system
		//game.physics.p2.setImpactEvents(true);

    },
	
	create: function () {
		game.stage.backgroundColor = 0xE1A193;;
		console.log("client started");
		socket.on("connect", onsocketConnected); 
		
		//listen to new enemy connections
		socket.on("new_enemyPlayer", onNewPlayer);
		//listen to enemy movement 
		socket.on("enemy_move", onEnemyMove);
		
		// when received remove_player, remove the player passed; 
		socket.on('remove_player', onRemovePlayer); 
	},
	
	update: function () {
		// emit the player input
		
		//move the player when the player is made 
		if (gameProperties.in_game) {
			var pointer = game.input.mousePointer;
			
			if (distanceToPointer(player, pointer) <= 50) {
				movetoPointer(player, 0, pointer, 100);
			} else {
				movetoPointer(player, 500, pointer);
			}
			
					
			//Send a new position data to the server 
			socket.emit('move_player', {x: player.x, y: player.y, angle: player.angle});
		}
	}
}

var gameBootstrapper = {
    init: function(gameContainerElementId){
		game.state.add('main', main);
		game.state.start('main'); 
    }
};;

gameBootstrapper.init("gameDiv");

			

Client side structure is not very complex. We just wait for a new player message from the server and create a new instance of enemy object, and when it moves we move the position of that enemy object to the correct position (sent from the server). The findPlayerbyId function is used to find the corrent enemy by its id. When the player disconnects, we use onRemovePlayer function to find the enemy object and remove it from the game. Add the line game.stage.disableVisibilityChange = true. This means when your cursor leaves the browser, it will not sleep the browser. This means we can monitor two browsers concurrently for development.


var express = require('express');

var app = express();
var serv = require('http').Server(app);

app.get('/',function(req, res) {
	res.sendFile(__dirname + '/client/index.html');
});
app.use('/client',express.static(__dirname + '/client'));

serv.listen(process.env.PORT || 2000);
console.log("Server started.");

var player_lst = [];

//a player class in the server
var Player = function (startX, startY, startAngle) {
  this.x = startX
  this.y = startY
  this.angle = startAngle
}

// when a new player connects, we make a new instance of the player object,
// and send a new player message to the client. 
function onNewplayer (data) {
	console.log(data);
	//new player instance
	var newPlayer = new Player(data.x, data.y, data.angle);
	console.log(newPlayer);
	console.log("created new player with id " + this.id);
	newPlayer.id = this.id; 	
	//information to be sent to all clients except sender
	var current_info = {
		id: newPlayer.id, 
		x: newPlayer.x,
		y: newPlayer.y,
		angle: newPlayer.angle,
	}; 
	
	//send to the new player about everyone who is already connected. 	
	for (i = 0; i < player_lst.length; i++) {
		existingPlayer = player_lst[i];
		var player_info = {
			id: existingPlayer.id,
			x: existingPlayer.x,
			y: existingPlayer.y, 
			angle: existingPlayer.angle,			
		};
		console.log("pushing player");
		//send message to the sender-client only
		this.emit("new_enemyPlayer", player_info);
	}
	
	//send message to every connected client except the sender
	this.broadcast.emit('new_enemyPlayer', current_info);
	
	player_lst.push(newPlayer); 

}

//update the player position and send the information back to every client except sender
function onMovePlayer (data) {
	var movePlayer = find_playerid(this.id); 
	movePlayer.x = data.x;
	movePlayer.y = data.y;
	movePlayer.angle = data.angle; 
	
	var moveplayerData = {
		id: movePlayer.id,
		x: movePlayer.x,
		y: movePlayer.y, 
		angle: movePlayer.angle
	}
	
	//send message to every connected client except the sender
	this.broadcast.emit('enemy_move', moveplayerData);
}

//call when a client disconnects and tell the clients except sender to remove the disconnected player
function onClientdisconnect() {
	console.log('disconnect'); 

	var removePlayer = find_playerid(this.id); 
		
	if (removePlayer) {
		player_lst.splice(player_lst.indexOf(removePlayer), 1);
	}
	
	console.log("removing player " + this.id);
	
	//send message to every connected client except the sender
	this.broadcast.emit('remove_player', {id: this.id});
	
}

// find player by the the unique socket id 
function find_playerid(id) {

	for (var i = 0; i < player_lst.length; i++) {

		if (player_lst[i].id == id) {
			return player_lst[i]; 
		}
	}
	
	return false; 
}

 // io connection 
var io = require('socket.io')(serv,{});

io.sockets.on('connection', function(socket){
	console.log("socket connected"); 
	
	// listen for disconnection; 
	socket.on('disconnect', onClientdisconnect); 
	
	// listen for new player
	socket.on("new_player", onNewplayer);
	// listen for player position update
	socket.on("move_player", onMovePlayer);
});

			

In the server side, we added onMovePlayer where we update the position of the player. Notice that we have “this.broadcast.emit”. This means that we are sending the data to every socket connected except the sender. Another type of message transmission we have is “this.emit”, which means “only send to the sender”. In onNewPlayer we do two things. First, when a new player connects, we have to send to that specific new player about everyone who is already connected to the game before. Secondly, we have to send everyone who is already connected (not the new player) about the new player.

You can use this socket.io cheat sheet to send to specific clients from the server: https://socket.io/docs/emit-cheatsheet/

However, this is a very naive and dangerous implementation for a multiplayer because the client is sending a position directly from the client. Imagine for an instance a hacker figures out this naive implementation and tries to send a different position to the server. Everyone will suffer a poor gameplay experience from a cheater! We will try to fix this naive implementation in the next tutorial!

Part 2 >>