Create A Basic Multiplayer Game In Phaser 3 With Socket.io – Part 2

In part one of this tutorial, we created our Node.js server, set up a basic Phaser game, and added Socket.io to allow communication between the two. If you missed it, you can find part one here. In this tutorial, we are going to focus on adding the client-side code that will: add and remove players from the game, handle player input, and allow players to pick up collectibles.

You can see what we will be completed below:

Let’s get started!

Course Files and Versions

If you didn’t complete part one and would like to continue from here, you can find the code for part one¬†here.

You can download all of the files associated with the source code for this part 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

ON SALE – FINAL DAYS
BUILD GAMES
BUILD YOUR OWN GAMES

‚úď 250+ coding courses

‚úď Interactive lessons

‚úď Guided learning paths

‚úď Help from expert mentors

GET IT ALL FOR JUST
$1

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. 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 public 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 game.js:

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

With the ship image loaded, we can now create the player in our game. In part one of this tutorial, we set up the Socket.io connection to emit a currentPlayers event anytime a new player connected to the game, and in this event, we also passed a players object that contains all of the current players. We will use this event to create our player.

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

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

Let’s review the code we just added:

  • 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¬†addPlayer()¬†function and passed it the current player’s information, and a reference to the current scene.

Now, let’s add the¬†addPlayer¬†function to¬†game.js. Add the following code to the bottom of the file:

function addPlayer(self, playerInfo) {
  self.ship = self.physics.add.image(playerInfo.x, playerInfo.y, 'ship').setOrigin(0.5, 0.5).setDisplaySize(53, 40);
  if (playerInfo.team === 'blue') {
    self.ship.setTint(0x0000ff);
  } else {
    self.ship.setTint(0xff0000);
  }
  self.ship.setDrag(100);
  self.ship.setAngularDrag(100);
  self.ship.setMaxVelocity(200);
}

In the code above we:

  • Created our player’s ship by using the x and y coordinates that we generated in our server code.
  • 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.
  • 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.
  • Lastly, we used¬†setDrag,¬†setAngularDrag, 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.

If you refresh your browser, you should see your player’s ship appear on the screen. If your server is not running, you can start it by navigating to your project folder in the command line, and run the following command:¬†node server.js.


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

Adding other players

Now that we have our player appearing in the game, we will add the logic for displaying other players in our game. In part one of the tutorial, we also set up Socket.io to emit a newPlayer and a disconnect event. We will use these two events, and our current logic for the currentPlayers event to add and remove other players from our game. To do this, update the create function to match the following:

function create() {
  var self = this;
  this.socket = io();
  this.otherPlayers = this.physics.add.group();
  this.socket.on('currentPlayers', function (players) {
    Object.keys(players).forEach(function (id) {
      if (players[id].playerId === self.socket.id) {
        addPlayer(self, players[id]);
      } else {
        addOtherPlayers(self, players[id]);
      }
    });
  });
  this.socket.on('newPlayer', function (playerInfo) {
    addOtherPlayers(self, playerInfo);
  });
  this.socket.on('disconnect', function (playerId) {
    self.otherPlayers.getChildren().forEach(function (otherPlayer) {
      if (playerId === otherPlayer.playerId) {
        otherPlayer.destroy();
      }
    });
  });
}

Let’s review the code we just added:

  • We created a new group called¬†otherPlayers, which will be used to manage all of the other 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.
  • We updated the function that is called when the¬†currentPlayers¬†event is emitted to now call the¬†addOtherPlayers¬†function when looping through the players object if that player is not the current player.
  • We used¬†socket.on()¬†to listen for the¬†newPlayer¬†and¬†disconnect¬†events.
  • When the¬†newPlayer event is fired, we call the¬†addOtherPlayers¬†function to add that new player to our game.
  • When the¬†disconnect¬†event is fired, we take that player’s id and we remove that player’s ship from the game. We do this by calling the¬†getChildren()¬†method on our¬†otherPlayers¬†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.
  • Lastly, we use the¬†destroy()¬†method to remove that game object from the game.

Now, let’s add the¬†addOtherPlayers¬†function to our game. Add the following code to the bottom of¬†game.js:

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

This code is very similar to the code we added in the¬†addPlayer()¬†function. The main difference is that we added the other player’s game object to our¬†otherPlayers¬†group.

The last thing we need to do before we can test our game logic for adding the other players is, we need to load an asset for these players. You can find that asset here.

This image will need to be placed in the assets folder. Once the image is there, we can load this image into our game. In the preload function, add the following line:

this.load.image('otherPlayer', 'assets/enemyBlack5.png');

Now, if you refresh your game in the browser, you should still see your player’s ship. If you open another tab or browser and navigate to your game, you should see multiple sprites appear in the game window, and if you close one of those games you should see that sprite disappear from the other games.


Handling player input

With the logic for displaying all of the players in the game in place, we will move on to handling player input and allow our player to move. We will handle player input by using Phaser’s built-in keyboard manager. To do this, add the following line at the bottom of the¬†create¬†function in¬†game.js:

this.cursors = this.input.keyboard.createCursorKeys();

This will populate the cursors object with our four main Key objects (up, down, left, and right), which will bind to those arrows on the keyboard. Then, we just need to see if these keys are being held down in the update function.

Add the following code to the update function in game.js:

if (this.ship) {
    if (this.cursors.left.isDown) {
      this.ship.setAngularVelocity(-150);
    } else if (this.cursors.right.isDown) {
      this.ship.setAngularVelocity(150);
    } else {
      this.ship.setAngularVelocity(0);
    }
  
    if (this.cursors.up.isDown) {
      this.physics.velocityFromRotation(this.ship.rotation + 1.5, 100, this.ship.body.acceleration);
    } else {
      this.ship.setAcceleration(0);
    }
  
    this.physics.world.wrap(this.ship, 5);
  }

In the code above, we did the following:

  • We are checking if the left, right, or up keys are pressed down.
  • If the left or right key is pressed, then we update the player’s angular velocity by calling¬†setAngularVelocity(). The angular velocity will allow the ship to rotate left and right.
  • If neither the left or right keys are pressed, then we reset the angular velocity back to 0.
  • If the up key is pressed, then we update the ship’s velocity, otherwise, we set it to 0.
  • Lastly, if the ship goes off screen we want it to appear on the¬†other side of the screen. We do this by calling¬†physics.world.wrap()¬†and we pass it the game object we want to wrap and an offset.

If you refresh your game, you should now be able to move your ship around the screen.


Handling other player movements

Now that our player is able to move, we will move on to handling other player’s movement in our game. In order to track other player movements, and move their sprites in our game, we will need to emit a new event any time a player moves. In the¬†update¬†function of¬†game.js¬†add the following code inside the¬†if¬†statement:

// emit player movement
var x = this.ship.x;
var y = this.ship.y;
var r = this.ship.rotation;
if (this.ship.oldPosition && (x !== this.ship.oldPosition.x || y !== this.ship.oldPosition.y || r !== this.ship.oldPosition.rotation)) {
  this.socket.emit('playerMovement', { x: this.ship.x, y: this.ship.y, rotation: this.ship.rotation });
}

// save old position data
this.ship.oldPosition = {
  x: this.ship.x,
  y: this.ship.y,
  rotation: this.ship.rotation
};

Let’s review the code we just added:

  • We created three new variables and use them to store information about the player.
  • We then check to see if the player’s rotation or position has changed by comparing these variables to the player’s previous rotation and position. If the player’s position or rotation has changed, then we emit a new event called¬†playerMovement¬†and pass it the player’s information.
  • Lastly, we store the player’s current rotation and position.

Next, we will need to update the Socket.io code in¬†server.js¬†to listen for the new¬†playerMovement¬†event. In¬†server.js, add the following code below the¬†socket.io(‘disconnect’)¬†code:

// when a player moves, update the player data
socket.on('playerMovement', function (movementData) {
  players[socket.id].x = movementData.x;
  players[socket.id].y = movementData.y;
  players[socket.id].rotation = movementData.rotation;
  // emit a message to all players about the player that moved
  socket.broadcast.emit('playerMoved', players[socket.id]);
});

When the¬†playerMovement event is received on the server, we update that player’s information that is stored on the server, emit a new event called¬†playerMoved¬†to all other players, and in this event we pass the updated player’s information.

Lastly, we will need to update the client side code to listen for this new event, and when this event is emitted, we will need to update that player’s sprite in the game. In the¬†create¬†function in¬†game.js¬†add the following code:

this.socket.on('playerMoved', function (playerInfo) {
  self.otherPlayers.getChildren().forEach(function (otherPlayer) {
    if (playerInfo.playerId === otherPlayer.playerId) {
      otherPlayer.setRotation(playerInfo.rotation);
      otherPlayer.setPosition(playerInfo.x, playerInfo.y);
    }
  });
});

Now, if you restart your server and open up the game in two different tabs or browsers, and move one of the players, you should see that player move in the other game.


Collecting Stars

With the game now handling other player movements, we need to give the players a goal. For this tutorial, we are going to add a star collectible to the game for the players to collect, and when they do, their team will get ten points. To do this, we will need to add a few more Socket.io events to our game, and we will start with the server logic first.

First, we will add two new variables to our server: star¬†and scores. The¬†star¬†variable will be used to store the position of our star collectible, and the¬†scores¬†variable will be used to keep track of both team’s score. In¬†server.js¬†, add the following code beneath the¬†var players = {};¬†line:

var star = {
  x: Math.floor(Math.random() * 700) + 50,
  y: Math.floor(Math.random() * 500) + 50
};
var scores = {
  blue: 0,
  red: 0
};

Next, we will emit two new events when a new player connects to the game,¬†starLocation, and¬†scoreUpdate. We will use these two events to send the new player the star collectible’s location, and the current scores of both teams. In¬†server.js, add the following code beneath the¬†socket.emit(‘currentPlayers’, players);¬†line:
// send the star object to the new player
socket.emit('starLocation', star);
// send the current scores
socket.emit('scoreUpdate', scores);

Lastly, we are going to listen for a new event called¬†starCollected, which will be triggered when a player collects the star. When this event is received, we will need to update the correct team’s score, generate a new location for the star, and let each player know about the updated scores and the stars new location.

In¬†server.js, add the following code beneath the¬†socket.on(‘playerMovement’)¬†code:

socket.on('starCollected', function () {
  if (players[socket.id].team === 'red') {
    scores.red += 10;
  } else {
    scores.blue += 10;
  }
  star.x = Math.floor(Math.random() * 700) + 50;
  star.y = Math.floor(Math.random() * 500) + 50;
  io.emit('starLocation', star);
  io.emit('scoreUpdate', scores);
});

Now that our server logic has been updated, we will need to update the client side code in¬†game.js. In order to display the two teams scores, we will use Phaser’s Text Game Object. In the¬†create¬†function, add the following code at the bottom of the function:
this.blueScoreText = this.add.text(16, 16, '', { fontSize: '32px', fill: '#0000FF' });
this.redScoreText = this.add.text(584, 16, '', { fontSize: '32px', fill: '#FF0000' });
  
this.socket.on('scoreUpdate', function (scores) {
  self.blueScoreText.setText('Blue: ' + scores.blue);
  self.redScoreText.setText('Red: ' + scores.red);
});

Let’s review the code we just added:

  • We created two new text game objects by calling¬†this.add.text(). When we created these two objects, we passed the location of where the object would be placed, the default text of the object, and the font size and fill that would be used for the text object.
  • When the¬†scoreUpdate¬†event is received, we then update the text of the game objects by calling the¬†setText()¬†method, and we pass the team’s score to each object.

If you refresh your game, you should see each teams’ score:


Finally, the last thing we need to add is the logic for the star collectible. First, we will need to load the asset for the star collectible in the preload function. The asset for the game can be downloaded here.

In the preload function, add the following code:

this.load.image('star', 'assets/star_gold.png');

Then, in the create function, add the following code below the score update code we just added:
this.socket.on('starLocation', function (starLocation) {
  if (self.star) self.star.destroy();
  self.star = self.physics.add.image(starLocation.x, starLocation.y, 'star');
  self.physics.add.overlap(self.ship, self.star, function () {
    this.socket.emit('starCollected');
  }, null, self);
});

In the code above we listened for the starLocation event, and when it is received we do the following:

  • We check to see if the star object exists, and if it does, we destroy it.
  • We add a new star game object to the player’s game, and we use the information passed to the event to populate its location.
  • Lastly, we added a check to see if the player’s ship and the star object are overlapping, and if they are, then we emit the¬†starCollected¬†event. By calling¬†physics.add.overlap, Phaser will automatically check for the overlap and run the provided function when there is an overlap.

If you refresh your game, you should now see the star collectible on the screen:


Conclusion

With the star collectible added to the game, that brings this tutorial to a close. In summary, this tutorial showed you how to create a basic multiplayer game in Phaser using Socket.io and Node.js.

I hope you enjoyed this tutorial and found it helpful. If you have any questions, or suggestions on what we should cover next, let us know in the comments below.