Tutorial : Creating a basic multiplayer game with Phaser and Eureca.io
The goal of this tutorial is to show you how to create basic multiplayer HTML5 game.
I’ll use Phaser for game developement and Eureca.io for client/server communication.
for this tutorial I suppose you already have some knowledge about HTML5 game developement (with Phaser in preference)
I also suppose that you have some knowledge about nodejs and that you already installed it.
The game code I’ll use is based on Tanks example game from phaser website.
This video shows the final result
let’s start 🙂
First step : refactoring the code
Some modifications and simplifications was made to make it suitable for multiplayer.
I factorized player and enemy tanks code in a class named Tank (this is the renamed EnemyTank Class from phaser example code) since in multiplayer mode enemy tanks are just remote players.
creation and update code was moved to Tank class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
// Tank = function (index, game, player) { this.cursor = { left:false, right:false, up:false, fire:false } this.input = { left:false, right:false, up:false, fire:false } var x = 0; var y = 0; this.game = game; this.health = 30; this.player = player; this.bullets = game.add.group(); this.bullets.enableBody = true; this.bullets.physicsBodyType = Phaser.Physics.ARCADE; this.bullets.createMultiple(20, 'bullet', 0, false); this.bullets.setAll('anchor.x', 0.5); this.bullets.setAll('anchor.y', 0.5); this.bullets.setAll('outOfBoundsKill', true); this.bullets.setAll('checkWorldBounds', true); this.currentSpeed =0; this.fireRate = 500; this.nextFire = 0; this.alive = true; this.shadow = game.add.sprite(x, y, 'enemy', 'shadow'); this.tank = game.add.sprite(x, y, 'enemy', 'tank1'); this.turret = game.add.sprite(x, y, 'enemy', 'turret'); this.shadow.anchor.set(0.5); this.tank.anchor.set(0.5); this.turret.anchor.set(0.3, 0.5); this.tank.id = index; game.physics.enable(this.tank, Phaser.Physics.ARCADE); this.tank.body.immovable = false; this.tank.body.collideWorldBounds = true; this.tank.body.bounce.setTo(0, 0); this.tank.angle = 0; game.physics.arcade.velocityFromRotation(this.tank.rotation, 0, this.tank.body.velocity); }; Tank.prototype.update = function() { for (var i in this.input) this.cursor[i] = this.input[i]; if (this.cursor.left) { this.tank.angle -= 1; } else if (this.cursor.right) { this.tank.angle += 1; } if (this.cursor.up) { // The speed we'll travel at this.currentSpeed = 300; } else { if (this.currentSpeed > 0) { this.currentSpeed -= 4; } } if (this.cursor.fire) { this.fire({x:this.cursor.tx, y:this.cursor.ty}); } if (this.currentSpeed > 0) { game.physics.arcade.velocityFromRotation(this.tank.rotation, this.currentSpeed, this.tank.body.velocity); } else { game.physics.arcade.velocityFromRotation(this.tank.rotation, 0, this.tank.body.velocity); } this.shadow.x = this.tank.x; this.shadow.y = this.tank.y; this.shadow.rotation = this.tank.rotation; this.turret.x = this.tank.x; this.turret.y = this.tank.y; }; |
also added a Tank.kill method to remove a tank from game scene and moved the fire() code to Tank.fire
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Tank.prototype.fire = function(target) { if (!this.alive) return; if (this.game.time.now > this.nextFire && this.bullets.countDead() > 0) { this.nextFire = this.game.time.now + this.fireRate; var bullet = this.bullets.getFirstDead(); bullet.reset(this.turret.x, this.turret.y); bullet.rotation = this.game.physics.arcade.moveToObject(bullet, target, 500); } } Tank.prototype.kill = function() { this.alive = false; this.tank.kill(); this.turret.kill(); this.shadow.kill(); } |
I also removed the damage handling stuff for simplification, so shooting other tanks will never kill them.
one last change consisted in the way player Input is handled.
the example code use phaser.input namespace directly (through game.input.keyboard.createCursorKeys() )
but in our case, the client should not handle any input directly, we’ll see why later.
so I created an Tank.input object and Tank.cursor Object to handle player inputs.
and this is the code of modified update() function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// function update () { player.input.left = cursors.left.isDown; player.input.right = cursors.right.isDown; player.input.up = cursors.up.isDown; player.input.fire = game.input.activePointer.isDown; player.input.tx = game.input.x+ game.camera.x; player.input.ty = game.input.y+ game.camera.y; turret.rotation = game.physics.arcade.angleToPointer(turret); land.tilePosition.x = -game.camera.x; land.tilePosition.y = -game.camera.y; for (var i in tanksList) { if (!tanksList[i]) continue; var curBullets = tanksList[i].bullets; var curTank = tanksList[i].tank; for (var j in tanksList) { if (!tanksList[j]) continue; if (j!=i) { var targetTank = tanksList[j].tank; game.physics.arcade.overlap(curBullets, targetTank, bulletHitPlayer, null, this); } if (tanksList[j].alive) { tanksList[j].update(); } } } } |
you can download the refactored code here to help you follow next tutorial steps.
Download refactored Tanks Game code
Now that we have a simple code that work, let’s transform it to a multiplayer game.
I’ll use Eureca.io, an RPC library I developed for internal needs, and decided to open source it (source code available here https://github.com/Ezelia/eureca.io)
the same principles here apply to any other networking library (socket.io, engine.io …etc) but you’ll see how Eureca.io makes things simpler 🙂
The web server
We will start by creating a basic web server for our game.
here we’ll install express library for nodejs, to make file service simpler, and it’s compatible with eureca.io.
express will also help you if you are building some webpages for your multiplayer game (it can handle dynamic webpages, sessions, cookies, forms …etc)
1 |
npm install express |
create a server.js file on the root directory of Tank game, and edit it with the following code
1 2 3 4 5 6 7 8 9 10 11 12 |
// var express = require('express') , app = express(app) , server = require('http').createServer(app); // serve static files from the current directory app.use(express.static(__dirname)); server.listen(8000); |
now start the server with the following command
1 |
node server.js |
open a browser and navigate to : http://localhost:8000/
if your Tank game is working you’re good for the next step
or you can simply download the code here 🙂
Download Tanks Game Step 1 code
Installing and preparing eureca.io
Now let’s start playing with Eureca.io
Eureca.io can use either engine.io or sockjs as network transport layer, by default, engine.io is used.
to use eureca.io with the default configuration we’ll need to install eureca.io and engine.io
1 2 |
npm install engine.io npm install eureca.io |
Now we’ll modify the server code to add eureca.io stuff
before : server.listen(8000)
we instantiace eureca.io server and attach it to the HTTP server with the following code
1 2 3 4 5 6 7 8 9 10 |
// //get EurecaServer class var EurecaServer = require('eureca.io').EurecaServer; //create an instance of EurecaServer var eurecaServer = new EurecaServer(); //attach eureca.io to our http server eurecaServer.attach(server); |
then we add some event listeners to detect client connections and disconnections
1 2 3 4 5 6 7 8 9 10 11 |
// //detect client connection eurecaServer.onConnect(function (conn) { console.log('New Client id=%s ', conn.id, conn.remoteAddress); }); //detect client disconnection eurecaServer.onDisconnect(function (conn) { console.log('Client disconnected ', conn.id); }); |
in the client side we’ll also make some modification
first add the following line to index.html, before tanks.js script
1 |
<script src="/eureca.js"></script> |
this will make eureca.io available to the client.
now edit tanks.js file and add the following code at the beginning
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// var ready = false; var eurecaServer; //this function will handle client communication with the server var eurecaClientSetup = function() { //create an instance of eureca.io client var eurecaClient = new Eureca.Client(); eurecaClient.ready(function (proxy) { eurecaServer = proxy; //we temporary put create function here so we make sure to launch the game once the client is ready create(); ready = true; }); } |
what we do here is creating a client initialisation method “eurecaClientSetup”, which instantiate eureca.io Client and wait for the client to be ready, then call game creation method (create() )
the create() methode was initally called by Phaser.Game() instantiation method will modify this line to it call eurecaClientSetup
1 |
var game = new Phaser.Game(800, 600, Phaser.AUTO, 'phaser-example', { preload: preload, create: eurecaClientSetup, update: update, render: render }); |
important : if you create a multiplayer game, you usually need to ensure that the server is available before starting the game code, this is what we are doing with eurecaClientSetup
one last thing.
you may noticed the ready variable set to false by default, this flag allow us to know if client/server initialisation is done. and the game created.
we use it to prevent phaser to call update methode before create().
so we need to add the following to update() method
1 2 3 4 5 |
// function update () { //do not update if client not ready if (!ready) return; |
you can download the resulting code here :
Download Tanks Game Step 2 coderun the server.js again (node server.js) and navigate to http://localhost:8000/
the game should start and you see that the server have detected the client connection.
now refresh the page and you’ll see that the client was diconnected then connected again.
if this is working, we are ready for the next step.
Spawn/Kill remote players
For our basic multiplayer game, the server need to keep track of all connected clients.
to distinguish every client, we’ll also need to have some uniq identifier for each player (we’ll use a unique ID generated by eureca.io)
this unique id is shared between client and server ; it allow them to synchronize players data and make correspondance remote clients and Tanks.
so the implementation will be as follow.
- when a new client connect, the server will create a uniq id (eureca.io session ID is used here)
- the server send this uniq id to the client
- the client create the game scene with player’s Tank and assignate thie unique ID to its Tank.
- the client notify the server that everything is ready in the client side (we’ll call this a handshake)
- the server get the notification and call client Spawn method for each connected player.
- the client spawn a Tank instance for each connected player
- when a client disconnect, the server identify it and remove it from the list of connected clients
- the server call Kill() methode of all connected clients
- each client remove the Tank instance of disconnected player
And this is how we implement this
Client side
Eureca.io instances have a special namespace called “exports”, all methods defined under this namespace become available for RPC.
we’ll see how to use it.
for this we need to modify eurecaClientSetup method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// var eurecaClientSetup = function() { //create an instance of eureca.io client var eurecaClient = new Eureca.Client(); eurecaClient.ready(function (proxy) { eurecaServer = proxy; }); //methods defined under "exports" namespace become available in the server side eurecaClient.exports.setId = function(id) { //create() is moved here to make sure nothing is created before uniq id assignation myId = id; create(); eurecaServer.handshake(); ready = true; } eurecaClient.exports.kill = function(id) { if (tanksList[id]) { tanksList[id].kill(); console.log('killing ', id, tanksList[id]); } } eurecaClient.exports.spawnEnemy = function(i, x, y) { if (i == myId) return; //this is me console.log('SPAWN'); var tnk = new Tank(i, game, tank); tanksList[i] = tnk; } } |
in the above example we have three methods that’ll be callable from the server side : setId, kill and spawnEnemy .
Note that the client is calling a remote server function : eurecaServer.handshake() .
Server side
the first thing it so tell Eureca.io that client methods (setId, kill and spawnEnemy) are trusted client functions, otherwise eureca.io will refuse to call them
in client/server developement we should never blindly trust the client.
the following code tells Eureca.io to trust those methods and create a clientList object that’ll hold clients data
1 2 3 4 |
// var eurecaServer = new EurecaServer({allow:['setId', 'spawnEnemy', 'kill']}); var clients = {}; |
Now let’s modify onConnect and onDisconnect methods
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// //detect client connection eurecaServer.onConnect(function (conn) { console.log('New Client id=%s ', conn.id, conn.remoteAddress); //the getClient method provide a proxy allowing us to call remote client functions var remote = eurecaServer.getClient(conn.id); //register the client clients[conn.id] = {id:conn.id, remote:remote} //here we call setId (defined in the client side) remote.setId(conn.id); }); //detect client disconnection eurecaServer.onDisconnect(function (conn) { console.log('Client disconnected ', conn.id); var removeId = clients[conn.id].id; delete clients[conn.id]; for (var c in clients) { var remote = clients[c].remote; //here we call kill() method defined in the client side remote.kill(conn.id); } }); |
Note how the server call the remote client functions : remote.setId(conn.id) and remote.kill(conn.id);
if you remember, the client also call a server side method, and here is how we declare it
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// eurecaServer.exports.handshake = function() { //var conn = this.connection; for (var c in clients) { var remote = clients[c].remote; for (var cc in clients) { remote.spawnEnemy(clients[cc].id, 0, 0); } } } |
Now start the server and open a first browser window on http://localhost:8000/
move the tank a little and open another browser window on http://localhost:8000/
in the first window you should see a tank spawning .
close the last window and the tank will disapear.
this is pretty good hah 🙂 but still not a multiplayer game.
the Tank movement is not reaplicated yet, and this is what we’ll do in the next step.
by the way, here is the full code of the above step 😀
Download Tanks Game Step 3 code
Handle input / Synchronize states
In a multiplayer game, the server need to control client states, the only trusted entity is the server. (there are some other variants like P2P games … but we’ll not discuss them here 🙂 )
in an ideal implementation of client/server game, both client and server simulate the movements, then the server will send state data to the client which will correct/compensate the local positions.
in our example we will only synchronize a minimum set of information and “trust” the client side simulation.
this is how it’ll work.
- when the player issue an input (movement or fire), it’ll not be handled directly by the local code.
- instead, we’ll send it to the server, the server will loop through all connected clients and send them the client input
- each client will apply this input to the client side copy of the Tank.
- the Tank handle the input sent by the server as it was issued by a local input
in addition to this, each time an input information is sent, we will also send information about Tank position, this information will be used to synchronize Tank states with all connected clients.
let’s write the code to handle this.
Client side
We’ll first edit eurecaClientSetup method and add the following exported method
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// eurecaClient.exports.updateState = function(id, state) { if (tanksList[id]) { tanksList[id].cursor = state; tanksList[id].tank.x = state.x; tanksList[id].tank.y = state.y; tanksList[id].tank.angle = state.angle; tanksList[id].turret.rotation = state.rot; tanksList[id].update(); } } |
remember : methods under exports namespace can be called from the server.
updateState method will update Tank.cursor with the shared player input
but will also correct the Tank position and angle.
Now we need to handle this in the Tank.update method
edit Tank.prototype.update and replace the following line
1 |
for (var i in this.input) this.cursor[i] = this.input[i]; |
with this code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// var inputChanged = ( this.cursor.left != this.input.left || this.cursor.right != this.input.right || this.cursor.up != this.input.up || this.cursor.fire != this.input.fire ); if (inputChanged) { //Handle input change here //send new values to the server if (this.tank.id == myId) { // send latest valid state to the server this.input.x = this.tank.x; this.input.y = this.tank.y; this.input.angle = this.tank.angle; this.input.rot = this.turret.rotation; eurecaServer.handleKeys(this.input); } } |
here we first detect if the local player made an input (mouse click or keyboard left/right/up) if so, instead of handling it directly we send it to the server throught eurecaServer.handleKeys
the server side handleKeys method will send back the input to all connected clients as we’ll see bellow.
Server side
First we need to allow the newly declared client method (updateState)
1 |
var eurecaServer = new EurecaServer({allow:['setId', 'spawnEnemy', 'kill', 'updateState']}); |
then we declare handleKeys method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// eurecaServer.exports.handleKeys = function (keys) { var conn = this.connection; var updatedClient = clients[conn.id]; for (var c in clients) { var remote = clients[c].remote; remote.updateState(updatedClient.id, keys); //keep last known state so we can send it to new connected clients clients[c].laststate = keys; } } |
and one little modification to the existing handshake method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// eurecaServer.exports.handshake = function() { for (var c in clients) { var remote = clients[c].remote; for (var cc in clients) { //send latest known position var x = clients[cc].laststate ? clients[cc].laststate.x: 0; var y = clients[cc].laststate ? clients[cc].laststate.y: 0; remote.spawnEnemy(clients[cc].id, x, y); } } } |
if you followed all those steps (or downloaded the final code from the link bellow 😀 )
you can start the server and open two or more windows.
Now when you move a Tank in one client or launch projectile, it also move in other windows 🙂
What next?
You have now a basic code and notions of multiplayer game.
as an exercice you can try to handle damages, kill and respawn, you can also to handle position synchronization better with smooth lag compensation.
if you liked this tutorial share it
and of course, comments and suggestions are welcome
Download Multiplayer Tanks Game final code