Memory Match in Modern Javascript with Phaser 3 - Part 3

A more involved game for beginners to Phaser 3 and modern JavaScript

by on 10 minute read updated on


If you've gone through the basic first game in modern JavaScript or the infinite jumper book then you've got the basics down so let's try making something a little bit more complicated!

We suggest Memory Match. A Mario Party-inspired memory game where you control a character to pick boxes until all matches are found within a limited amount of time.

In part 2, we added a 3x3 grid of boxes with depth sorting to make sure everything is layered properly.

For part 3, we are going to start selecting boxes and revealing what is inside of them.

We also have a video version on YouTube if you prefer to watch or want to see how it is coded in real-time.

When a Box Can Be Opened

The player will be able to open a box by pressing the space key. But first, we need to know which box the player wants to open.

There's a couple of ways to do this and we will take the simplest approach by designating a box active when the player collides with it.

Update the code that creates a collider like this:

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

	// replace this 👇
	// this.physics.add.collider(this.boxGroup, this.player)

	// by adding a collision callback
	this.physics.add.collider(
		this.boxGroup,
		this.player,
		this.handlePlayerBoxCollide, // 👈 here
		undefined,
		this
	)
}

We broke this line up into multiple lines for ease of reading. You are free to keep it as one line if you prefer.

The main thing to notice is the addition of the this.handlePlayerBoxCollide callback and the passing of this as the last argument.

This will allow Phaser to call our handlePlayerBoxCollide() method with the context set to this so that we can continue to reference properties on the Scene.

Check out this video for more information about JavaScript scopes and contexts as it relates to Phaser 3.

Next, let's create the handlePlayerBoxCollide() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * 
 * @param {Phaser.Physics.Arcade.Sprite} player 
 * @param {Phaser.Physics.Arcade.Sprite} box
 */
handlePlayerBoxCollide(player, box)
{
	if (this.activeBox)
	{
		return
	}

	this.activeBox = box

	this.activeBox.setFrame(9)
}

This method simply sets the passed in box–which represents the box the player collided with–and sets it as the activeBox. Then it changes the frame to 9 so that it is displayed as a green box instead of the gray one.

Notice that activeBox is a class property. Remember to add it as a property declaration like this:

1
2
3
4
5
6
7
export default class Game extends Phaser.Scene
{
	/** @type {Phaser.Physics.Arcade.Sprite} */
	activeBox

	// other code...
}

Run into a box and you'll see it turn green. ✅

Now, we need to change it back to being gray when the player is too far and can no longer open it.

When a Box is No Longer Active

We will consider an active box to no longer be active when the distance between the player and the activeBox is greater than 64px.

Let's start by adding an updateActiveBox() method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
updateActiveBox()
{
	if (!this.activeBox)
	{
		return
	}

	// get the distance here 👇
	const distance = Phaser.Math.Distance.Between(
		this.player.x, this.player.y,
		this.activeBox.x, this.activeBox.y
	)

	if (distance < 64) // 👈 do nothing if still near
	{
		return
	}

	// return to using frame 10 when too far
	this.activeBox.setFrame(10)

	// and make activeBox undefined
	this.activeBox = undefined
}

First, we early exit and do nothing if this.activeBox is not set. This happens on lines 3 - 6.

Then we get the distance between the player and the currently active box. We'll do nothing if that distance is less than 64.

Once the distance is 64 or greater, we set the activeBox back to using frame 10 and set this.activeBox to undefined to designate that no box is currently active.

Give this a try by adding a call to updateActiveBox() in the update() method after the player movement code.

1
2
3
4
5
6
7
8
update()
{
	// player movement code...

	this.updateActiveBox()

	// other code...
}

You should now see that touching a box sets it to be active and then walking away sets it to not active.

What's in a Box?

We'll need to have something in each box before we open them or there will be nothing to show!

There are many ways to do this as well and we will use the simplest approach of creating a 2D array at the top of Game.js that defines what is inside each box.

Create a level variable after the module imports like this:

1
2
3
4
5
6
7
8
import Phaser from 'phaser' // 👈 module imports

// 👇 level variable as 2D array of numbers
const level = [
	[1, 0, 3],
	[2, 4, 1],
	[3, 4, 2]
]

Each unique number in this 2D array represents a different animal that will come out of the box.

You can use strings if that is easier to understand but it is often easier to visually align a 2D array with numbers because words can vary widely in length.

For numbers to work, we just need to agree that 0 is a Bear, 1 is a Chicken, 2 is a Duck, 3 is a Parrot, and 4 is a Penguin.

Next, let's change how we create the boxes to use the level array instead of a hard-coded row and column size.

In the createBoxes() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
createBoxes()
{
	// other code...

	// notice that we are using 👇 instead of just 3
	for (let row = 0; row < level.length; ++row)
	{
		// same thing here as well 👇
		for (let col = 0; col < level[row].length; ++col)
		{
			/** @type {Phaser.Physics.Arcade.Sprite} */
			const box = this.boxGroup.get(width * xPer, y, 'sokoban', 10)
			box.setSize(64, 32)
				.setOffset(0, 32)
				.setData('itemType', level[row][col]) // 👈 notice

			xPer += 0.25
		}

		xPer = 0.25
		y += 150
	}
}

We replaced the hard-coded 3 in the for loops to use level.length and level[row].length respectively.

Then we call setData() on each created box and give the 'itemType' key the value from level[row][col]. This value is the number representing which animal should be revealed by the box.

The 'itemType' key is just something that we decided to use. You can use any string with setData() to store arbitrary data with a GameObject.

Preload Animal Textures

We'll need to preload each texture for the 5 different animals that can be revealed by a box.

Download the Animal Pack Redux by Kenney here if you haven't already.

You'll need these images:

  • bear.png
  • chicken.png
  • duck.png
  • parrot.png
  • penguin.png

Pick out the variants that you like best. There's round, square, outlined, and others.

Then add them to the Preloader Scene like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
preload()
{
	// previous code...

	this.load.image('bear', 'textures/bear.png')
	this.load.image('chicken', 'textures/chicken.png')
	this.load.image('duck', 'textures/duck.png')
	this.load.image('parrot', 'textures/parrot.png')
	this.load.image('penguin', 'textures/penguin.png')
}

Opening a Box

Now that each box knows what it can reveal, we can add logic to open a box.

Start by adding this openBox(box) method:

 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
/**
 * 
 * @param {Phaser.Physics.Arcade.Sprite} box 
 */
openBox(box)
{
	if (!box)
	{
		return
	}

	const itemType = box.getData('itemType')
		
	/** @type {Phaser.GameObjects.Sprite} */
	let item

	switch (itemType)
	{
		case 0:
			item = this.add.sprite(box.x, box.y, 'bear')
			break

		case 1:
			item = this.add.sprite(box.x, box.y, 'chicken')
			break

		case 2:
			item = this.add.sprite(box.x, box.y, 'duck')
			break

		case 3:
			item = this.add.sprite(box.x, box.y, 'parrot')
			break

		case 4:
			item = this.add.sprite(box.x, box.y, 'penguin')
			break
	}

	// TODO: do something with item
}

This code is fairly straight-forward. We get the value stored in the 'itemType' key of each box. Then we use a switch statement to create the appropriate Sprite for the given type.

Next, we want to specify that the box is opened and then tween the item from a scale and alpha of 0 to 1 so that it appears with a bit a fanfare.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
openBox(box)
{
	// previous code...

	box.setData('opened', true)

	item.scale = 0
	item.alpha = 0

	this.tweens.add({
		targets: item,
		y: '-=50',
		alpha: 1,
		scale: 1,
		duration: 500
	})
}

We need to call this method to see it in action so let's make pressing the space key call openBox().

Add this to the update() method right above the this.updateActiveBox() line:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
update()
{
	// player movement code...

	const spaceJustPressed = Phaser.Input.Keyboard.JustUp(this.cursors.space)
	if (spaceJustPressed && this.activeBox)
	{
		this.openBox(this.activeBox)

		// reset box after opened
		this.activeBox.setFrame(10)
		this.activeBox = undefined
	}

	this.updateActiveBox()

	// other code...
}

Now when you make a box active and press space, an item will animate-in and become revealed. 🎉

Quick Code Clean Up

We'll wrap up with some quick code clean-up. You may have noticed that instead of creating a new Sprite each time a box is opened we can reuse Sprites by using a Group.

Recall that we used a Group for boxes to make it easier to add colliders but reusing GameObjects was another benefit.

We can make the change by creating a new class property called itemsGroup like this:

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

	/** @type {Phaser.GameObjects.Group} */
	itemsGroup

	create()
	{
		// other code...

		this.itemsGroup = this.add.group()
	}
}

Next, update the switch statement handling itemType to look like this:

 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
openBox(box)
{
	// other code...

	switch (itemType)
	{
		case 0:
			item = this.itemsGroup.get(box.x, box.y)
			item.setTexture('bear')
			break

		case 1:
			item = this.itemsGroup.get(box.x, box.y)
			item.setTexture('chicken')
			break

		case 2:
			item = this.itemsGroup.get(box.x, box.y)
			item.setTexture('duck')
			break

		case 3:
			item = this.itemsGroup.get(box.x, box.y)
			item.setTexture('parrot')
			break

		case 4:
			item = this.itemsGroup.get(box.x, box.y)
			item.setTexture('penguin')
			break
	}
}

Take note that we are using item.setTexture() instead of passing in the texture key to this.itemsGroup.get(). We do this because items are being reused and this ensures the actual texture we want will be used and not the previously used texture.

The second item to improve is the update() method as it looks a bit cluttered now.

Let's simplify it by moving the player movement logic into a separate method so that update() is easier to read.

 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
updatePlayer()
{
	const speed = 200

	if (this.cursors.left.isDown)
	{
		this.player.setVelocity(-speed, 0)
		this.player.play('left-walk', true)
	}
	else if (this.cursors.right.isDown)
	{
		this.player.setVelocity(speed, 0)
		this.player.play('right-walk', true)
	}
	else if (this.cursors.up.isDown)
	{
		this.player.setVelocity(0, -speed)
		this.player.play('up-walk', true)
	}
	else if (this.cursors.down.isDown)
	{
		this.player.setVelocity(0, speed)
		this.player.play('down-walk', true)
	}
	else
	{
		this.player.setVelocity(0, 0)
		const key = this.player.anims.currentAnim.key
		const parts = key.split('-')
		const direction = parts[0]
		this.player.play(`${direction}-idle`)
	}

	const spaceJustPressed = Phaser.Input.Keyboard.JustUp(this.cursors.space)
	if (spaceJustPressed && this.activeBox)
	{
		this.openBox(this.activeBox)

		this.activeBox.setFrame(10)
		this.activeBox = undefined
	}
}

Then change update() to look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
update()
{
	this.updatePlayer()

	this.updateActiveBox()

	this.children.each(c => {
		/** @type {Phaser.Physics.Arcade.Sprite} */
		// @ts-ignore
		const child = c

		child.setDepth(child.y)
	})
}

Next Steps

Items are being revealed but we don't have any logic to check if a match was made.

In part 4, we will check for matches and then handle what happens when there is a match and when there is no match.

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 memory match memory guide modern javascript

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