How to Make a Menu with a Selection Cursor in Phaser 3

For menus in games that only support keyboard or gamepad input

by on 8 minute read


Does your game have a main menu with options that can be selected using the keyboard arrow keys or a controller's D-pad?

Maybe it is something inspired by Final Fantasy, Harvest Moon, or Super Mario Bros?

Great choice! They are tried and true UI mechanics but perhaps you are unsure of how to move a selection cursor to the correct button or know which button is then selected? 🤔

If that's the case then this article is what you're looking for! We'll show you how to create a menu like this 👇 for any number of items.

Example Set-Up

This example will consist of one Scene that loads a button image and a cursor hand image. Both images are from Kenney's UI Pack: Space Expansion.

The code will be in TypeScript although it will be very light on types. You can easily convert it to modern JavaScript by removing all the type information.

But if you would like to learn more about making games in Phaser 3 and TypeScript then we have a great, free book to help you get started!

Learn to make an Infinite Runner in Phaser 3 with TypeScript!

Drop your email into the box below to get this free 90+ page book and join our newsletter.

Learn more about the book here.

Here's what our barebones Scene looks like:

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

export default class MainMenuScene extends Phaser.Scene
{
	private cursors!: Phaser.Types.Input.Keyboard.CursorKeys

	constructor()
	{
		super('main-menu')
	}

	init()
	{
		this.cursors = this.input.keyboard.createCursorKeys()
	}

	preload()
    {
		this.load.image('glass-panel', 'assets/glassPanel.png')
		this.load.image('cursor-hand', 'assets/cursor_hand.png')
    }

    create()
    {
		// TODO
	}

	selectButton(index: number)
	{
		// TODO
	}

	selectNextButton(change = 1)
	{
		// TODO
	}

	confirmSelection()
	{
		// TODO
	}
	
	update()
	{
		const upJustPressed = Phaser.Input.Keyboard.JustDown(this.cursors.up!)
		const downJustPressed = Phaser.Input.Keyboard.JustDown(this.cursors.down!)
		const spaceJustPressed = Phaser.Input.Keyboard.JustDown(this.cursors.space!)
		
		if (upJustPressed)
		{
			this.selectNextButton(-1)
		}
		else if (downJustPressed)
		{
			this.selectNextButton(1)
		}
		else if (spaceJustPressed)
		{
			this.confirmSelection()
		}
	}

We create a cursors class property to store an instance of CursorKeys for simple access to the up, down, and space keys. They will be used to move our selection up and down as well as confirm the selection.

The create() method is where we will create the menu buttons. The 3 other methods will handle selecting the appropriate menu items.

Notice that we use selectNextButton() in the update() method when up or down is pressed. This is where our logic for moving to the next button will go.

Lastly, the confirmSelection() method is called when the space key is pressed for taking an action associated with the selected button.

Creating Menu Buttons

We'll create 3 buttons in a vertical layout for Play, Settings, and Credits.

Each button will consist of an Image with a Text object layered on top of it.

 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
create()
{
	const { width, height } = this.scale

	// Play button
	const playButton = this.add.image(width * 0.5, height * 0.6, 'glass-panel')
		.setDisplaySize(150, 50)
	
	this.add.text(playButton.x, playButton.y, 'Play')
		.setOrigin(0.5)

	// Settings button
	const settingsButton = this.add.image(playButton.x, playButton.y + playButton.displayHeight + 10, 'glass-panel')
		.setDisplaySize(150, 50)

	this.add.text(settingsButton.x, settingsButton.y, 'Settings')
		.setOrigin(0.5)

	// Credits button
	const creditsButton = this.add.image(settingsButton.x, settingsButton.y + settingsButton.displayHeight + 10, 'glass-panel')
		.setDisplaySize(150, 50)

	this.add.text(creditsButton.x, creditsButton.y, 'Credits')
		.setOrigin(0.5)
}

Now that we have 3 buttons, we can keep track of where a selection cursor should go by storing the buttons in an Array and keeping track of which index is currently selected.

Let's do that by creating two new class property called buttons and selectedButtonIndex:

1
2
3
4
5
6
7
8
9
export default class MainMenuScene extends Phaser.Scene
{
	// other properties...

	private buttons: Phaser.GameObjects.Image[] = []
	private selectedButtonIndex = 0

	// other code...
}

Then in create() we can add the 3 buttons to the buttons Array like this:

1
2
3
4
5
6
7
create()
{
	// previous code...
	this.buttons.push(playButton)
	this.buttons.push(settingsButton)
	this.buttons.push(creditsButton)
}

Lastly, we can add the hand cursor to the Scene and store a reference to it in a class property called buttonSelector:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export default class MainMenuScene extends Phaser.Scene
{
	// other properties...

	private buttonSelector!: Phaser.GameObjects.Image

	create()
	{
		// previous code...

		this.buttonSelector = this.add.image(0, 0, 'cursor-hand')
	}

	// other code...
}

We use a class property to store a reference to the hand cursor so that we can adjust its x and y position in selectButton(index).

Selecting a Button

Now that we've created the menu we can implement the selectButton(index) method to move the buttonSelector and tint the selected button.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
selectButton(index: number)
{
	const currentButton = this.buttons[this.selectedButtonIndex]

	// set the current selected button to a white tint
	currentButton.setTint(0xffffff)

	const button = this.buttons[index]

	// set the newly selected button to a green tint
	button.setTint(0x66ff7f)

	// move the hand cursor to the right edge
	this.buttonSelector.x = button.x + button.displayWidth * 0.5
	this.buttonSelector.y = button.y + 10

	// store the new selected index
	this.selectedButtonIndex = index
}

This method will retrieve the Image from the this.buttons Array using the passed in index and set it as the newly selected button.

It unselects the currently selected button by setting the tint back to white.

Then it gives the newly selected button a green tint and moves the buttonSelector to be over the right edge of the button.

Lastly, it sets this.selectedButtonIndex to the passed in index value.

Add a call to this.selectButton(0) in the create() method to start the menu with the first option selected:

1
2
3
4
5
create()
{
	// previous code...
	this.selectButton(0)
}

Selecting the Next Button

With the selectButton(index) method implemented, selecting the next button up or down is as simple as passing in an index value that is -1 or +1 from what selectedButtonIndex is.

Here's what selectNextButton(change) should look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
selectNextButton(change = 1)
{
	let index = this.selectedButtonIndex + change

	// wrap the index to the front or end of array
	if (index >= this.buttons.length)
	{
		index = 0
	}
	else if (index < 0)
	{
		index = this.buttons.length - 1
	}

	this.selectButton(index)
}

The bulk of the code in this method just wraps the index value to 0 if it exceeds the length of the array or to this.buttons.length - 1 if it goes under zero. This will keep index within the bounds of the Array.

Remember that selectNextButton() is called by the update() method when the up or down keys are pressed.

Passing in -1 will result in selecting the button above the current one and 1 will result in selecting the button below.

This menu is almost completely functional! The last thing is to confirm the selection.

Confirming a Selection

Buttons are usually activated by a mouse click or a touch. How would we do it using the space key? 🤔

We can use the EventEmitter instance that exists on every GameObject. You may have used something like .on('click', this.handler) with a Sprite or Image before.

We can do something similar to that by emitting a different event name in confirmSelection():

1
2
3
4
5
6
7
8
confirmSelection()
{
	// get the currently selected button
	const button = this.buttons[this.selectedButtonIndex]

	// emit the 'selected' event
	button.emit('selected')
}

Then to use the 'selected' event we just need to listen to it after we create each button like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
create()
{
	// other code...

	playButton.on('selected', () => {
		console.log('play')
	})

	settingsButton.on('selected', () => {
		console.log('settings')
	})

	creditsButton.on('selected', () => {
		console.log('credits')
	})
}

Now, when you press space the selected button will get an event named 'selected' and the handler will get called.

You can replace the example console.log() with more appropriate logic like going to a different Scene.

One final thing to remember is that each .on() should have a matching .off() to ensure that events are cleaned up. Something like this:

1
2
3
4
5
6
7
create()
{
	this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
		playButton.off('selected')
		// ...
	})
}

Next Steps

This seemingly simple menu selection mechanic took quite a bit of explaining!

You can extend this with as many buttons as you need and use it anywhere you have a selection menu. Perhaps in a dialogue box that asks for player input? 🧐

For cases where the location of the selection cursor on each button can be different, store an offset with each button reference instead of just the button in the buttons Array.

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 ui menu cursor

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