Basic Button in Phaser 3 with RxJS and TypeScript

Simple button in TypeScript with Reactive Programming concepts

by on 8 minute read updated on


Buttons are commonly used in game menus and UI but they are not built into Phaser 3.

However, every GameObject in Phaser can be made interactive and respond to events like up, out, and over.

This means we can make a button pretty easily!

In this article, we will look at creating a basic button in Phaser that uses RxJS to dispatch click events.

Why RxJS?

RxJS is a reactive programming library for JavaScript that combines the Observer pattern, Iterator pattern, and functional programming.

Reactive programming provides a clear, concise, and clean way to deal with real-time events like UI interactions, API responses, and more.

It makes implementing a feature like double click clear and trivial compared to doing it the traditional way.

1
2
3
4
5
6
7
8
9
button.onClick()
	.pipe(
		bufferTime(300),
		filter(values => values.length === 2),
		map(values => values.pop())
	)
	.subscribe(evt => {
		console.log('double click')
	})

This code is taking a stream of click events that get buffered for 300 milliseconds, then filtered for groups with only 2 events, and then returns the last event to subscribers. Tada! Double click detected.

We won't go into the specifics of RxJS in this article but let us know in the comments below if you'd like to learn more about how you can use it in your games.

Creating a Button

A button is just an object that responds to mouse_up, mouse_down, mouse_over, and mouse_out events.

Any GameObject in Phaser can listen for those events and respond accordingly.

For example, a button can change color or texture for each of those events.

Because a button is visual, we are going to subclass Phaser.GameObjects.Image and then add some button specific logic.

The code is in TypeScript and quite long but it is all very simple. 👌

  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
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import Phaser from 'phaser'

const WHITE = 0xffffff

export default class Button extends Phaser.GameObjects.Image
{
	private upTexture: string
	private upTint: number
	private downTexture: string
	private downTint: number
	private overTexture: string
	private overTint: number
	private disabledTexture: string
	private disabledTint: number

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

		this.setTint(tint)

		this.upTexture = texture
		this.upTint = tint
		this.downTexture = texture
		this.downTint = tint
		this.overTexture = texture
		this.overTint = tint
		this.disabledTexture = texture
		this.disabledTint = tint

		this.setInteractive()
			.on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, this.handleUp, this)
			.on(Phaser.Input.Events.GAMEOBJECT_POINTER_OUT, this.handleOut, this)
			.on(Phaser.Input.Events.GAMEOBJECT_POINTER_DOWN, this.handleDown, this)
			.on(Phaser.Input.Events.GAMEOBJECT_POINTER_OVER, this.handleOver, this)
	}

	setUpTexture(texture: string)
	{
		this.upTexture = texture
		return this
	}

	setUpTint(tint: number)
	{
		this.upTint = tint
		return this
	}

	setDownTexture(texture: string)
	{
		this.downTexture = texture
		return this
	}

	setDownTint(tint: number)
	{
		this.downTint = tint
		return this
	}

	setOverTexture(texture: string)
	{
		this.overTexture = texture
		return this
	}

	setOverTint(tint: number)
	{
		this.overTint = tint
		return this
	}

	setDisabledTexture(texture: string)
	{
		this.disabledTexture = texture
		return this
	}

	setDisabledTint(tint: number)
	{
		this.disabledTint = tint
		return this
	}

	setDisabled(disabled: boolean)
	{
		if (disabled)
		{
			this.setTexture(this.disabledTexture)
			this.setTint(this.disabledTint)
			this.disableInteractive()
			return this
		}

		this.setTexture(this.upTexture)
		this.setTint(this.disabledTint)
		this.setInteractive()

		return this
	}

	private handleUp(pointer: Phaser.Input.Pointer)
	{
		this.handleOver(pointer)
	}

	private handleOut(pointer: Phaser.Input.Pointer)
	{
		this.setTexture(this.upTexture)
		this.setTint(this.upTint)
	}

	private handleDown(pointer: Phaser.Input.Pointer)
	{
		this.setTexture(this.downTexture)
		this.setTint(this.downTint)
	}

	private handleOver(pointer: Phaser.Input.Pointer)
	{
		this.setTexture(this.overTexture)
		this.setTint(this.overTint)
	}
}

Starting at the top of the class we have a tint and texture property for each state the button can be in: up, down, over, or disabled.

The starting values given to the constructor will be the values for the up state. texture and tint values can then be changed by their respective set methods like setOverTexture(texture: string) and setOverTint(tint: number).

We set the button to interactive on line 31 and then register listeners for each event.

And that's the core logic for display and interaction. Simple!

Handling Click Events

The Button class we just made can be displayed in a Scene and respond to events but there's no designated click event.

We can use .on(Phaser.Input.Events.GAMEOBJECT_POINTER_UP, handler) but we will leverage the power of RxJS instead. 💪

 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
import Phaser from 'phaser'

import { Subject } from 'rxjs'

export default class Button extends Phaser.GameObjects.Image
{
	// properties ...

	private clickSubject: Subject<Phaser.Input.Pointer> = new Subject()

	// constructor ...

	destroy(fromScene: boolean = false)
	{
		this.clickSubject.complete()

		super.destroy(fromScene)
	}

	onClick()
	{
		return this.clickSubject.asObservable()
	}

	// setters ...

	private handleUp(pointer: Phaser.Input.Pointer)
	{
		this.handleOver(pointer)

		this.clickSubject.next(pointer)
	}

	// other states ...
}

You'll need to run npm install rxjs to import Subject like we are doing on line 3. We are also using the phaser3-parcel-template to bootstrap this example project.

A Subject is created on line 9 that will handle dispatching events.

There will often only be a single observer listening to a click event but there is no reason why two or more can't be listening as well. A Subject is a special Observable that allows broadcasting to multiple observers so it is well suited for this job.

Line 15 is just some cleanup where we set the Subject to complete.

Then on line 20 is the onClick() method that returns an Observable from the clickSubject. This is how external code using the Button class will listen for click events.

And finally, on line 31 we dispatch a click event by calling this.clickSubject.next() and passing in the pointer instance. Any code listening for click events from this button will be notified immediately. 🎉

Putting it in a Scene

Let's see our Button class in action by putting it in a Scene.

We will be using image assets from Kenney's platformer pack.

 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
import Phaser from 'phaser'
import Button from '../buttons/Button'

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')
	}

	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 = new Button(this, 400, 250, ButtonUp, Orange)
			.setDownTexture(ButtonDown)
			.setOverTint(LightOrange)

		this.add.existing(button)

		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 key lines are 32 where we create the button and then line 36 where we add it to the Scene.

Then on line 38, we use the onClick() method to get an Observable and subscribe to it.

Each time a click event happens we spawn a gem that will bounce around the screen randomly.

It should look something like this:

Adding Text

Buttons often have text. Something like “Play” or “Settings”.

You can add text using this.add.text() and placing it at the same location as the Button.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
create()
{
	const gems = this.physics.add.group({
		classType: Phaser.Physics.Arcade.Image
	})
	this.physics.add.collider(gems, gems)

	const button = new Button(this, 400, 250, ButtonUp, Orange)
		.setDownTexture(ButtonDown)
		.setOverTint(LightOrange)

	this.add.existing(button)

	this.add.text(400, 250, 'Text over Button', { color: 'black' })
		.setOrigin(0.5, 0.5)

	button.onClick().subscribe(pointer => {
		this.spawnGem(gems, button.x, button.y - 50)
	})
}

The Text we added on line 14 is a completely separate GameObject so you will have to update its position whenever you move the button.

Phaser was designed to have a flat hierarchy. This may seem odd if you've used Flash, cocos2d, or other popular game frameworks.

Instead of allowing every GameObject to have children, Phaser has a special GameObject called a Container that can be used to group objects together.

Next Steps

You can now make a simple button that can change textures or colors on different states and respond to click events.

In the next article, we will look at using a Container to group a Button and Text instance together so that they can be moved, scaled, rotated, and manipulated as one object.

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