Memory Match in Modern Javascript with Phaser 3 - Part 1

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

by on 11 minute read


Have you gone through our Making Your First phaser 3 Game in Modern JavaScript series? And maybe the Infinite Jumper in Phaser 3 with Modern JavaScript book as well?

That means you've got the basics down so let's try making something a little bit more complicated!

We nominate Memory Match. It is 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.

Here's a video of it from Mario Party on the Nintendo 64.

In this series, we will go over creating a 2D version using Phaser 3 and 2 assets from Kenney: Sokoban assets and Animal Pack Redux assets.

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

Getting Started

We'll be using the phaser3-parcel-template to quickly set up our project. You'll need Nodejs and NPM installed to use it.

See this article for detailed instructions on how to set it up.

Make sure you run these commands to ensure that the project is working properly:

1
2
3
npm install

npm run start

You'll get a basic Phaser 3 logo bouncing around the screen when you go to http://localhost:8080.

The template will start you off with a HellowWorldScene in the Scenes folder so let's start by deleting it. We'll make a Game Scene instead.

Create a new Game.js file in the Scenes folder with this barebones Scene code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import Phaser from 'phaser'

export default class Game extends Phaser.Scene
{
	constructor()
	{
		super('game')
	}

	create()
	{
	}
}

Then go to main.js and replace the references to HelloWorldScene with Game 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
import Phaser from 'phaser'

// delete this 👇
// import HelloWorldScene from './scenes/HelloWorldScene'

// replace with:
import Game from './scenes/Game'

const config = {
	type: Phaser.AUTO,
	width: 800,
	height: 600,
	physics: {
		default: 'arcade',
		arcade: {
			gravity: { y: 200 }
		}
	},
	// change this 👇
	// scene: [HelloWorldScene]

	// to:
	scene: [Game]
}

export default new Phaser.Game(config)

The result will be a black screen. 🎉

You can try stopping the local web server and running it again with npm run start if you see any strange errors.

Adding Assets

First, download the Sokoban assets from Kenney here.

Then, create a public folder in your project at the same level as your src folder.

    .
    ├── node_modules
    ├── public 👈
    ├── src
    ├── package.json

Inside the public folder, create a textures folder. Look for the sokoban_tilesheet.png from the Sokoban assets and copy it into the textures folder.

    .
    ├── node_modules
    ├── public
    │   ├── textures
	│   │   ├── sokoban_tilesheet.png
    ├── src
    ├── package.json

Now that we have the assets in our project, we can preload and then use them.

Preloading Assets

Let's start by creating a Preloader Scene. We will preload all necessary assets here before the game starts.

Create a Preloader.js file in the Scenes folder like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import Phaser from 'phaser'

export default class Preloader extends Phaser.Scene
{
	constructor()
	{
		super('preloader')
	}

	preload()
	{

	}

	create()
	{

	}
}

Next, we can preload the sokoban_tilesheet.png as a spritesheet in Phaser:

1
2
3
4
5
6
preload()
{
	this.load.spritesheet('sokoban', 'textures/sokoban_tilesheet.png', {
		frameWidth: 64
	})
}

Spritesheet? Tilesheet?

In Phaser 3, a spritesheet is an atlas where each frame is the same size and can be referenced by their frame index. Kenney's Sokoban asset has another file called sokoban_spritesheet.png but each frame in that atlas is not the same size.

This difference is just a matter of opinion on what the term spritesheet means. All that matters is that you want to use sokoban_tilesheet.png and load it into Phaser as a spritesheet. 😎


Next, we can add code to switch Scenes in the create() method so that the Game Scene is started after all assets are preloaded:

1
2
3
4
create()
{
	this.scene.start('game')
}

The last thing we need to do is register the Preloader Scene with the game in main.js like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import Phaser from 'phaser'

import Game from './scenes/Game'

// import Preloader 👇
import Preloader from './scenes/Preloader'

const config = {
	// ...
	// add it 👇 to the beginning of the scene list
	scene: [Preloader, Game]
}

export default new Phaser.Game(config)

Add an Image from a Spritesheet

Let's make sure everything is loading correctly by adding an image to the Game Scene from the loaded spritesheet.

Each frame in the spritesheet can be referenced by their index where 0 is the 64 x 64 square at the top left of the spritesheet. Then index 1 is the next 64 x 64 square to the right. Index 2 is the square to the right of that and so on.

We will use frame 52 to show a character facing down or towards the bottom of the screen.

In the create() method of the Game Scene:

1
2
3
4
create()
{
	this.add.image(400, 300, 'sokoban', 52)
}

This should result in a little man standing in the middle of the screen.

Create Animations

Our character will need to move around so let's make some animations for walking down, up, left, and right as well as being idle when facing down, up, left, and right.

This means we'll need 8 animations. 4 for walking in each direction and 4 for standing idle in each direction.

Let's start by creating the simpler idle animations in the create() method of the Preloader Scene:

 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()
{
	this.anims.create({
		key: 'down-idle',
		frames: [{ key: 'sokoban', frame: 52 }]
	})

	this.anims.create({
		key: 'up-idle',
		frames: [{ key: 'sokoban', frame: 55 }]
	})

	this.anims.create({
		key: 'left-idle',
		frames: [{ key: 'sokoban', frame: 81 }]
	})

	this.anims.create({
		key: 'right-idle',
		frames: [{ key: 'sokoban', frame: 78 }]
	})

	// start game scene
	this.scene.start('game')
}

These are all animations that are 1 frame long. Notice that we only define 1 frame in the frames array for each created animation.

Each is also given a key value with a consistent naming convention of “FACING_DIRECTION-idle”. We will use the same convention for the walk animations. You'll see how this is useful when we add keyboard input. 🤓

Now for the walk animations:

 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
create()
{
	// previous animations

	this.anims.create({
		key: 'down-walk',
		frames: this.anims.generateFrameNumbers('sokoban', { start: 52, end: 54 }),
		frameRate: 10,
		repeat: -1
	})

	this.anims.create({
		key: 'up-walk',
		frames: this.anims.generateFrameNumbers('sokoban', { start: 55, end: 57 }),
		frameRate: 10,
		repeat: -1
	})

	this.anims.create({
		key: 'left-walk',
		frames: this.anims.generateFrameNumbers('sokoban', { start: 81, end: 83 }),
		frameRate: 10,
		repeat: -1
	})

	this.anims.create({
		key: 'right-walk',
		frames: this.anims.generateFrameNumbers('sokoban', { start: 78, end: 80 }),
		frameRate: 10,
		repeat: -1
	})

	// start game scene
	this.scene.start('game')
}

Notice that these are multi-frame animations and we use Phaser's generateFrameNumbers() helper method to automatically generate the frames from a start value to an end value.

Then we set repeat to -1 so that it loops forever and give it a frameRate of 10. Feel free to adjust this to your liking.

Playing an Animation

Let's make sure everything is working by testing the down-walk animation in the Game Scene:

1
2
3
4
5
6
7
8
9
create()
{
	// remove this 👇
	// this.add.image(400, 300, 'sokoban', 52)

	// add this 👇
	this.add.sprite(400, 300, 'sokoban', 52)
		.play('down-walk')	// 👈 play the animation
}

You should see a little man facing down while running in place. 👌

Add Player Movement

We will wrap up part 1 with some player movement when using the keyboard arrow keys.

Let's start by creating an instance of Phaser's CursorKeys that will quickly give us access to the arrow keys and the space key.

Create an init() method in the Game Scene like this:

1
2
3
4
init()
{
	this.cursors = this.input.keyboard.createCursorKeys()	
}

Optionally, you can also add a property declaration as a class field like this:

1
2
3
4
5
6
7
export default class Game extends Phaser.Scene
{
	/** @type {Phaser.Types.Input.Keyboard.CursorKeys} */
	cursors

	// other code...
}

Class fields make it easier to understand the code at first glance by clearly specifying what properties a class has. You don't need to do this but we recommend it.

Next, let's add a player property and create a Phaser.Physics.Arcade.Sprite to represent our player:

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

	/** @type {Phaser.Physics.Arcade.Sprite} */
	player

	// other code...

	create()
	{
		const { width, height } = this.scale

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

Destructuring

We are using some new syntax on line 12 called Destructuring Assignment. This specific operation is known as Object Destructuring where we take the width and height properties from this.scale and create 2 variables of the same name from it.

It is a shortcut that is the same as this:

1
2
const width = this.scale.width
const height = this.scale.height

You'll see that the player gets created and then falls into the abyss.

This is from the gravity of the Arcade Physics engine. We don't need gravity in this game so let's turn it off in main.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const config = {
	// ...
	physics: {
		default: 'arcade',
		arcade: {
			gravity: { y: 0 } // 👈 change to 0
		}
	},
	scene: [Preloader, Game]
}

Next, we can handle movement in the update() method. Our character can move up, down, left, and right one at a time. That means he won't be able to move diagonally.

We'll enforce this using if/else and setVelocity() so that only 1 direction will be handled at any given time. Then we will consider the player idle if no arrow keys are being pressed.

Add this update() method to the Game Scene:

 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
update()
{
	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`)
	}
}

Notice that setVelocity() always sets the direction not being handled to 0 so that the player cannot move up and left or up and right, etc.

We use a speed value of 200 and make it negative or positive depending on the direction. Feel free to adjust this value for a faster or slower character.

Then we handle the idle case in the final else block where we set velocity to 0, 0 and create an animation key from the current animation key based on the convention we used in the Preloader Scene.

We need to do this because the player can be moving in any of the 4 directions before he stops and would need to become idle while facing that same direction.

Because all our animations keys adhere to a convention, we can just take the current animation key, split it by '-', and take the first element. This will give us the direction the player last faced.

Then we take that direction and create an idle animation key to have the player properly stop while facing the same direction he was moving in. 🤓

Your player should now respond to keyboard arrow keys and move around as expected.

Next Steps

That's it for part 1. We ended with a character that can walk around with different animations.

In the next part, we will add boxes for the player to interact with and use depth sorting to make sure everything layers properly.

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