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:

May 09 2018 22 35 22
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 Phaser Mini-Degree is now available on Zenva Academy. Learn to code and make impressive games with JavaScript and Phaser 3!

Get Instant 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 pointeroverpointerout, 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.

Screen Shot 2018 05 10 at 10.10.43 PM
If you move your mouse over the close button, it should change colors.

Screen Shot 2018 05 10 at 10.18.04 PM
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.

Screen Shot 2018 05 10 at 10.45.30 PM
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.

May 12 2018 07 57 14
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:

Screen Shot 2018 05 12 at 1.24.30 PM
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:

Screen Shot 2018 05 12 at 1.29.40 PM
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.

May 09 2018 22 35 22
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.