How to Create a Phaser MMORPG – Part 3

In Parts 1 and 2 of this tutorial series, we started building our Phaser 3 MMORPG. In Part 2, we did the following:

  • Refactored the client-side logic of our game.
  • Updated the Player creation game logic.
  • Added logic for creating the other players that joined the game.
  • Added the logic for creating the enemy game objects.

In Part 3 of this tutorial series, we will finish our Phaser 3 MMORPG by doing the following:

  • Add the logic for moving the enemy game objects.
  • Add logic to allow the players to attack the enemy game objects.
  • Add logic for allowing players to chat.

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

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

Let’s get started!

""ACCESS NOW</a> </div> </div></div> </div></div> </div></div> </div></div></div>"}" data-sheets-userformat="{"2":2579,"3":{"1":0},"4":[null,2,16777215],"7":{"1":[{"1":2,"2":0,"5":[null,2,0]},{"1":0,"2":0,"3":3},{"1":1,"2":0,"4":1}]},"12":0,"14":{"1":2,"2":16777215}}">

BUILD GAMES

FINAL DAYS: Unlock 250+ coding courses, guided learning paths, help from expert mentors, and more.

Enemy Movement

At the end of the last tutorial, we added the logic for creating the enemy game objects. Currently, the enemy game objects are generated in one random location and then they do nothing. To make the game a little more interesting, we will update the enemy game objects to randomly move around the map. Normally, we would want the enemies in our game to be more intelligent and we would have them fight back. That is outside the scope of this tutorial, so we recommend that you look into finding out how to do so in order to take your game further.

To do this, add the following code below the createEnemies function in the WorldScene class that is located in the public/assets/js/game.js file:

moveEnemies () {
  this.spawns.getChildren().forEach((enemy) => {
    const randNumber = Math.floor((Math.random() * 4) + 1);

    switch(randNumber) {
      case 1:
        enemy.body.setVelocityX(50);
        break;
      case 2:
        enemy.body.setVelocityX(-50);
        break;
      case 3:
        enemy.body.setVelocityY(50);
        break;
      case 4:
        enemy.body.setVelocityY(50);
        break;
      default:
        enemy.body.setVelocityX(50);
    }
  });

  setTimeout(() => {
    this.spawns.setVelocityX(0);
    this.spawns.setVelocityY(0);
  }, 500);
}

In the code above, we did the following:

  • First, we used the getChildren method on the spawns Phaser Group object to get an array of all the child game objects that belong to that Group.
  • Then, we used the forEach method to loop through that array of game objects.
  • While looping through the game objects, we generate a random number between 1 and 4. We then use a switch statement to update either the x or y velocity of the enemy game object’s physics body.
  • Lastly, we use the setTimeout method to call a function after 500 milliseconds. In the function that is called, we update the x and y velocity of the enemy game object’s physics body to be 0.

Now, we just need to call the moveEnemies function. We do not want to call this function every time the update function is called, so we will use a Phaser Time event to trigger the moveEnemies function. To do this, add the following code at the bottom of the createEnemies function in the WorldScene class:

// move enemies
this.timedEvent = this.time.addEvent({
  delay: 3000,
  callback: this.moveEnemies,
  callbackScope: this,
  loop: true
});

In the code above, we did the following:

  • First, we used this.time.addEvent to create a new Time Event.
  • When we called the addEvent method, we passed the following object as an argument:
    • delay – how long to wait before calling this function. When the loop parameter is set to true, the delay parameter will also be used to control how long we wait between each call.
    • callback – the function to call when the event is triggered.
    • callbackScope – the scope that will be used when the callback function is called.
    • loop – used to have the event repeat. By default, this will be set to false.

Now, if you save your code changes and refresh your browser, you should see that the enemy game objects will move randomly. Note: to start your server, you can run the following command in your terminal at the root of your project: npm run start

Attack Enemies

With the logic for moving enemy game objects in place, we will now work on adding the logic for allowing the player to attack enemies. In order to achieve this, we will create a new game object for the player’s weapon and add it to the player container. To do this, add the following logic to the createPlayer function in the WorldScene class above the this.updateCamera() line:

// add weapon
this.weapon = this.add.sprite(10, 0, 'sword');
this.weapon.setScale(0.5);
this.weapon.setSize(8, 8);
this.physics.world.enable(this.weapon);

this.container.add(this.weapon);
this.attacking = false;

Then, add the following line at the bottom of the createPlayer function:

this.physics.add.overlap(this.weapon, this.spawns, this.onMeetEnemy, false, this);

Let’s review the code we just added:

  • First, we created a new weapon game object using the sword sprite. We then scaled and set the size on game object by calling the following methods: setScale and setSize.
  • We then enabled physics on the weapon game object.
  • Next, we added the new weapon game object to the player container object, and created a new variable called attacking and set it to false. We will use this variable to keep track of when a player is attacking.
  • Finally, we added a new overlap between the weapon game object and the enemies group. When these overlap, the onMeetEnemy function will be triggered.

Next, we need to update the logic that is used in the onMeetEnemy function in the WorldScene class. Replace all of the logic in the onMeetEnemy function with the following code:

onMeetEnemy(player, enemy) {
  if (this.attacking) {
    const location = this.getValidLocation();
    enemy.x = location.x;
    enemy.y = location.y;
  }
}

In the code above, we did the following:

  • First, we checked to see if the player is attacking.
  • If the player is attacking when the weapon collides with the enemy game object, then we get a new valid location for that enemy.
  • Finally, we update the location of the enemy to be that new location.

If you save your code changes and refresh your browser, you should see that the player sprite now holds a weapon, and if you press the spacebar key the player should attack with the sword. If you try attacking an enemy, they should be moved to a new location.

Handling other player’s movement

Now that the player is can attack enemies, we will work on adding logic for moving other player’s sprites in our game. To handle other player’s movement, when a player moves in our game we will send a message to the server with that player’s location. Then, we will send those details to each of the other players.

Since we already have the logic in place on the server, we just need to update our client-side code. To do this, open public/assets/js/game.js and add the following code at the bottom of the create function in the WorldScene class:

this.socket.on('playerMoved', function (playerInfo) {
  this.otherPlayers.getChildren().forEach(function (player) {
    if (playerInfo.playerId === player.playerId) {
      player.flipX = playerInfo.flipX;
      player.setPosition(playerInfo.x, playerInfo.y);
    }
  }.bind(this));
}.bind(this));

Let’s review the code we just added:

  • First, we listened for the playerMoved message and when this message is received, a playerInfo object is also sent.
  • Inside the function that is triggered when the playerMoved message is received, we get the children of the otherPlayers group and loop through them using the forEach method.
  • When looping through the array of game objects, we check to see if the provided playerId of the playerInfo object matches the current game object. If the playerId is a match, then we update that game objects flipX property and update the x and y position of the game object.

Next, add the following code to the bottom of the update function in the WorldScene class:

// emit player movement
var x = this.container.x;
var y = this.container.y;
var flipX = this.player.flipX;
if (this.container.oldPosition && (x !== this.container.oldPosition.x || y !== this.container.oldPosition.y || flipX !== this.container.oldPosition.flipX)) {
  this.socket.emit('playerMovement', { x, y, flipX });
}
// save old position data
this.container.oldPosition = {
  x: this.container.x,
  y: this.container.y,
  flipX: this.player.flipX
};

In the code above, we did the following:

  • First, we get the x, y, and flipX values of the player container object and store those in a few variables.
  • Next, we check to see if the container game object has an oldPosition property, and if it does, then we check to see if the current position of the player game object is different that the values stored in the oldPosition property.
  • If the values are different, then we emit a playerMovement message along with an object that has the x, y, and flipX values.
  • Lastly, we store those values in the oldPosition property of the container game object.

Now, if you save your code changes, refresh your browser, and open a new tab with an instance of the Phaser game, you should be able to test these latest changes. If you move your player by the other player’s location, you should be able to switch tabs and see that the other player’s sprite is now near your player’s sprite.

Adding chat

With logic for updating the other player’s game object in place, we will now work on adding chat to our game. For the chat in our game, we will add a simple div that will allow player’s to input messages, send them, and then on the server, we will save those messages in our database and broadcast those message to all player’s in our game.

To get started, open public/game.html and replace the code in the body tag with the following code:

<body>
  <div class="flex-container">
    <div id="content"></div>
    <div id="chatContainer">
      <div class="chatArea">
        <ul id="messages" class="messages"></ul>
      </div>
      <input id="inputMessage" class="inputMessage" placeholder="Type here..." type="text">
    </div>
  </div>
  <script src="assets/js/refreshToken.js"></script>
  <script src="assets/js/game.js"></script>
</body>

In the code above, we added some divs for displaying our chat interface, an input to allow player’s to enter text, and an ul element for displaying the messages that players type. Lastly, we included the refreshToken script that has the logic for refreshing the player’s token that is used for authentication.

Next, in public/assets/js/game.js add the following code at the top of the file:

const inputMessage = document.getElementById('inputMessage');
const messages = document.getElementById('messages');

window.addEventListener('keydown', event => {
  if (event.which === 13) {
    sendMessage();
  }
  if (event.which === 32) {
    if (document.activeElement === inputMessage) {
      inputMessage.value = inputMessage.value + ' ';
    }
  }
});

function sendMessage() {
  let message = inputMessage.value;
  if (message) {
    inputMessage.value = '';
    $.ajax({
      type: 'POST',
      url: '/submit-chatline',
      data: {
        message,
        refreshToken: getCookie('refreshJwt')
      },
      success: function(data) {},
      error: function(xhr) {
        console.log(xhr);
      }
    })
  }
}

function addMessageElement(el) {
  messages.append(el);
  messages.lastChild.scrollIntoView();
}

Let’s review the code we just added:

  • First, we stored a reference to our input element and the messages ul element.
  • Next, we added an event listener for the keydown event, and when this event is fired we check to see if the spacebar key and return keys were pressed.
    • If the return key was pressed, then we call a new function called sendMessage.
    • If the spacebar key was pressed, then we check to see if the input element is currently focused, and if it is then we update the value of the text input. The reason we are doing this is that Phaser is listening for our spacebar input and if you try pressing the spacebar key in the input element, the space is never added to the text.
  • Then, we created a new function called sendMessage that will send a POST request to a new endpoint, submit-chatline, that we will add to our server.
    • In the POST request, we send the chat message along with the user’s refresh token.
  • Lastly, we created another new function called addMessageElement which will append a new li element to the messages ul element, and then we will have the messages scroll to that message.

Then, in the create function in the WorldScene class add the following code at the bottom of the function:

this.socket.on('new message', (data) => {
  const usernameSpan = document.createElement('span');
  const usernameText = document.createTextNode(data.username);
  usernameSpan.className = 'username';
  usernameSpan.appendChild(usernameText);

  const messageBodySpan = document.createElement('span');
  const messageBodyText = document.createTextNode(data.message);
  messageBodySpan.className = 'messageBody';
  messageBodySpan.appendChild(messageBodyText);

  const messageLi = document.createElement('li');
  messageLi.setAttribute('username', data.username);
  messageLi.append(usernameSpan);
  messageLi.append(messageBodySpan);

  addMessageElement(messageLi);
});

In the code above, we did the following:

  • First, we listened for a new event message called new message. When this message is fired, the function will receive a data object that has the player’s name that sent a message and the message that the player sent.
  • We then create a new span element, and added the player’s name to that span element.
  • Next, we created another new span element, and we added the player’s message to that span element.
  • Finally, we created a new li element, and added both of the span‘s we created to that element. We then called the addMessageElement function and passed the new li element as an argument.

Now, we just need to add some logic to our server to handle the messages. In app.js add the following code below the `app.use(‘/’, passport.authenticate(‘jwt’, { session : false }), secureRoutes);` line:

app.post('/submit-chatline', passport.authenticate('jwt', { session : false }), asyncMiddleware(async (req, res, next) => {
  const { message } = req.body;
  const { email, name } = req.user;
  // await ChatModel.create({ email, message });
  io.emit('new message', {
    username: name,
    message,
  });
  res.status(200).json({ status: 'ok' });
}));

Then, at the top of the file add the following code below the other require statements:

const asyncMiddleware = require('./middleware/asyncMiddleware');

In the code above, we did the following:

  • First, we created a new POST endpoint, /submit-chatline, and we authenticate the user that is calling that endpoint.
  • Next, we pull the user’s message from the request body, and then we pull the user’s email and name from the request user object.
  • We then call the create method on the ChatModel and pass this method the user’s email and message. For now, we commented out this line until we create that model.
  • Finally, we use io.emit to broadcast the new message event to users.

With the server updated, we can now test the changes. If you restart your server, refresh your browser and navigate back to the login page, and then log in, you should be taken to the game page. If you type some messages, you should see them appear on the screen.

Updating User Token

Currently, when a user sends a message, the name is appearing as undefined. To fix this, we need to update the token we are storing for the user to include their name. To do this, open routes/main.js and in the `router.post(‘/login’, async (req, res, next) => {` update the body object to be:

const body = {
  _id: user._id,
  email: user.email,
  name: user.name,
};

Then, update the following code:

// store tokens in memory
tokenList[refreshToken] = {
  token,
  refreshToken,
  email: user.email,
  _id: user._id
};

to be:

// store tokens in memory
tokenList[refreshToken] = {
  token,
  refreshToken,
  email: user.email,
  _id: user._id,
  name: user.name
};

Lastly, in the `router.post(‘/token’, (req, res) => {` function, update the body object to be:

const body = { email: tokenList[refreshToken].email, _id: tokenList[refreshToken]._id, name: tokenList[refreshToken].name };

If you save your code changes, restart the server, and refresh the browser, you can test the new code. Once you log in, if you send a message, you should see your user’s name appear next to the message.

Storing messages in the database

To store the player’s messages in the database, we need to create a new model. To do this, create a new file called chatModel.js in the models folder. In this file, add the following code:

const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const ChatSchema = new Schema({
  email: {
    type: String,
    required: true
  },
  message: {
    type: String,
    required: true
  }
});

const ChatModel = mongoose.model('chat', ChatSchema);

module.exports = ChatModel;

In the code above, we created a new model called ChatModel and in this model we will only be storing the user’s email and the message that they sent.

Then, in app.js add the following code at the top of the file with the other require statements:

const ChatModel = require('./models/chatModel');

Next, in the `/submit-chatline’` function, uncomment the following line:

await ChatModel.create({ email, message });

Lastly, you can uncomment the following code in app.js:

app.get('/game.html', passport.authenticate('jwt', { session : false }), function (req, res) {
  res.sendFile(__dirname + '/public/game.html');
});

Attacking while chatting

With the chat model in place, the last thing we need to do is fix an issue when the player is typing a message. When the user is typing a message, if they press the spacebar key, the player will still attack. For our game, we only want the player to attack when they are focused on the game and when they are focused on the input box, the spacebar key should add a space.

To fix this, we just need to check if the input is focused when we check if the player can attack in the update function. In the update function, replace the following line:

if (Phaser.Input.Keyboard.JustDown(this.cursors.space) && !this.attacking) {

with the following:

if (Phaser.Input.Keyboard.JustDown(this.cursors.space) && !this.attacking && document.activeElement !== inputMessage) {

Conclusion

With that player attacking issue fixed, that brings this tutorial series to an end.

I hope you enjoyed this 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.