Create a Dialog Modal Plugin in Phaser 3 – Part 2

In part one of this tutorial, we covered the following topics:

  • How to create a basic plugin in Phaser 3.
  • How to load and register a plugin.
  • Setup the default properties for our plugin.
  • Setup the logic for dynamically placing the dialog window.
  • Created the basic dialog window.

In case you missed it, you can find part one here. In this tutorial, we are going to wrap up our dialog window plugin by adding the following:

  • Adding a close button to the dialog window.
  • Adding logic to show/hide the window.
  • Added logic to set and display dialog text in the window.
  • Add logic to animate the text.

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 see what we will be creating below:


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

Let’s get started!

Learn Phaser 3 with our newest Mini-Degree

The HTML5 Game Development Mini-Degree is now available for Pre-Order on Zenva Academy. Learn to code and make impressive games with JavaScript and Phaser 3!

Get Instant Early Access

Closing the window

In order to create our close button, we are going to use Phaser’s Graphic and Text GameObjects. First, we will use the¬†Text GameObject to create the close button, and then we will make it interactive so we can click on it. Then, we will use the¬†GameObject.Graphics to create a small rectangle border around the button to set it apart.

Open up dialog_plugin.js in your code editor and add the following code below the _createOuterWindow method at the bottom of the file:

// Creates the close dialog window button
_createCloseModalButton: function () {
  var self = this;
  this.closeBtn = this.scene.make.text({
    x: this._getGameWidth() - this.padding - 14,
    y: this._getGameHeight() - this.windowHeight - this.padding + 3,
    text: 'X',
    style: {
      font: 'bold 12px Arial',
      fill: this.closeBtnColor
    }
  });
  this.closeBtn.setInteractive();

  this.closeBtn.on('pointerover', function () {
    this.setTint(0xff0000);
  });
  this.closeBtn.on('pointerout', function () {
    this.clearTint();
  });
  this.closeBtn.on('pointerdown', function () {
    self.toggleWindow();
  });
}

Let’s review the code we just added:

  • First, we created the ‘x’ close button by calling¬†scene.make.text, and we passed the x and y position of the text, the text that is displayed in the game, and the style of the text we are displaying.
  • In order to have our plugin work with different game sizes, we calculate the x and y positions¬†dynamically.
    • For the x position, we call the¬†_getGameWidth()¬†method to get the width of the game, and we subtract the padding between the window and the edge of the game. This will give us the position of the right border of our window, and from there we can¬†subtract some pixels to keep the button near the right of the window.
    • For the y position, we call the¬†_getGameHeight()¬†method to get the height of the game, and then we subtract the height of our dialog window and the padding between the bottom of the window and the bottom of the scene. This will give us the position for the top of the window, and from there we can add a few pixels to keep the close button near the top of the window.
  • Next, we call the¬†setInteractive¬†method on our¬†closeBtn¬†GameObject. The¬†setInteractive¬†method will pass the GameObject to the Input Manager so it can be enabled for input.
  • Lastly, with the¬†closeBtn¬†enabled for input, we add code to listen for the¬†pointerover,¬†pointerout, and¬†pointerdown¬†events.
    • In the¬†pointerover¬†event, we change the tint of the¬†closeBtn, and on the¬†pointerdown¬†event, we remove the tint.
    • In the¬†pointerdown¬†event, we call a new method called¬†toggleWindow. This method will be used to show/hide our dialog window.

Next, in the _createWindow method add the following code at the bottom of the function:

this._createCloseModalButton();

If you reload your game in the browser, you should see the new close button.


If you move your mouse over the close button, it should change colors.


Now, we will add the border around the close button. In dialog_plugin.js, add the following code below the _createCloseModalButton method we just added:

// Creates the close dialog button border
_createCloseModalButtonBorder: function () {
  var x = this._getGameWidth() - this.padding - 20;
  var y = this._getGameHeight() - this.windowHeight - this.padding;
  this.graphics.strokeRect(x, y, 20, 20);
}

In the code above we:

  • Calculated the x and y position of our rectangle border using the same logic we used when we created the close button.
  • Called the¬†strokeRect¬†method to draw our rectangle without any fill.

To see the new border in our game, we need to add the following code to the bottom of the _createWindow method:

this._createCloseModalButtonBorder();

If you refresh your game, you should see the new border.


Finally, to show/hide the window we need to create the toggleWindow method. We will use this method to update the visible property of each GameObject of the window.

In dialog_plugin.js, add the following code below the  _createCloseModalButtonBorder method we just added:

// Hide/Show the dialog window
toggleWindow: function () {
  this.visible = !this.visible;
  if (this.text) this.text.visible = this.visible;
  if (this.graphics) this.graphics.visible = this.visible;
  if (this.closeBtn) this.closeBtn.visible = this.visible;
}

Now, if you refresh your game and click on the close button, the dialog modal should disappear from the game.


Adding text to the window

With the basic functionality of the window in place, we will now work on adding the actual dialog piece. In dialog_plugin.js, add the following code below the toggleWindow method we just added:

// Sets the text for the dialog window
setText: function (text) {
  this._setText(text);
},

// Calcuate the position of the text in the dialog window
_setText: function (text) {
  // Reset the dialog
  if (this.text) this.text.destroy();

  var x = this.padding + 10;
  var y = this._getGameHeight() - this.windowHeight - this.padding + 10;

  this.text = this.scene.make.text({
    x,
    y,
    text,
    style: {
      wordWrap: { width: this._getGameWidth() - (this.padding * 2) - 25 }
    }
  });
}

Let’s review the code we just added:

  • We added two new methods:¬†setText¬†and¬†_setText. The¬†setText¬†method will be the method we call from¬†game.js, and it takes the text we want to set in the dialog window as a parameter. Currently, this method just calls the¬†_setText¬†method, and we will add more code to this method in a bit.
  • The¬†_setText¬†method is used to create a new Text GameObject and to place the new GameObject in the dialog window.
  • In the¬†_setText¬†method, we first check to see if the¬†text¬†property of our plugin has been set, and if it has been, then we destroy that GameObject.
  • Then, we calculate the x position for the new GameObject by adding the¬†padding¬†property and 10 pixels, which will place the text GameObject just inside the dialog window border.
  • Next, we calculate the y position for the new GameObject by getting the height of the scene. Then we subtract the padding between the window and the bottom of the scene, and then we subtract the height of dialog window. Lastly, we add 10 pixels to place the new GameObject just below the top border of the dialog window.
  • Finally, we call the¬†scene.make.text¬†method to create the new GameObject. For the style of the new Text GameObject, we pass the¬†wordWrap¬†property. The¬†wordWrap¬†property will force the text to stay within the width you pass, by inserting the remaining text on a new line. Phaser does this by looking at the all the spaces within the string and for each one, if the length of the string at that point will go over the word wrap width then it will insert a new line character.

Now, we can test the new setText method. In game.js, add the following code at the bottom of the create function:

this.sys.dialogModal.setText('Testing this.');

If you refresh the game, you should see the next text displayed:


If you replace the¬†‘Testing this’¬†text with a longer string, you can see the word wrap keeps the text inside the dialog window. Update the following line¬†this.sys.dialogModal.setText(‘Testing this.’);¬†to be:

this.sys.dialogModal.setText('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.');

If you refresh the game, you should see the new text:


Animating the text

With the dialog window now displaying the text, we are going to add an option to the dialog window that will allow for the text to animate onto the screen. To do this, we are going to take the text that is passed to the setText method as an argument, and then we will create an array of each character in that string. Then, we are going to set up an event that will slowly display each character of the array.

Now, in dialog_model.js update the setText method to match the following code:

// Sets the text for the dialog window
setText: function (text, animate) {
  // Reset the dialog
  this.eventCounter = 0;
  this.dialog = text.split('');
  if (this.timedEvent) this.timedEvent.remove();

  var tempText = animate ? '' : text;
  this._setText(tempText);

  if (animate) {
    this.timedEvent = this.scene.time.addEvent({
      delay: 150 - (this.dialogSpeed * 30),
      callback: this._animateText,
      callbackScope: this,
      loop: true
    });
  }
}

The code snippet above is pretty small, but there is a lot going on, so let’s review what we just added:

  • First, we added an additional parameter to the¬†setText¬†method called¬†animate. This parameter will allow for you to specify if the text will be animated or not.
  • Then, we reset the properties that are used for animating the text.
    • We take the text that is passed in as an argument, and we use the¬†split¬†method to create an array of all the characters in that string. We then store this array in the¬†dialog¬†property.
    • The¬†eventCounter¬†property is used for displaying each character in the¬†dialog¬†property.
    • Lastly, we check to see if the¬†timedEvent¬†property exists and we remove it if it does.
  • Next, we create a variable called¬†tempText. If the¬†animate¬†argument was set to true, then we set the¬†tempText¬†variable to be an empty string, and if the¬†animate¬†argument was set to false, then we set¬†tempText¬†to equal the¬†text¬†argument. We then call the¬†_setText¬†method and pass it the¬†tempText¬†variable.
  • Lastly, we check if the¬†animate¬†argument was set to true, and if it was then we create a new Phaser Time Event and store it in the¬†timedEvent¬†property.
    • Phaser 3 has a new Clock class, and every state in Phaser 3 has one of these classes tied to it. These clocks allow you to add events to them, and when you add these events you can specify when and how often you would like that event to trigger by passing an Object with that information.
    • The¬†delay¬†property is used to specify the delay in milliseconds until this event will trigger.
    • The¬†callback¬†property allows for you to specify which function will be called when the event is triggered. We set this to the¬†_animateText¬†method, which we haven’t added yet. This method will be used for displaying the text that is stored in the¬†dialog¬†property.
    • The¬†callbackScope¬†property allows for you specify the scope in which the callback function will be called. For this event, we passed the scope of the plugin.
    • Finally, the¬†loop¬†property is used to specify if you want the event to keep triggering at the specified delay. When you set a loop to repeat, you can call the¬†remove¬†method which will kill the event.

Now that the event is in place, let’s add the¬†_animateText¬†method. In¬†dialog_model.js¬†add the following code below the¬†setText¬†method:

// Slowly displays the text in the window to make it appear annimated
_animateText: function () {
  this.eventCounter++;
  this.text.setText(this.text.text + this.dialog[this.eventCounter - 1]);
  if (this.eventCounter === this.dialog.length) {
    this.timedEvent.remove();
  }
}

Every time the _animateText method is called we do the following:

  • We increment the¬†eventCounter¬†property by one.
  • We update the current text of our Text GameObject by calling the¬†setText¬†method, and we pass the current text value plus the next character in our¬†dialog¬†array.
  • Finally, we check to see if we printed out each character in our¬†dialog¬†array by checking if the¬†eventCounter¬†property is equal to the length of the array, and if it is then we call the¬†remove¬†method on our event.

Finally, to test the new animated text, we just need to update the¬†this.sys.dialogModal.setText(‘…’);¬†line in¬†game.js¬†to be:

this.sys.dialogModal.setText('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', true);

Now, if you refresh your game you should see the same text appear in the dialog window, but it should appear one character at a time.


Additional Cleanup

Before we wrap up, let’s add a few lines of code to our plugin that will run when the close button is clicked, and when the scene is shut down. This code will be used to kill the Phaser Time Event if it is running, and it will destroy the Text GameObject if it exists.

First, in dialog_plugin.js add the following code to the shutdown method:

if (this.timedEvent) this.timedEvent.remove();
if (this.text) this.text.destroy();

Then, in the _createCloseModalButton method add the following code to the pointerdown event listener function:
if (self.timedEvent) self.timedEvent.remove();
if (self.text) self.text.destroy();

Conclusion

With those last pieces of code in place, that brings this tutorial to a close. In summary, this tutorial showed you how to create a plugin for Phaser 3.

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.