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:

2018 04 22 1732

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

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 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 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.

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.

2018 04 20 2141
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.

2018 04 20 2233
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.

Apr 20 2018 23 25 04
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.

Apr 21 2018 22 47 59
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:

2018 04 22 1703
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:

2018 04 22 1728
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.