Container Button in Phaser 3 with RxJS and TypeScript

Keep Button and Text together with a Container

by on 6 minute read


Phaser doesn't come with buttons but making a basic one is easy.

One issue with the basic button is that text can't be added to the button as a child. Phaser is designed to have a flat hierarchy so you can't add children like you might be used to in Flash, cocos2d, or other game frameworks.

Instead, Phaser has a Container type that is designed to group multiple GameObjects.

Those coming from Flash or Unity have probably created nodes called “container” to group things for simpler to manage animations.

That is the same concept. In this article, we will create a ButtonContainer class that inherits from Phaser.GameObjects.Container to hold a Button and a Text object.

Wrapping a Button

The ButtonContainer class will use a little programming kung-fu to reuse all the logic we implemented in Button.

First, we will add an IButton interface to Button.ts. Then add it to the Button class definition with implements. We are using TypeScript so you can ignore this if you prefer plain JavaScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
declare global
{
	interface IButton extends Phaser.GameObjects.GameObject, Phaser.GameObjects.Components.Transform
	{
		onClick(): Observable<Phaser.Input.Pointer>

		setUpTexture(texture: string): this
		setUpTint(tint: number): this
		setDownTexture(texture: string): this
		setDownTint(tint: number): this
		setOverTexture(texture: string): this
		setOverTint(tint: number): this
		setDisabledTexture(texture: string): this
		setDisabledTint(tint: number): this

		setDisabled(disabled: boolean): this
	}
}

export default class Button extends Phaser.GameObjects.Image implements IButton
{
	// ...
}

Next, our ButtonContainer will implement a version of IButton with two additional methods to handle text.

1
2
3
4
5
6
7
8
declare global
{
	interface IButtonContainer extends IButton
	{
		setText(text: string): this
		setTextStyle(style: object): this
	}
}

Put this in ButtonContainer.ts.

When we create the ButtonContainer class we will implement IButtonContainer which will include all the methods defined in IButton.

We are doing this so that we can create a Button in ButtonContainer and then forward the IButton method calls to the Button instance.

This allows us to reuse all the code we already wrote!

ButtonContainer

The ButtonContainer class will be very simple compared to Button because we are forwarding most of the heavy lifting to Button.

The main difference is that we will create a Text and add it along with the Button.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import Phaser from 'phaser'
import Button from './Button'

// ... declare IButtonContainer interface here

export default class ButtonContainer extends Phaser.GameObjects.Container implements IButtonContainer
{
	private button: IButton
	private text: Phaser.GameObjects.Text

	constructor(scene: Phaser.Scene, x: number, y: number, texture: string, tint: number = 0xffffff)
	{
		super(scene, x, y)

		this.button = new Button(scene, 0, 0, texture, tint)
		this.text = scene.add.text(0, 0, 'Button', { color: 'black' })
			.setOrigin(0.5, 0.5)

		this.add(this.button)
		this.add(this.text)
	}

	onClick()
	{
		return this.button.onClick()
	}

	setText(text: string)
	{
		this.text.text = text
		return this
	}

	setTextStyle(style: object)
	{
		this.text.setStyle(style)
		return this
	}

	setUpTexture(texture: string)
	{
		this.button.setUpTexture(texture)
		return this
	}

	setUpTint(tint: number)
	{
		this.button.setUpTint(tint)
		return this
	}

	// ... implement the rest of the IButton methods and forward to this.button like above
}

Most of the ButtonContainer class will be methods that forward to this.button.

The key differences are in the constructor where we create a Button and a Text and then call this.add() to add them as children to the ButtonContainer.

This will allow us to manipulate everything as a single object. 🎉

Augment GameObjectFactory

We can use ButtonContainer right now by calling new ButtonContainer() but we can also get a little fancy and augment Phaser's GameObjectFactory.

This will allow us to use a nicer, idiomatic syntax like: this.add.buttonContainer()

1
2
3
4
Phaser.GameObjects.GameObjectFactory.register('buttonContainer', function (x: number, y: number, key: string, tint: number = 0xffffff) {
	// @ts-ignore
    return this.displayList.add(new ButtonContainer(this.scene, x, y, key, tint))
})

You can add this to the bottom of ButtonContainer.ts.

For TypeScript, you'll also want to add a type definition file like types/GameObjectFactory.d.ts.

1
2
3
4
5
6
7
declare namespace Phaser.GameObjects
{
	interface GameObjectFactory
	{
		buttonContainer(x: number, y: number, texture: string, tint?: number): IButtonContainer
	}
}

This uses Declaration Merging to provide appropriate IntelliSense and type checking.

Using ButtonContainer

Our Scene is going to be very similar to the one from the basic button.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import Phaser from 'phaser'
import ButtonContainer from '../buttons/ButtonContainer'

const ButtonUp = 'button_up'
const ButtonDown = 'button_down'
const Gem = 'gem'

const Orange = 0xFFAD00
const LightOrange = 0xffcd60

export default class ButtonDemo extends Phaser.Scene
{
	constructor()
	{
		super('button-demo-basic')
	}

	preload()
    {
		this.load.image(ButtonUp, 'assets/grey_button03.png')
		this.load.image(ButtonDown, 'assets/grey_button00.png')
		this.load.image(Gem, 'assets/gemRed.png')
    }

    create()
    {
		const gems = this.physics.add.group({
			classType: Phaser.Physics.Arcade.Image
		})
		this.physics.add.collider(gems, gems)

		const button = this.add.buttonContainer(400, 250, ButtonUp, Orange)
			.setDownTexture(ButtonDown)
			.setOverTint(LightOrange)
			.setText('Together Forever!')

		button.onClick().subscribe(pointer => {
			this.spawnGem(gems, button.x, button.y - 50)
		})
	}
	
	private spawnGem(group: Phaser.Physics.Arcade.Group, x, y)
	{
		const gem: Phaser.Physics.Arcade.Image = group.get(x, y, Gem)
		gem.setVelocity(Phaser.Math.Between(-100, 100), Phaser.Math.Between(-100, 100))
		gem.setBounce(1, 1)
		gem.setCollideWorldBounds()
	}
}

The only differences are the highlighted lines!

We import the ButtonContainer class on line 2 so that Phaser's GameObjectFactory will be properly augmented with the buttonContainer() factory function.

Then the button creation code is on lines 32 - 35. We don't need to create a separate Text object because the ButtonContainer already creates one.

Run the code and it should look like this:

Next Steps

You now have a button that is more concise to create and easier to animate. From here you can implement more UI elements like checkboxes and radio buttons.

Or you can leverage the fact that Phaser is an HTML5 framework and use the wide selection of CSS UI frameworks like Bootstrap, Bulma, and more!

In the next article, we will show you how to use Phaser's DOMElement to create HTML buttons that can be controlled from the game code!

Let us know in the comments below if anything doesn’t work or is unclear. We’ll be happy to fix or clarify!

Be sure to sign up for our newsletter so you don't miss any future Phaser 3 game development tips and techniques!

Drop your email into the box below.

Don't miss any Phaser 3 game development content

Subscribers get exclusive tips & techniques not found on the blog!

Join our newsletter. It's free. We don't spam. Spamming is for jerks.

Phaser 3 Buttons RxJS TypeScript

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

Fix Stretched Image Distortions in Phaser 3 with 9-Slice Scaling

by on

Are you having image distortion problems when scaling to make a button or panel graphic bigger? Multiple versions of the …

5 minute read

Command Pattern to Undo Player Actions

by on

Are you looking for a clean and reusable way to implement undo for player actions? Perhaps you are making a turn-based …

15 minute read

Advanced Logging with the Strategy Pattern

by on

Have you ever tried debugging a problem with your game that only seems to happen in production? The Developer Tools …

7 minute read

State Pattern for Character Movement in Phaser 3

by on

Writing clean and well-organized code is something all game developers aspire to. We want code to be reusable and easy …

7 minute read

Didn't find what you were looking for?


comments powered by Disqus