Firebase Leaderboard with Rex Plugins in Phaser 3

Add a leaderboard without having to write server side code

by on 9 minute read


Are you looking to add more replayability to your game?

Leaderboards are a long-standing feature of single-player games that give players a reason to play multiple times.

Who doesn't want to make it onto the top 10 list of their favorite game?

In this article, we will look at using Firebase and Rex Plugins to implement a leaderboard with no server-side programming for a Phaser 3 game.

The examples are in TypeScript and build on top of the Rocket Mouse game from our free Infinite Runner in Phaser 3 with TypeScript book.

Learn to make an Infinite Runner in Phaser 3 with TypeScript!

Drop your email into the box below to get this free 90+ page book and join our newsletter.

Learn more about the book here.

The majority of the leaderboard code will be in a self-contained Scene but we will reference some code from the Rocket Mouse project.

Installing Rex Plugins

There are several ways to install phaser3-rex-plugins. We will take the npm approach and import just the LeaderBoard class.

First, run this command to install the plugin to your project:

npm install phaser3-rex-plugins

Set Up Firebase Project

Create a Firebase account and project that will hold the leaderboard.

Head over to https://firebase.google.com to do that.

If you already have a Firebase project then you don't need to create a new one.

Create a web app in your project from the Project Overview section. Firebase will ask for a project name and then give you some code to add to your game.

We will use the CDN approach in this article but you can also install Firebase as an npm package and import it.

Add these script tags to your index.html:

1
2
3
4
5
6
7
8
9
<html>
	<head>
		<!-- The core Firebase JS SDK is always required and must be listed first -->
		<script src="https://www.gstatic.com/firebasejs/7.15.1/firebase-app.js"></script>

		<!-- add Firestore -->
		<script defer src="https://www.gstatic.com/firebasejs/7.15.0/firebase-firestore.js"></script>
	</head>
</html>

The last thing we need is to initialize Firebase and we will do that in a Leaderboard Scene.

Create Leaderboard Scene

Let's start by creating an empty Scene with Firebase initialization at the top:

 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 SceneKeys from '../consts/SceneKeys'

// declare firebase to resolve TypeScript error
declare const firebase: any

var firebaseConfig = {
	apiKey: 'YOUR_KEY',
	authDomain: 'XXXXXXXXX',
	databaseURL: 'XXXXXXXXX',
	projectId: 'XXXXXXXXX',
	storageBucket: 'XXXXXXXXX',
	messagingSenderId: 'XXXXXXXXX',
	appId: 'XXXXXXXXX'
}

// Initialize Firebase
firebase.initializeApp(firebaseConfig)

export default class Leaderboard extends Phaser.Scene
{
	constructor()
	{
		super(SceneKeys.Leaderboard)
	}
}

Most of the code here is given to you by the Firebase dashboard when you create a new project. You can also find it in Settings > Your apps.

Make sure you include the Firebase config values for your project on lines 8 - 14.

We use declare const firebase: any to let TypeScript know that a global variable called firebase should exist. This is the case because we are loading Firebase from the CDN. You won't need this if you are importing Firebase as a module.

Then we have a basic Scene that is given a key defined in the SceneKeys enum. It is from the Rocket Mouse project and looks like this:

1
2
3
4
5
6
7
8
9
enum SceneKeys
{
	Preloader = 'preloader',
	Game = 'game',
	GameOver = 'game-over',
	Leaderboard = 'leaderboard'
}

export default SceneKeys

You can also just use a string literal.

Create Rex Firebase Leaderboard

Let's import the LeaderBoard class from phaser3-rex-plugins at the top of the Leaderboard Scene like this:

1
2
3
// other imports...

import { LeaderBoard } from 'phaser3-rex-plugins/plugins/firebase-components'

Next, we will create a LeaderBoard instance and store it in a class property in the init() hook:

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

export default class Leaderboard extends Phaser.Scene
{
	private leaderboard: any

	// constructor...

	init(data: { score: number })
	{
		this.leaderboard = new LeaderBoard({
			root: 'leaderboard'
		})

		// NOTE: each individual player should have a different ID and name
		this.leaderboard.setUser({
			userID: 'test-uid3',
			userName: 'Rocket Man'
		})
	}
}

Notice that we are using the type any for the leaderboard property on line 5. This is a simple way to work around the lack of official support for TypeScript.

You can also create an interface based on the plugin docs.

We are expecting a data argument to be given with a score property. It will be used later when we go to save a score.

Then on line 12, we use the key 'leaderboard' for the root property. This will be the name of Firestore collection that stores each entry.

Lastly, we are using placeholder values to set the user on lines 16 - 19. Each of your players should have a different userID and userName.

One way is to use Firebase Auth to create and manage user accounts. Another way is to simply create a UUID and store it in a browser cookie. We won't be covering user accounts in this article.

Regenerator Runtime?

You might run into an error that says regenerator-runtime is not defined. It is talking about this library.

That can be fixed by installing it to your project with this command:

npm install regenerator-runtime

Then import it in the Leaderboard Scene:

1
import 'regenerator-runtime'

Enable Firestore

You'll need to enable Firestore from the Firebase Dashboard before leaderboard data can be saved or retrieved.

Go to the Database section in Firebase and click the enable Firestore button.

You'll go through a wizard that asks about location and security. Select Test Mode for security. This will let you continue working on the leaderboard without having to figure out the proper security rules right now.

Save and Display Scores

We've done a lot of set-up so far and now we are ready to use the leaderboard.

Recall that we are expecting a score to be given in the init() hook. Let's store that value as a class property so that we can use it in the create() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export default class Leaderboard extends Phaser.Scene
{
	// other properties...
	private newScore = 0

	init(data: { score: number })
	{
		// previous code

		this.newScore = data.score
	}
}

Next, we can use this.newScore to save it to Firebase and then retrieve all available scores in create():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
async create()
{
	// post new score
	await this.leaderboard.post(this.newScore)

	// get first page of scores
	const scores = await this.leaderboard.loadFirstPage()

	console.dir(scores)
}

Notice we are using async to modify the create() method. This lets us use the async/await feature of TypeScript for more readable code when dealing with Promises.

The score provided to the init() hook is used on line 4 to save it to the database.

Then we retrieve the first page of scores and store it in the scores variable.

Next, we will display the top 5 scores instead of just logging them to the console:

 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
async create()
{
	// previous code...

	// create a translucent backing
	this.add.graphics({ fillStyle: { color: 0x000000, alpha: 0.7 } })
			.fillRoundedRect(96, 80, 600, 400, 15)

	// display first 5 scores
	const x = 128
	let y = 128
	const size = 5

	for (let i = 0; i < size; ++i)
	{
		const num = this.add.text(x, y, `${i + 1}.`, {
			fontSize: '32px',
			color: '#fff',
			backgroundColor: '#4A90E2',
			padding: { left: 10, right: 10, top: 10, bottom: 10 }
		})
		.setOrigin(0, 0.5)

		if (i < scores.length)
		{
			const scoreItem = scores[i]

			const name = this.add.text(num.x + num.width + 10, y, scoreItem.userName, {
				fontSize: '32px'
			})
			.setOrigin(0, 0.5)
	
			const nameWidth = 400
			this.add.text(name.x + nameWidth + 10, y, scoreItem.score.toString(), {
				fontSize: '32px'
			})
			.setOrigin(0, 0.5)	
		}

		y += 75
	}
}

There's a bit of code here but it is all fairly simple.

First, we create a translucent backing that the 5 scores will sit on top of.

Then, we use a for loop that displays a rank number on line 16. If there is a score for that rank we display the name and score.

Lastly, we increment y by 75 so that the next row begins under the previous row.

Quick tip: we used the Text Styler tool to design the rank number text.

It will look something like this:

Leaderboard example

Using the Leaderboard Scene

This section will differ somewhat depending on your project. We will be assuming the Rocket Mouse project mentioned earlier.

We want to run the Leaderboard Scene when the player has died. That can be found in the handleOverlapLaser method in the Game Scene.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private handleOverlapLaser(obj1: Phaser.GameObjects.GameObject, obj2: Phaser.GameObjects.GameObject)
{
	const mouse = obj2 as RocketMouse

	mouse.kill()

	// run the Leaderboard Scene if it is not running
	if (this.scene.isActive(SceneKeys.Leaderboard))
	{
		return
	}

	this.scene.run(SceneKeys.Leaderboard, { score: this.score })
}

This method is called when Rocket Mouse hits a laser obstacle and gets killed.

We run the Leaderboard Scene on line 13 and pass in the current score.

Notice that we check that the Leaderboard Scene is not already running before running it. An overlap can be triggered multiple times and we only want the Scene to run once.

In Rocket Mouse, the GameOver Scene is also run so we can adjust the y value of the message to be under the leaderboard like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// in the GameOver Scene
create()
{
	const { width, height } = this.scale

	this.add.text(width * 0.5, height * 0.9, 'Press SPACE to Play Again', {
		fontSize: '32px',
		color: '#FFFFFF',
		backgroundColor: '#000000',
		shadow: { fill: true, blur: 0, offsetY: 0 },
		padding: { left: 15, right: 15, top: 10, bottom: 10 }
	})
	.setOrigin(0.5)
}

The key change is that we use height * 0.9 on line 6 instead of the original height * 0.5.

Lastly, remember to stop the Leaderboard Scene when the game is restarted like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// in the Game Scene
update(t: number, dt: number)
{
	if (this.cursors.space?.isDown && this.mouse.isDead)
	{
		this.scene.stop(SceneKeys.GameOver)
		this.scene.stop(SceneKeys.Leaderboard)
		this.scene.restart()
		return
	}

	// other code...
}

Next Steps

You now have a global leaderboard to let your players compete with each other for high score!

The leaderboard plugin has more features that you can find here. We are just showing 5 scores but you can add paging to show more.

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 rex-plugins leaderboard firebase firestore typescript

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

How to Load Images Dynamically in Phaser 3

by on

Does your game have a lot of images that not every player sees? Maybe a collectible card game? Loading those images …

6 minute read

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

Didn't find what you were looking for?


comments powered by Disqus