Memory Match in Modern Javascript with Phaser 3 - Part 6

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 the previous part, we handled selecting the bear which stuns the player and then added logic to check when the level is completed.

In this final part, we will look at adding a countdown timer that will cause the player to lose when time runs out.

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

Controlling a Countdown

We can add some challenge to Memory Match by adding a timer that shows how much time the player has left to find all the matches.

Let's start by creating a CountdownController class to hold all the logic for counting down and updating a Text display to show how much time is left.

Create a new CountdonwController.js file in the scenes folder with this barebones set up:

 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
export default class CountdownController
{
	/** @type {Phaser.Scene} */
	scene

	/** @type {Phaser.GameObjects.Text} */
	label

	/**
	 * 
	 * @param {Phaser.Scene} scene 
	 * @param {Phaser.GameObjects.Text} label 
	 */
	constructor(scene, label)
	{
		this.scene = scene
		this.label = label
	}

	/**
	 * @param {() => void} callback
	 * @param {number} duration 
	 */
	start(callback, duration = 45000)
	{
	}

	stop()
	{
	}

	update()
	{
	}
}

This class takes in a Scene reference and Text reference. We will pass these in when we created a new CountdownController in the Game Scene.

Then we have 3 unimplemented methods: start(), stop(), and update().

The start() method will begin the countdown and then call a callback function when it is finished. An optional duration can be passed in to change how long the countdown should be.

Then the stop() method will stop the countdown and the update() method will handle calculating the current time remaining and updating the passed in Text instance stored in this.label.

That's the structure of this class. Notice that it does not extend from a Phaser class or have a visual component. It is a plain JavaScript class that controls a visual Text object. Hence the name Countdown Controller.

Creating a Timer

With the CountdownController skeleton ready, we can simply implement each method to perform their specific tasks.

Let's start with the start() method. It will create a TimerEvent and then invoke a given callback when it is finished.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
start(callback, duration = 45000)
{
	// 1️⃣ stop in case one is already running
	this.stop()

	// 2️⃣ create a TimerEvent with given duration
	this.timerEvent = this.scene.time.addEvent({
		delay: duration,
		callback: () => {
			this.label.text = '0' // 👈 set to 0 since time is up

			this.stop()
			
			// 3️⃣ execute callback when finished
			if (callback)
			{
				callback()
			}
		}
	})
}

First, we call stop() just in case there is already another TimerEvent running. This CountdownController assumes just 1 countdown at a time.

Then we create a TimerEvent using this.scene.time.addEvent() by passing in a configuration object with a duration or delay and a callback.

Notice that timerEvent is a class property so we should add that to the class like this:

1
2
3
4
5
6
7
export default class CountdownController
{
	/** @type {Phaser.Time.TimerEvent} */
	timerEvent

	// other code...
}

Lastly, once this timer is finished we execute the callback if one was given.

Next, let's see what the stop() method looks like:

1
2
3
4
5
6
7
8
stop()
{
	if (this.timerEvent)
	{
		this.timerEvent.destroy()
		this.timerEvent = undefined
	}
}

This very simply checks if this.timerEvent exists and then destroys it if it does. Nothing will happen if this.timerEvent is already undefined.

Last method left is the update() method. This is where the countdown time math happens.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
update()
{
	if (!this.timerEvent || this.duration <= 0)
	{
		return
	}

	// 1️⃣ get the elapsed time
	const elapsed = this.timerEvent.getElapsed()

	// 2️⃣ subtract from total duration
	const remaining = this.duration - elapsed

	// 3️⃣ convert from milliseconds to seconds
	const seconds = remaining / 1000

	// 4️⃣ change label to show new value
	this.label.text = seconds.toFixed(2)
}

We early exit if there is no timerEvent or if the duration is 0 or less.

Then we get the elapsed time from the timerEvent and subtract it from the total duration to get time remaining. Time remaining is in milliseconds so we divide by 1000 to get time in seconds.

With time in seconds, we set this.label to show the time up to 2 decimal places with toFixed(2).

Finally, notice that we are using this.duration which is a class property so add that to the class and set it to the duration argument given to start() like this:

1
2
3
4
5
6
7
8
start(callback, duration = 45000)
{
	this.stop()
		
	this.duration = duration // 👈

	// other code...
}

And that is the complete CountdownController class!

Using CountdownController from Game

Using the CountdownController means we need to create an instance by passing in the Scene and a Text object. Then we call start() to begin the countdown and make sure that update() is called from the Scene's update() method.

Here's how we create a CountdownController in 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
import Phaser from 'phaser'

// import the CountdownController class 👇
import CountdownController from './CountdownController'

export default class Game extends Phaser.Scene
{
	// other properties...

	/** @type {CountdownController} */
	countdown // 👈 add as class property

	create()
	{
		// previous code...

		// create a Text object 👇
		const timerLabel = this.add.text(width * 0.5, 50, '45', { fontSize: 48 })
			.setOrigin(0.5)

		// 👇 create a new instance
		this.countdown = new CountdownController(this, timerLabel)
	}

	// other code...
}

First, we import the CountdownController class so that we can use it in the Game Scene.

Then we create a class property on line 11 and set it to a new instance on line 22 with a newly added timerLabel.

Next, we need to make sure the countdown's update() method is being called:

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

	this.countdown.update()
}

Make sure to also call stop() when all matches have been found in the checkForMatch() method:

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

	this.time.delayedCall(1000, () => {
		first.box.setFrame(8)
		second.box.setFrame(8)

		if (this.matchesCount >= 4)
		{
			// game won
			this.countdown.stop() // 👈

			// other code...
		}
	})
}

Now, we need to call start() and pass in a callback method when the countdown is finished.

Add this after we create a new instance of CountdownController in the create() method:

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

	this.countdown = new CountdownController(this, timerLabel)

	// call start() here 👇
	this.countdown.start(this.handleCountdownFinished.bind(this))
}

We are passing in the handleCountdownFinished() method so we'll have to create it.

Notice that we are using .bind(this). This will return a new function based on the handleCountdownFinished() method with its context set as this whenever it is invoked.

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

Here's what handleCountdownFinished() looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
handleCountdownFinished()
{
	// disable player like we've done before
	this.player.active = false
	this.player.setVelocity(0, 0)

	// create a You Lose! message
	const { width, height } = this.scale
	this.add.text(width * 0.5, height * 0.5, 'You Lose!', { fontSize: 48 })
		.setOrigin(0.5)
}

This should look fairly similar to the logic when a level is completed except we show a “You Lose!” message instead.

You should see something like this when the countdown expires and not all matches have been found 👇

You Lose! - Countdown Finished

Memory Match Extras

We hope you've enjoyed making Memory Match! There's a bunch more you can add to this game to make it your own.

Some core improvements you can add are:

  • music
  • sound effects
  • restart after losing
  • better fonts (Web or Bitmap)
  • preload animation

That's why we created the Memory Match Extras video course that covers all these things plus gamepad support 🎮, code organization & clean-up 💻, using nineslice for dialogs 🔪, and more!

Learn more in this short preview 👇



There are even bonuses to help you debug code and navigate the official Phaser documentation better so that you can solve more of your own problems without hoping for a response on forums or Discord.

Check out the course page to learn more!

Next Steps

You can find the complete source code for the Memory Match game we created in this series here.

Use it as a way to compare and contrast against what you did. The Memory Match Extras course comes with an updated & improved source code plus a TypeScript version.

If you are not yet ready for a course then 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