Creating A Simple Multiplayer Game In Phaser 3 With An Authoritative Server – Part 2

In Part 1 of this tutorial, we created our Node.js server, set up a basic Phaser game, and set up our server to run Phaser in headless mode. If you missed the beginning, you can find it here. In this tutorial, we are going to focus on adding the Socket.IO library to game, adding the server logic for adding and removing players from the game, and adding the client side logic for adding a player to the game.

If you didn’t complete Part 1 and would like to continue from there, you can find the code for it here.

Let’s get started!

Course Files and Versions

You can download all of the files associated with the source code for Part 2 here.

At the time this tutorial was written, the following versions were used. You may need to use these versions to have the same results from this tutorial.

  • Node.js: 10.13.0
  • JSDOM: 13.0.0
  • Express: 4.16.4
  • Socket.IO: 2.1.1
  • Datauri: 1.1.0

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.

Adding Socket.IO

With Phaser now running on our server, we will now work on adding Socket.IO to our game. If you are not familiar with Socket.IO, it is a JavaScript library that enables real-time, bi-directional communication between web clients and servers. To use Socket.IO, we need to update our client and server code to enable the communication between the two.

Back in your terminal, run the following command: npm install –save socket.io. If your server is still running, you can either: open a new terminal window and run the code in your project folder, or stop the server (CTRL + C) and then run the command. This will install the Socket.IO node package and save it in our package.json file.

Now, in server.js add the following code below the var server = require(‘http’).Server(app); line:

const io = require('socket.io').listen(server);

Then, below the `dom.window.gameLoaded` code add the following line:

dom.window.io = io;

This will inject our socket.io instance into jsdom which will allow us to access it in our Phaser code that is running on the server. Now, in authoritative_server/js/game.js add the following code in the create function:

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

In the code above we:

  • referenced the socket.io module and had it listen to our server object.
  • added logic to listen for connections and disconnections.

Next, we will update the client side code to include the Socket.IO library. Open up public/index.html and add the following line at the top of the <body> element:

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

Then, open up public/js/game.js and add the following code in the create function:

this.socket = io();

Now, if you save your code changes, restart the server, and refresh your game in the browser you should see the message about a user being connected. If you refresh your game in the browser you should see a new message about a user disconnecting and then another message about a user connecting.

Screen Shot 2018 11 22 at 10.48.40 PM

Adding players – Server

With our socket connection setup, we can move on to adding players to our game. The first thing we need to do is load the asset that will be used for the player’s ship. For this tutorial, we will be using some images from Kenny’s Space Shooter Redux asset pack. The asset for the game can be downloaded here.

In the authoritative_server folder, create a new folder called assets and place the image there. To load the image in our game, you will need to add the following line inside the preload function in authoritative_server/js/game.js:

this.load.image('ship', 'assets/spaceShips_001.png');

With the ship image loaded, we will add the rest of the logic for adding the players. In order to keep all of the player’s games in sync, we will need a way to notify all players when a user connects or disconnects from the game. Also, when a new player connects we will need a way to let the new player know of all the other players in the game.

To do this, we can use our socket connections to send messages to each client. For the player data, we will need to keep track of each player that connects and disconnects.

In, authoritative_server/js/game.js add the following code at the top of the file:

const players = {};

We will use this object to keep track of all the players that are currently in the game. Next, at the top of the create function add the following code:

const self = this;
this.players = this.physics.add.group();

Next, in the callback function of the socket.io connection event add the following code below the console.log(‘a user connected’); line:

// create a new player and add it to our players object
players[socket.id] = {
  rotation: 0,
  x: Math.floor(Math.random() * 700) + 50,
  y: Math.floor(Math.random() * 500) + 50,
  playerId: socket.id,
  team: (Math.floor(Math.random() * 2) == 0) ? 'red' : 'blue'
};
// add player to server
addPlayer(self, players[socket.id]);
// send the players object to the new player
socket.emit('currentPlayers', players);
// update all other players of the new player
socket.broadcast.emit('newPlayer', players[socket.id]);

Let’s review the code we just added:

  • We created a new variable called self and we use it to store a reference to this Phaser Scene.
  • We created a new Phaser physics group, which will be used to manage all of the players in our game. If you are not familiar with groups in Phaser, they are a way for us to manage similar game objects and control them as one unit. One example is, instead of having to check for collisions on each of those game objects separately, we can check for collision between the group and other game objects.
  • When a player connects to a web socket, we update the players object with some data about the player and we store this data as an object and we use the socket.id as the key for the object.
  • We are storing the rotation, x, and y position of the player, and we will use this to control were we create sprites on the client side, and use this data to update each players games.
  • We also store the playerId so we can reference it in the game, and we added a team attribute that will be used later.
  • We used socket.emit and socket.broadcast.emit to emit an event to the client side socket. socket.emit will only emit the event to this particular socket (the new player that just connected).  socket.broadcast.emit will send the event to all other sockets (the existing players).
  • In the currentPlayers event, we are passing the players object to the new player. This data will be used to populate all of the player sprites in the new player’s game.
  • In the newPlayer event, we are the passing the new player’s data to all other players, that way the new sprite can be added to their game.
  • Lastly, we called a new function called addPlayer, which will be used for creating the player on the server.

Now, we will create the addPlayer function. This function will be used to create new player game objects and it will add those game objects to the player’s group we just created. Add the following code below the update function:

function addPlayer(self, playerInfo) {
  const player = self.physics.add.image(playerInfo.x, playerInfo.y, 'ship').setOrigin(0.5, 0.5).setDisplaySize(53, 40);
  player.setDrag(100);
  player.setAngularDrag(100);
  player.setMaxVelocity(200);
  player.playerId = playerInfo.playerId;
  self.players.add(player);
}

In the code above we:

  • Created a new player’s ship by using the x and y coordinates that we generated earlier.
  • Instead of just using self.add.image to create our player’s ship, we used self.physics.add.image in order to allow that game object to use the arcade physics.
  • We used setOrigin() to set the origin of the game object to be in the middle of the object instead of the top left. The reason we did this because, when you rotate a game object, it will be rotated around the origin point.
  • We used setDisplaySize() to change the size and scale of the game object. Originally, our ship image was 106×80 px, which was a little big for our game. After calling setDisplaySize() the image is now 53×40 px in the game.
  • Lastly, we used setDragsetAngularDrag, and setMaxVelocity to modify how the game object reacts to the arcade physics. Both setDrag and setAngularDrag are used to control the amount of resistance the object will face when it is moving. setMaxVelocity is used to control the max speed the game object can reach.

Removing players – Server

With the logic for adding players on the server in place, we will add the logic for removing a player from the server. When a player disconnects, we need to remove that player’s data from our players object, and we need to emit a message to all other players about this user leaving, that way we can remove that player’s sprite from the client’s game.

In the callback function of the socket.io disconnect event add the following code below the console.log(‘user disconnected’); line:

// remove player from server
removePlayer(self, socket.id);
// remove this player from our players object
delete players[socket.id];
// emit a message to all players to remove this player
io.emit('disconnect', socket.id);

Then, add the following code below the addPlayer function:

function removePlayer(self, playerId) {
  self.players.getChildren().forEach((player) => {
    if (playerId === player.playerId) {
      player.destroy();
    }
  });
}

In the code above we:

  • Created a new function called removePlayer. This function will take the socket.id of the player that disconnected and it will find that player’s game object in our Phaser group and it will destroy it.
  • We do this by calling the getChildren() method on our players group. The getChildren() method will return an array of all the game objects that are in that group, and from there we use the forEach() method to loop through that array until we find the game object with the matching id.
  • Once the game object is destroyed on the server, we then delete that player from our players object by using the delete operator to remove that property.
  • Finally, we used the io.emit to send a message to all sockets. In this event, we are passing the socket.id of the user that disconnected that way we can remove that player’s sprite from the client side code.

Your authoritative_server/js/game.js file should look like the following:

const players = {};

const config = {
  type: Phaser.HEADLESS,
  parent: 'phaser-example',
  width: 800,
  height: 600,
  physics: {
    default: 'arcade',
    arcade: {
      debug: false,
      gravity: { y: 0 }
    }
  },
  scene: {
    preload: preload,
    create: create,
    update: update
  },
  autoFocus: false
};

function preload() {
  this.load.image('ship', 'assets/spaceShips_001.png');
}

function create() {
  const self = this;
  this.players = this.physics.add.group();

  io.on('connection', function (socket) {
    console.log('a user connected');
    // create a new player and add it to our players object
    players[socket.id] = {
      rotation: 0,
      x: Math.floor(Math.random() * 700) + 50,
      y: Math.floor(Math.random() * 500) + 50,
      playerId: socket.id,
      team: (Math.floor(Math.random() * 2) == 0) ? 'red' : 'blue'
    };
    // add player to server
    addPlayer(self, players[socket.id]);
    // send the players object to the new player
    socket.emit('currentPlayers', players);
    // update all other players of the new player
    socket.broadcast.emit('newPlayer', players[socket.id]);

    socket.on('disconnect', function () {
      console.log('user disconnected');
      // remove player from server
      removePlayer(self, socket.id);
      // remove this player from our players object
      delete players[socket.id];
      // emit a message to all players to remove this player
      io.emit('disconnect', socket.id);
    });
  });
}

function update() {}

function addPlayer(self, playerInfo) {
  const player = self.physics.add.image(playerInfo.x, playerInfo.y, 'ship').setOrigin(0.5, 0.5).setDisplaySize(53, 40);
  player.setDrag(100);
  player.setAngularDrag(100);
  player.setMaxVelocity(200);
  player.playerId = playerInfo.playerId;
  self.players.add(player);
}

function removePlayer(self, playerId) {
  self.players.getChildren().forEach((player) => {
    if (playerId === player.playerId) {
      player.destroy();
    }
  });
}

const game = new Phaser.Game(config);
window.gameLoaded();

Now, if you save your code changes, restart the server, and refresh your game in the browser you should see an error message in your console.

Screen Shot 2018 11 23 at 1.03.02 AM

From the looks of the error message, it looks like jsdom does not support the URL.createObjectURL method. This static method is used to create a DOMString containing an object URL that can be used to reference the contents of the specified source object. In order to resolve this error, we will need to implement a method that returns a similar value.

To do that, we will use the datauri package to return a data URI. To use this package, we will need to add it to our project. In the terminal, stop your server and run the following command:

npm install --save datauri

Next, we will need to include the library in our server code. To do this, open server/index.js and add the following above the const { JSDOM } = jsdom; line:

const Datauri = require('datauri');

const datauri = new Datauri();

Then, in the setupAuthoritativePhaser function add the following code above the dom.window.gameLoaded = () => { line:

dom.window.URL.createObjectURL = (blob) => {
  if (blob){
    return datauri.format(blob.type, blob[Object.getOwnPropertySymbols(blob)[0]]._buffer).content;
  }
};
dom.window.URL.revokeObjectURL = (objectURL) => {};

Let’s review the code we just added:

  • First, we included the datauri package and we created a new instance of it.
  • Then, we created the createObjectURL and revokeObjectURL functions that are not implemented in jsdom.
  • For the revokeObjecURL function, we won’t have the function do anything.
  • For the createObjectURL function, we use the datauri.format method to format the blob into the format we need.

Now, if you save your code changes and start the server everything should start up fine.

Adding players – Client

With our server code for adding players in place, we will now work on the client side code. The first thing we need to do is load the asset that will be used for the player. In the public folder, create a new folder called assets, and in this folder copy over the spaceShips_001.png image from the other assets folder.

To load the image in our game, you will need to add the following line inside the preload function in public/js/game.js:

this.load.image('ship', 'assets/spaceShips_001.png');

With the ship image loaded, we can now create the player in our game. Earlier, we set up Socket.IO to emit a currentPlayers event anytime a new player connects to the game, and when this event is fired we also passed a players object that contains the data on the current players.

Update the create function in public/js/game.js to match the following:

function create() {
  var self = this;
  this.socket = io();
  this.players = this.add.group();

  this.socket.on('currentPlayers', function (players) {
    Object.keys(players).forEach(function (id) {
      if (players[id].playerId === self.socket.id) {
        displayPlayers(self, players[id], 'ship');
      }
    });
  });
}

Let’s review the code we just added:

  • First, we created a new Phaser group which will be used to manage all of the player’s game objects on the client side.
  • We used socket.on to listen for the currentPlayers event, and when this event is triggered, the function we provided will be called with the players object that we passed from our server.
  • When this function is called, we loop through each of the players and we check to see if that player’s id matches the current player’s socket id.
  • To loop through the players, we use Object.keys() to create an array of all the keys in the Object that is passed in. With the array that is returned we use the forEach() method to loop through each item in the array.
  • Lastly, we called the displayPlayers() function and passed it the current player’s information, and a reference to the current scene.

Now, let’s add the displayPlayer function to public/js/game.js. Add the following code to the bottom of the file:

function displayPlayers(self, playerInfo, sprite) {
  const player = self.add.sprite(playerInfo.x, playerInfo.y, sprite).setOrigin(0.5, 0.5).setDisplaySize(53, 40);
  if (playerInfo.team === 'blue') player.setTint(0x0000ff);
  else player.setTint(0xff0000);
  player.playerId = playerInfo.playerId;
  self.players.add(player);
}

In the code above we:

  • Created our player’s ship by using the x and y coordinates that we generated in our server code.
  • We used setOrigin() to set the origin of the game object to be in the middle of the object instead of the top left.
  • We used setDisplaySize() to change the size and scale of the game object.
  • We used setTint() to change the color of the ship game object, and we choose the color depending on the team that was generated when we created our player info on the server.
  • We stored the playerId so we can find the game object by that id later.
  • Lastly, we added the player’s game object to the Phaser group we created.

If you refresh your browser, you should see your player’s ship appear on the screen.

Screen Shot 2018 11 23 at 1.58.48 AM

Also, if you refresh your game, you should see your ship appear in different locations, and it should randomly be red or blue.

Conclusion

With our client-side code for displaying the player in place, that brings Part 2 of this tutorial series to an end. In Part 3, we continue our multiplayer game by:

  • Adding the client side logic for adding the other players to our game.
  • Adding the logic for player input.
  • Adding the logic for collectibles.

I hoped you enjoyed our second instalment and found it helpful. If you have any questions or suggestions on what we should cover next, please let us know in the comments below.