Memory Match in Modern Javascript with Phaser 3 - Part 2

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

by on 8 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 1, we set up the project and created a character that can walk around using the keyboard arrow keys.

Now in Part 2, we will add boxes in a 3x3 grid for the player to interact with and use depth sorting to make sure everything is layered properly.

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

Creating a Box

Let's start by creating a single box using the frame at index 10 from the sokoban asset.

Add this to the create() method of the Game Scene:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
create()
{
	const { width, height } = this.scale

	this.player = this.physics.add.sprite(width * 0.5, height * 0.6, 'sokoban')
		.play('down-idle')

	// create the box 👇
	this.physics.add.sprite(width * 0.5, height * 0.5, 'sokoban', 10)
}

We are adding it as a Phaser.Physics.Arcade.Sprite because the player will want to collide with it later.

Your game should show a gray box above the player.

We will need 8 more boxes and add a collider with the player for each. While we can do something like this:

1
2
3
4
this.physics.add.collider(this.player, box1)
this.physics.add.collider(this.player, box2)
this.physics.add.collider(this.player, box2)
// ...

It would take much fewer lines and be harder to mess up if we used a Group instead.

Creating Boxes with a Group

You've likely seen and used Groups before. They can hold a bunch of Sprites that can then be reused.

Our boxes will have static Physics bodies as they don't need to move. They will stay where they are once we place them.

We can create a boxGroup like this in the create() method of the Game Scene:

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

	// remove this previous line 🗑
	this.physics.add.sprite(width * 0.5, height * 0.5, 'sokoban', 10)

	// then add this 👇
	const boxGroup = this.physics.add.staticGroup()
}

Now, we can use the boxGroup to create the 9 boxes we need using 2 for loops.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
create()
{
	const { width, height } = this.scale

	// previous code...

	const boxGroup = this.physics.add.staticGroup()

	let xPer = 0.25
	let y = 150
	for (let row = 0; row < 3; ++row)
	{
		for (let col = 0; col < 3; ++col)
		{
			boxGroup.get(width * xPer, y, 'sokoban', 10)

			xPer += 0.25
		}

		xPer = 0.25
		y += 150
	}
}

We use a nested for loop to create a 3x3 grid of boxes at specific positions spaced at 25%, 50%, and 75% of screen width horizontally and 150px vertically.

The call to boxGroup.get() creates a new box and adds it to the Scene at the given x and y positions.

Notice that we are using a percentage to determine the x position instead of a specific number like we do with the y position. We add 0.25 to xPer each time a new box is created on a row and then reset it back to 0.25 for each new row.

You'll get something that looks like this:

Man with Boxes

Let's Depth Sort!

Move the player around and you'll notice that he is always behind the boxes. He doesn't collide with the boxes but we'll fix that later.

First, let's add some depth sorting so that the player will sort behind a box when it looks like he is behind and in front when he looks like he is in front.

Phaser will do the depth sorting for us if we specify a value using setDepth(). All we need to do is set that value to the y position value of the Sprite.

Add this to the update() method in the Game Scene:

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

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

		child.setDepth(child.y)
	})
}

The call to setDepth() on line 10 that passes in child.y is straight forward.

The lines above might be slightly confusing. We are using JSDoc annotations to help VS Code provide us with more intelligent code completion.

Then the // @ts-ignore line is to tell VS Code that we don't care about the TypeScript-aided type checking error even though it is valid because we will mostly be working with Phaser.Physics.Arcade.Sprite objects.

This will mean that we might add a GameObject to the Scene that isn't a Phaser.Physics.Arcade.Sprite and doesn't have the setDepth() method. An error will occur when that happens.

But we will leave it this way for the sake of simplicity. Another approach is to have a list of items that should be depth sorted and only call setDepth() on those items. Feel free to implement it that way!

Now the player should properly appear behind boxes when he is behind them and in front when he is in front of them.

Collisions Between Player and Boxes

Next, we can add a collider between the player and the boxes so that the player can't just run through them.

Add this to the create() method in the Game Scene:

1
2
3
4
5
6
create()
{
	// previous code...

	this.physics.add.collider(this.player, boxGroup)
}

The player won't be able to run through boxes anymore but we also lose that feeling of depth where the player partly overlaps the box when he is in front of it and the box partly overlaps the player when he is behind it.

We can bring that back by adjusting the size of the collision boxes. Let's turn on physics debug in main.js to better see what is going on.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const config = {
	// ...
	physics: {
		default: 'arcade',
		arcade: {
			debug: true, // 👈
			gravity: { y: 0 }
		}
	}
}

You'll see some colored rectangles around the player and the boxes. They represent the collision box of each Physics body.

To achieve the effect we want, make the player's collision box cover just the lower half of his body.

Add this to where we create the player:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
create()
{
	const { width, height } = this.scale

	this.player = this.physics.add.sprite(width * 0.5, height * 0.6, 'sokoban')
		.setSize(40, 16)	// 👈 this
		.setOffset(12, 38)	// 👈 and this
		.play('down-idle')
	
	// other code...
}

We use setSize() to change the size of the collision box. Then we use setOffset() to properly place where that box should be.

You should see that the player's collision box is now much smaller.

Next, let's make a similar adjustment to the boxes:

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

	for (let row = 0; row < 3; ++row)
	{
		for (let col = 0; col < 3; ++col)
		{
			/** @type {Phaser.Physics.Arcade.Sprite} */
			const box = boxGroup.get(width * xPer, y, 'sokoban', 10)

			// like this 👇 similar to before
			box.setSize(64, 32)
				.setOffset(0, 32)

			xPer += 0.25
		}

		xPer = 0.25
		y += 150
	}
}

We use the same setSize() and setOffset() methods but with different values. A box variable is created to store a reference to each created box and JSDoc is, again, used to help with code completion.

The player and boxes will now collide while retaining the depth effect. 🎉

Code Clean Up

We'll wrap up this part with some minor code clean-up.

The box creation logic is adding a lot of specific code to the create(). It would be better to keep the code in create() high level so that you can easily see what is being created.

We can do that by moving the box creation logic to a separate method like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
createBoxes()
{
	const width = this.scale.width

	let xPer = 0.25
	let y = 150
	for (let row = 0; row < 3; ++row)
	{
		for (let col = 0; col < 3; ++col)
		{
			/** @type {Phaser.Physics.Arcade.Sprite} */
			const box = this.boxGroup.get(width * xPer, y, 'sokoban', 10)
			box.setSize(64, 32)
				.setOffset(0, 32)

			xPer += 0.25
		}

		xPer = 0.25
		y += 150
	}
}

The added createdBoxes() method contains the logic previously in create() plus an additional width variable that consts the screen width.

Notice that we are using this.boxGroup on line 12. We left the creation of the boxGroup in create() so we made a class property to allow access to it from createBoxes().

Add an optional property declaration like this:

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

	// other code...
}

Then the create() method was updated to look like this:

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

	this.boxGroup = this.physics.add.staticGroup()

	this.createBoxes()

	this.physics.add.collider(this.player, this.boxGroup)
}

Next Steps

We now have a player and a world to move around in.

In the next part, we will look at changing a box's color when it can be selected and then show the item in the box when it gets selected.

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