Memory Match in Modern Javascript with Phaser 3 - Part 4

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

by on 7 minute read


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 3, we added logic to determine when a box can be opened to reveal the item inside.

Now in part 4, we will show you how to check for a match and handle the two possible outcomes: match or no match.

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

Fixing Depth Sort

You've likely noticed that the item revealed by a box depth sorts behind the box and the player depending on where he is.

This is because we are calling setDepth() on all children in update(). We could avoid this by adding the children that we want to sort to a list and only calling setDepth() on those GameObjects.

But, we will show you another way using setData() to flag certain GameObjects.

Add this to the openBox() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * 
 * @param {Phaser.Physics.Arcade.Sprite} box 
 */
openBox(box)
{
	// previous code...

	box.setData('opened', true)

	// add these two lines 👇
	item.setData('sorted', true)
	item.setDepth(2000)

	// tween code...
}

We give the item a flag with the key 'sorted' and then set its depth to a relatively high number of 2000 so that it will be on top of everything else.

The 'sorted' flag will then be used in the update() method like this:

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

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

		// 👇 add this check and early exit
		if (child.getData('sorted'))
		{
			return
		}

		child.setDepth(child.y)
	})
}

Now, revealed items will always be above everything else in the game.

Disallow Open Boxes from Opening Again

Since we fixed one bug, let's fix another! You'll notice that you can open a box that has already been opened.

That doesn't seem right. We can fix that quite easily because we've already added an 'opened' flag to each box that is opened.

Add this to handlePlayerBoxCollide(player, box) in the Game Scene:

 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)
{
	const opened = box.getData('opened')
	
	if (opened)
	{
		return
	}

	// other code...
}

We simply check if a box is opened and then early exit if it is. Easy peasy.

Checking for a Match

A match can happen after two newly opened boxes have revealed their items.

We can check for this after the tween that reveals the item. But first, we need to store the last two opened boxes and revealed items in an Array to only check the newly opened boxes and not all open boxes.

Add a selectedBoxes class property like this:

1
2
3
4
5
6
7
export default class Game extends Phaser.Scene
{
	/** @type {{ box: Phaser.Physics.Arcade.Sprite, item: Phaser.GameObjects.Sprite }[]} */
	selectedBoxes = []

	// other properties...
}

Then we will push the opened box and revealed item to this list in openBox() before we create the tween:

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

	// add this line 👇
	this.selectedBoxes.push({ box, item })

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

Recall that we defined the selectedBoxes Array as containing an object with a box and item property. That's why we are pushing an object with a box and item property on line 6.

We are using a modern JavaScript shorthand to create the object by automatically using the variable name as the key. You can also write it like this:

1
2
3
4
this.selectedBoxes.push({
	box: box,
	item: item
})

This modern syntax removes the need to type the same thing twice.

Next, we want to check if there are 2 open items after an item has been revealed. We can do that by using the onComplete callback provided by a tween.

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

	this.tweens.add({
		targets: item,
		y: '-=50',
		alpha: 1,
		scale: 1,
		duration: 500,
		onComplete: () => {
			// check that we have 2️⃣ items recently opened
			if (this.selectedBoxes.length < 2)
			{
				return
			}

			// we have to create this method
			this.checkForMatch()
		}
	})
}

If we do not have at least 2 items opened then we early exit and wait until a second box is opened.

Once 2 boxes have been opened, we will call the checkForMatch() method that looks 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
33
34
checkForMatch()
{
	// pop from end to get second and first opened boxes
	const second = this.selectedBoxes.pop()
	const first = this.selectedBoxes.pop()

	// no match if the revealed items are not the same texture
	if (first.item.texture !== second.item.texture)
	{
		// hide the items and set box to no longer opened
		this.tweens.add({
			targets: [first.item, second.item],
			y: '+=50',
			alpha: 0,
			scale: 0,
			duration: 300,
			delay: 1000,
			onComplete: () => {
				this.itemsGroup.killAndHide(first.item)
				this.itemsGroup.killAndHide(second.item)

				first.box.setData('opened', false)
				second.box.setData('opened', false)
			}
		})
		return
	}

	// we have a match! wait 1 second then set box to frame 8
	this.time.delayedCall(1000, () => {
		first.box.setFrame(8)
		second.box.setFrame(8)
	})
}

We start by calling pop() twice to get the selected boxes in the reverse order they were added. This means the first one popped is the second box opened.

You could compare against index 0 and 1 but we feel using a human-readable variable name is easier to think about. 🤗

Then we determine if there is no match by comparing the texture property of the revealed items. This will compare the texture key that each item is using such as: 'duck', 'parrot', etc.

When there is no match, we tween both items away by doing the reverse of what we did to reveal them. We also call killAndHide() on each item to properly set them to inactive so that they can be reused later. Both boxes also set their 'opened' flags to false so the player can try them again.

If there is a match, we wait 1 second and then set each box to use the frame at index 8 which is a blue box.

Try it out and you'll see matched items staying on-screen while unmatched items go back into their boxes. 👏

Reset Reused Items

Let's wrap up by making sure that reused items from the itemsGroup are set to active and visible when they are to be revealed.

Because we used killAndHide() to properly set each unmatched item to be inactive, we also have to make sure to reactivate them when we call this.itemsGroup.get() because we can be getting a reused Sprite.

All we need to do is add these two lines to the openBox() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 
 * @param {Phaser.Physics.Arcade.Sprite} box 
 */
openBox(box)
{
	// item selection code...

	box.setData('opened', true)

	item.setData('sorted', true)
	item.setDepth(2000)

	// add these 2️⃣ lines
	item.setActive(true) // 👈
	item.setVisible(true) // 👈

	item.scale = 0
	item.alpha = 0

	// tween & other code...
}

Next Steps

We fixed a couple of bugs and implemented logic to handle matches. But you'll notice that we haven't handled the bear or when all matches have been made.

That's what we'll do in part 5! The bear will act as Bowser and stun the player for 1 second.

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