Extending a Phaser Class to Make Reusable Game Objects
Once you’ve made it through the first couple of features in your game, you’ll realize that games involve a lot of repetitive elements. You might use a button to start the game, end the game, toggle the music, select a level or choose a character all on the same screen.
You can certainly create each element independently — but that will take a lot more time (that you could spend testing your game). Taking the existing Phaser classes and extending them to create reusable elements will speed up your game development and simplify testing and prototyping new levels, features, etc.
In this tutorial, we will walk through extending a Phaser built-in class to create a reusable button element. You can follow along from where my previous tutorials (Part I | Part II) left off or start from scratch in a TypeScript Phaser project.
Creating a Button class
We have previously created each button in our game by adding an image with interactions enabled, text and event handlers like this:
const settingsButton = this.add.image(200, 400, 'button').setInteractive();
const settingsButtonText = this.add.text(0, 0, 'Settings', {
color: '#000', fontSize: '28px'
});
Phaser.Display.Align.In.Center(settingsButtonText, settingsButton);settingsButtonText.on('pointerdown', () => {
settingsButton.setTexture('button_pressed');
if (this.gameSettings[1].value) {
this.sound.play('buttonSound');
}
}).on('pointerup', () => {
settingsButton.setTexture('button');
this.scene.launch('settings');
this.scene.stop();
});
This may not look like much code right now, but having to do this for every button we want to add will eventually get unruly. We can reuse a lot of this code and greatly reduce the amount we have to do manually for each unique button.
To create a reusable button element, we will start by extending the Phaser.GameObjects.Container
class. In your game.component.ts
file (or a separate .ts
file that you’ll import later), add the following:
class Button extends Phaser.GameObjects.Container { constructor(scene, x, y, fontColor, key1, key2, text) {
super(scene); this.scene = scene;
this.x = x;
this.y = y; const button = this.scene.add.image(x, y,
key1).setInteractive();
const buttonText = this.scene.add.text(x, y, text, { fontSize:
'28px', color: fontColor });
Phaser.Display.Align.In.Center(buttonText, button); this.add(button);
this.add(buttonText); button.on('pointerdown', () => {
button.setTexture(key2);
}); button.on('pointerup', () => {
button.setTexture(key1);
}); this.scene.add.existing(this);
}}
Now, we can go back to our settingsButton
and clean it up a little.
const settingsButton = new Button(this, 100, 100, '#000', 'button', 'button_pressed', 'Settings');
If you build and refresh your game, you will see a very similar setup as before. The position of your button may have shifted from where it was before because of the different coordinate systems but you can adjust the x
and y
values you pass in to the new Button()
method to get it where you want it.
When you click the button, you may notice we don’t have the sound anymore. Because we’re pulling the gameSettings
from localStorage
in the individual scene, we don’t have access to the preferences in our Button class. There is an easy way to fix this though.
In our MainScene
we will add a new method to play the button sound:
class MainScene extends Phaser.Scene {
... playButtonSound() {
if (this.gameSettings[1].value) {
this.sound.play('buttonSound');
}
}
}
And then we will update our Button
class to call this method, deferring the decision to play to our MainScene
instead:
class Button extends Phaser.GameObjects.Container {
... button.on('pointerdown', () => {
...
scene.playButtonSound(); })...
}
Next, you may notice that pressing the button no longer takes you to the SettingsMenu scene. Another simple fix that is achieved by adding an optional variable to our Button
method that handles a target scene.
class Button extends Phaser.GameObjects.Container {
targetScene: any; constructor(scene,x,y,fontColor,key1,key2,text,targetScene?) {
...
this.targetScene = targetScene; button.on('pointerup', () => {
button.setTexture(key1);
if (this.targetScene) {
setTimeout(() => {
this.scene.scene.launch(targetScene);
this.scene.scene.stop(scene);
}, 300);
}
}
}
And a minor update to our settingsButton
setup:
const settingsButton = new Button(this, 200, 400, '#000', 'button', 'button_pressed', 'Settings', 'settings');
Using the Button class
Now that we’ve created our Button
class, we can update our other buttons to use it. In the previous tutorial, we added toggle buttons for music and sound effects. We’ll update those buttons to use our new class now:
class SettingsMenu extends Phaser.Scene {
... create() {
const soundFxButton = new Button(this, 300, 115, '#000',
'button', 'button_pressed', this.gameSettings[1].value ===
true ? 'On' : 'Off'); const musicButton = new Button(this, 300, 100, '#000',
'button', 'button_pressed', this.gameSettings[0].value ===
true ? 'On' : 'Off'); ...
}
}
And remember, we will also need to add the playButtonSound
method to this class so we can play or not play the sound when a button is pressed depending on the user’s preferences:
playButtonSound() {
if (this.gameSettings[1].value) {
this.sound.play('buttonSound');
}
}
This gets us 90% of the way — but now clicking the music or sound effects button doesn’t toggle the setting. That’s because we’ve only given our Button
class the ability to change the scene. Since buttons will inevitably have different tasks, we may want to break out into separate classes or add a type variable to our Button
constructor so we know what actions the button should complete.
For our purposes, we’ll add a type
variable but either option is a good approach. Just a note, we will need to add type
to our constructor before the targetScene?
parameter because optionals should always be last.
class Button extends Phaser.GameObjects.Container {
...
currentText: any; constructor(scene,x,y,fontColor,key1,key2,text,type,targetScene?){
...
if (type === 'navigation') {
this.targetScene = targetScene;
} else if (type === 'toggle') {
this.currentText = text;
} ... button.on('pointerdown'), () => { ... }).on('pointerup', () => {
...
if (this.targetScene) {
...
} else if (this.currentText) {
buttonText.text = buttonText.text === 'On' ? 'Off' : 'On';
}
});
...
}
After updating our button creation calls to include type, you should have this in MainScene
:
const settingsButton = new Button(this, 100, 100, '#000', 'button', 'button_pressed', 'Settings', 'navigation', 'settings');
And this in SettingsMenu
:
const soundFxButton = new Button(this, 300, 115, '#000', 'button',
'button_pressed', this.gameSettings[1].value === true ? 'On' :
'Off', 'toggle');const musicButton = new Button(this, 300, 180, '#000',
'button', 'button_pressed', this.gameSettings[0].value === true ?
'On' : 'Off', 'toggle');
With this setup, when you click one of the toggle buttons the text will update. However, if you explore the localStorage settings (by clicking the Application tab of your Developer Tools in Chrome and expanding the Local Storage
option, you’ll see that our settings are no longer updating.
Like the additional method we created for navigation, we will need an additional method to notify our scene that a setting has been changed. We’ll do this by adding one more variable to our Button
class to identify the button and creating a toggleItem
method in our SettingsMenu
scene to handle this update.
In Button
class:
class Button extends Phaser.GameObjects.Container {
constructor(scene, x, y, fontColor, key1, key2, text, type, name,
targetScene?) {
...
this.name = name;
... button.on('pointerup', () => {
...
} else if (this.currentText) {
buttonText.text = buttonText.text === 'On' ? 'Off' : 'On';
scene.toggleItem(this, buttonText.text);
}
});
...
}
}
And in SettingsMenu
:
...
const soundFxButton = new Button(this, 300, 115, '#000', 'button',
'button_pressed', this.gameSettings[1].value === true ? 'On' :
'Off', 'toggle', 'sfx');...
const musicButton = new Button(this, 300, 180, '#000',
'button', 'button_pressed', this.gameSettings[0].value === true ?
'On' : 'Off', 'toggle', 'music');....toggleItem(button, text) {
if (button.name === 'sfx') {
this.gameSettings[1].value = text === 'On' ? true : false;
} else if (button.name === 'music') {
this.gameSettings[0].value = text === 'On' ? true : false;
}
localStorage.setItem('myGameSettings',
JSON.stringify(this.gameSettings));
}
To complete the circle and show off how quickly we can setup a new button, we’re going to add a ‘Back’ button on the Settings menu so the player can easily return to the Main menu.
class SettingsMenu extends Phaser.Scene {
create() {
... const backButton = new Button(this, 180, 230, '#000', 'button',
'button_pressed', 'Back', 'navigation', 'back', 'main');
}
}
And that’s it! We have successfully extended a Phaser class to make a reusable button that suits all of our needs. You should now have a better idea of what extending a class looks like and how it can provide simplifications of many game development situations.
You can download the completed code here: https://github.com/brsullivan/phaser-angular-app-step3
Thanks for reading this tutorial! Please feel free to leave me any feedback or questions in the comments.