0% found this document useful (0 votes)
172 views

Phaser3 TypeScript

This document describes how to build an infinite runner game in Phaser 3 using TypeScript. It covers setting up the project, creating game scenes and assets like spritesheets, adding player controls and obstacles, and implementing game over handling. The goal is to teach readers how to structure a full infinite runner game using best practices in Phaser and TypeScript.

Uploaded by

Igor Tiburcio
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
172 views

Phaser3 TypeScript

This document describes how to build an infinite runner game in Phaser 3 using TypeScript. It covers setting up the project, creating game scenes and assets like spritesheets, adding player controls and obstacles, and implementing game over handling. The goal is to teach readers how to structure a full infinite runner game using best practices in Phaser and TypeScript.

Uploaded by

Igor Tiburcio
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 101

Infinite Runner in Phaser 3 with

TypeScript

Tommy Leung

© 2020 Ourcade
Contents

Introduction 1
Why TypeScript? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
How to Use this Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Example Source Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2

Getting Started 4
Installing Node.js and NPM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Creating the project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Code Editing and Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

Repeating Background 7
Where to put static assets? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Adjusting Game size . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Create a new Game Scene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Understanding Origin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Repeating with a TileSprite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

Adding Rocket Mouse 13


Creating a Sprite Sheet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Loading a Sprite Sheet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Note on Sprite Sheet vs Sprite Atlas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Creating a Sprite from an Atlas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Creating the Run Animation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

Preloader and Enums 20


Creating a Preloader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Creating Animations in the Preloader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
What are Enums? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Using Enums for Texture Keys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Using Enums for Scene and Animation Keys . . . . . . . . . . . . . . . . . . . . . . . . . . 24

i
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Running and Scrolling 26


Adding Physics to Rocket Mouse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Run with Velocity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Scrolling the Background . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Start Rocket Mouse on the Floor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

Decorating the House 31


Adding a Mouse Hole Decorations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Looping the Mouse Hole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Let There be Windows! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Class it up with Bookcases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Avoid Overlapping Windows with Bookcases . . . . . . . . . . . . . . . . . . . . . . . . . . 40

Adding a Jetpack 44
Creating a RocketMouse Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Using RocketMouse in the Game Scene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Adding Physics to a Container . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Add Some Flames . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Turn the Jetpack On and Off . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Adding Thrust . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Adding Fly and Fall Animations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54

Creating a Laser Obstacle 58


Preload the Laser Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
Composing the Laser Obstacle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Add LaserObstacle to Game Scene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Looping the LaserObstacle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Making the Laser Dangerous . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Collide and Zap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Poor Man’s State Machine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68

Game Over and Play Again 72


Create a GameOver Scene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Show the Game Over Scene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Pressing Space to Play Again . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Another Way to Show GameOver Scene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Should the Game Over Scene Restart the Game Scene? . . . . . . . . . . . . . . . . . 76

Tommy Leung ii
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Add Coins to Collect 78


Load the Coin Image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Creating Coins with a Group . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Other Coin Formations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
Collecting Coins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
Displaying a Score . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Note on Collision Box Sizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
Creating Coins Periodically . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85

To Infinity and Beyond 87


Where Does our World End? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Creating a Seamless Teleport . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Teleporting the Background Seamlessly . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Fixing Coin Teleportation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Optional Animation Updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

Epilogue 94

About the Author 96

Tommy Leung iii


Introduction

Thank you for downloading this book!

We will be going through 11 chapters of game development in Phaser 3 using TypeScript to create an
infinite runner like Jetpack Joyride.

Our hero will even have a jetpack!

This book is intended for developers who have dabbled in Phaser 3 by making at least one game or by
going through the Infinite Jumper in Phaser 3 with Modern JavaScript book.

We won’t be going through the language basics of TypeScript but you can use the TypeScript Handbook
for that.

Instead, we will take you through a step-by-step process of making a Phaser 3 game using TypeScript
instead of JavaScript.

Phaser 3 comes with official TypeScript support but the TypeScript experience is sometimes wonky
and even confusing for those new to the framework or language.

There are certain situations that may be awkward or don’t fit cleanly with TypeScript expectations but
are completely normal in the greater JavaScript ecosystem.

We’ll show you ways around those issues while maintaining the static analysis and other features that
make TypeScript so useful and popular in larger codebases.

Why TypeScript?

TypeScript was first released in 2012 as an open-source project by Microsoft. It has slowly gained
popularity over the years and appears to be really taking off as of 2019.

The fact that more and more professional teams and open-source projects are using TypeScript is a
sure sign that we should be paying attention.

Phaser 4, currently in early development, is one of these modern projects being written with Type-
Script.

There are clear signs of momentum but if that isn’t enough for you then let’s look at the benefits.

1
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

TypeScript gives you access to modern language features currently being designed into JavaScript
without having to wait for browsers to implement support or users to upgrade devices.

Features like private or protected access modifiers, async/await, optional chaining, generics,
and more that exist in modern languages like C#, Swift, or Kotlin.

There’s also type-safety that lets you write code that is easier to maintain, update, and used by oth-
ers–including a future version of yourself!

If you’ve ever had the experience of opening up an old JavaScript project to make a change only to
spend hours trying to figure out what the data being passed around looks then TypeScript could be
your new best friend.

TypeScript’s use static types help solve this problem by providing information about what the data is or
how it is structured without having to rely on having comments or trusting that they are up to date.

There are more benefits to using TypeScript for making games in Phaser 3 but we won’t list them all.

The fact that you picked up this book probably means you are already sold on TypeScript so let’s start
using it!

How to Use this Book

This book is designed for you to read and follow along from start to finish.

It is not a reference book that you can easily jump around in.

Each chapter builds on the previous and the level of complexity increases as you progress.

The goal is to build a working game that you can feel comfortable modifying and adding features on
your own.

Example Source Code

You can download the full source code of the infinite runner game we’ll be making on Github at:

https://github.com/ourcade/infinite-runner-template-phaser3

The repository uses git-lfs so be sure to have it installed before you use git clone to pull down
the image assets as well.

Alternatively, you can just download a zip from the releases tab.

Tommy Leung 2
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

We intend to improve this book with updates. Feel free to let me know of any errors, unclear instructions,
or any other problems by sending an email to [email protected] or letting us know on Twitter
@ourcadehq.

Now, let’s make a game, shall we?

Ourcade is a playful game development community for open-minded and optimistic learners and
developers. Visit us at http://ourcade.co

Tommy Leung 3
Getting Started

This book will go over making an infinite runner game like Jetpack Joyride called Rocket Mouse. The
goal is to keep our jetpack-wearing mouse alive for as long as possible collecting coins and avoiding
obstacles.

We recommend that you check out our Infinite Jumper in Phaser 3 with Modern JavaScript book if you
are new to Phaser 3 and web development.

This book will be more advanced and use common web development tools like Node.js, the Node
Package Manager (npm), and the command line.

As the title of the book suggests, we will be using TypeScript to help write code that is easier to reason
about and maintain over months or years.

Installing Node.js and NPM

The simplest way to get Node.js is to install it from https://nodejs.org.

There are other ways to install it as well but we won’t go into those options here.

If you already have Node.js and npm installed then you can skip this section. Check out this blog post
instead.

Once you get to the Node.js download page, select the version recommended for most users. At the
time of this writing it is the option on the left for v12.16.x.

The exact version does not matter for what we’ll be doing.

Download the installer and follow the on-screen instructions to install Node.js on your computer.

Then open a terminal or command prompt and run this command:

1 node --version

It will respond with a version of Node.js if installed properly. This should mean that the Node Package
Manager or npm is also installed.

Let’s check that by running this command:

4
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 npm --version

This will also respond with a version. Note that the versions for Node.js and npm will not match each
other and that is to be expected.

Creating the project

We will be using the phaser3-typescript-parcel-template to bootstrap our project.


You can find it on Github here.
If you are familiar with git then feel free to clone it.
If not, you can download it as a zip by clicking the green “Clone or download” button.
Feel free to change the name of the folder from ‘phaser3-typescript-parcel-template’ to something like
‘rocket-mouse-game’.
This project template uses Parcel as the application bundler and development web server.
If you’ve heard of webpack or rollup then parcel is similar to those tools except it prides itself on
being zero-config to start and blazingly fast.
You’ll need to have Parcel installed so open a command line window and run this command:

1 npm install -g parcel-bundler

To confirm that Parcel was installed successfully, run this command:

1 parcel --version

It will respond with a version number like 1.12.4.


Now let’s get this project template ready for use! Open the project folder in the command line and run
the command:

1 npm install

This will install dependencies like Phaser 3 and a couple of Parcel plugins to automate some mundane
tasks.
Next, run this command to start the development server:

1 npm run start

You’ll know that the development server is ready when you see a message like “Built in 3.25s”.
There will also be a line above it that says:

Tommy Leung 5
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Server running at http://localhost:8000.

Open a new browser window and go to http://localhost:8000.

You should see a black window with the Phaser logo bouncing around!

Code Editing and Testing

This book assumes you are using Visual Studio Code as your code editor and a modern web browser
like Google Chrome.

If you don’t have an attachment to another editor like Atom, Brackets, or Webstorm then we highly
recommend you download and install VS Code here.

Now open the project in your code editor and we’ll start writing some code in the next chapter!

Tommy Leung 6
Repeating Background

The first thing we need with an infinite runner is background scenery that repeats forever.
We will be using assets from Game Art Guppy.
There’s a lot of different art assets that you can download from there for free.
We’ll be using the House 1 Background - Repeatable asset that comes with a laser obstacle, coins to
collect, and various decor.
There’s also a sleeping dog and cat but we won’t be using them in this book. You are welcome to find
creative uses for them!

Where to put static assets?

Our project template is set up to serve static files like images in a folder named public.
Create one at your project root on the same level as the src folder.
It should look like this:

1 rocket-mouse-game
2 o-- node_modules
3 o-- public // <-- here
4 o-- src
5 o-- scenes
6 -- HelloWorldScene.ts
7 -- index.html
8 -- main.ts
9 -- .eslintignore
10 -- .eslintrc.js
11 -- .gitignore
12 -- package.json
13 -- tsconfig.json

Then create a folder named house inside the public folder. This is where we will put all the background
and environment images.
The Game Art Guppy assets have a standard resolution and @2x high-resolution versions. We will be
using the standard resolution versions in this book.

7
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Copy those from the asset’s “Object” folder into house as well as the “bg_repeat_340x640.png” file
from the “Background” folder.

Your public folder should look like this:

1 rocket-mouse-game
2 o-- node_modules
3 o-- public
4 o-- house
5 -- bg_repeat_340x640.png
6 -- object_bookcase1.png
7 -- object_bookcase2.png
8 -- object_coin.png
9 -- object_laser_end.png
10 -- object_laser.png
11 -- object_mousehole.png
12 -- object_window1.png
13 -- object_window2.png
14 // other files...

Files and folders in public will be served from the root of our web server.

For example, you can load the bookcase at http://localhost:8000/house/object_bookcase1.png.

Try restarting the web server by terminating the process in the command line with ctrl + c and
running npm run start again if the image does not load.

If it still doesn’t work then double-check the directory structure to make sure it matches.

Adjusting Game size

You’ll notice that our background asset is 640 pixels tall but our game is currently 600 pixels tall. Let’s
adjust the height of our game to match.

Open main.ts and change the height property of the config variable from 600 to 640.

1 import Phaser from 'phaser'


2
3 import HelloWorldScene from './scenes/HelloWorldScene'
4
5 const config: Phaser.Types.Core.GameConfig = {
6 type: Phaser.AUTO,
7 width: 800,
8 height: 640, // <-- here
9 // other code...
10 }
11
12 export default new Phaser.Game(config)

Tommy Leung 8
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Save main.js and your browser window should automatically reload with the updates. It might be
hard to tell that 40 extra pixels were added but you can confirm by right-clicking on the game and
selecting “Inspect”.

The DevTools window will show up and highlight the canvas element being used to render the game.

It should look like this:

1 <canvas width="800" height="640">

Create a new Game Scene

Let’s create a new Scene called Game in the scenes folder where HelloWorldScene.ts is.

Make a new file named Game.ts with the following code:

1 import Phaser from 'phaser'


2
3 export default class Game extends Phaser.Scene
4 {
5 constructor()
6 {
7 super('game')
8 }
9
10 preload()
11 {
12
13 }
14
15 create()
16 {
17
18 }
19 }

This creates a new Game Scene by subclassing Phaser.Scene. It is given the key 'game' to uniquely
identify it from other Scenes.

Now, let’s load our repeating background image and display it in the Scene.

Tommy Leung 9
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 preload()
2 {
3 this.load.image('background', 'house/bg_repeat_340x640.png')
4 }
5
6 create()
7 {
8 this.add.image(0, 0, 'background')
9 .setOrigin(0, 0)
10 }

The preload() method now loads the bg_repeat_340x640.png file and assigns it to the key '
background'.

Then we create an image in create() using that key. We place it at (0, 0) and then use setOrigin
(0) to change the origin–it is also known as a pivot or anchor point–to the top left of the image.

By default, most GameObjects like images and sprites have an origin of (0.5, 0.5) which is the
middle of the object.
Next, we’ll need to add this Game Scene to the scene property of our GameConfig in main.ts to see it
in the browser.
Go to main.ts and makes these updates:

1 import Phaser from 'phaser'


2
3 // we can delete this line
4 // import HelloWorldScene from './scenes/HelloWorldScene'
5
6 // add this line to import Game
7 import Game from './scenes/Game'
8
9 const config: Phaser.Types.Core.GameConfig = {
10 type: Phaser.AUTO,
11 width: 800,
12 height: 640,
13 physics: {
14 default: 'arcade',
15 arcade: {
16 gravity: { y: 200 }
17 }
18 },
19 // replace HelloWorldScene with Game in the 'scene' property
20 scene: [Game]
21 }
22
23 export default new Phaser.Game(config)

Notice that we import Game at the top and then add it to the scene property as an Array.

Tommy Leung 10
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

The scene property does not have to be an Array but it often is because your game will likely have
more than one Scene.

Feel free to delete HelloWorldScene.ts as we will not be using it anymore.

After these changes, your game should look something like this:

Understanding Origin

Play around with setOrigin(x, y) to get a feel for how it works.

The origin designates where (0, 0) should be local to the image or sprite.

The values are from 0 - 1 where 0 is the left or top and 1 is the right or bottom for x and y respec-
tively.

This is different than the Scene coordinates of (0, 0) for placing the background image in the game.

Tommy Leung 11
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Adjusting the origin of an image or sprite is useful to help us more easily reason about transforming
them.

For example, rotating an image is usually done with the origin set to (0.5, 0.5) because most things
spin from the center like a wheel.

It would take a lot more mental work to emulate a rotation from the center with an origin set to (0, 0).
You’d have to set the rotation and then figure out what the x and y offset should be for it to look
accurate.

Repeating with a TileSprite

Phaser has a built-in TileSprite that will conveniently repeat the same image over a given width.

We can achieve the same effect manually by creating 3 images and setting the x value to be next to the
previous image.

But why do that when Phaser can handle it for us?

Here’s how to make a TileSprite:

1 create()
2 {
3 // store the width and height of the game screen
4 const width = this.scale.width
5 const height = this.scale.height
6
7 // change this.add.image to this.add.tileSprite
8 // notice the changed parameters
9 this.add.tileSprite(0, 0, width, height, 'background')
10 .setOrigin(0)
11 }

This is fairly similar to adding an image except we use this.add.tileSprite and pass in width and
height to let the TileSprite know how much to repeat the background image.

The width and height values are retrieved from Phaser’s ScaleManager.

Now, your browser should reload and show you the background repeated across the game screen.
There should be no more black space.

Next, we will add our jetpack-wearing hero: Rocket Mouse!

Tommy Leung 12
Adding Rocket Mouse

First, download the Rocket Mouse assets from Game Art Guppy here.

Our intrepid hero will have these animations:

• running
• flying
• falling
• dead

He will be running whenever he is on the ground. Flying when the jetpack is engaged. Falling when
he’s in the air but no longer ascending. And dead when he runs into the deadly home security laser.

This must be a house owned by spies. . . or a supervillain.

Creating a Sprite Sheet

Animations usually use sprite sheets or atlases because it is more efficient to pick a different frame
from a single texture than to switch textures constantly.

The art did not come with a sprite sheet or atlas so we will have to make one.

We recommend using TexturePacker by CodeAndWeb. There are other options as well but
TexturePacker’s free version will do everything we need.

Download and install TexturePacker and then open it or your texture packing tool of choice.

Drag the standard resolution images of Rocket Mouse (the files without @2x) from the “rocket-
mouse_animations” folder into TexturePacker’s Sprite panel on the left.

Note: the flying animation image at standard resolution is missing from the download. Go to this issue
from our Github repository for this project and grab rocketmouse_fly01.png.

In the Settings panel on the right side of TexturePacker select “Phaser (JSONHash)” for Framework.
Then set the Data file and Texture file paths to a characters folder inside this project’s public
folder.

13
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

We are using the file name rocket-mouse.

You will have to create this folder before being able to select it in TexturePacker. Make it in the same
way we did for the house assets.

The rest of the settings you can leave alone. This is what TexturePacker should look like:

You can also choose “Phaser 3” as the Framework if you have TexturePacker Pro. Both options will
work for this project.

Click the “Publish sprite sheet” button near the middle top of TexturePacker to generate the PNG and
JSON files required for Phaser.

Your project should look like this:

1 rocket-mouse-game
2 o-- node_modules
3 o-- public
4 o-- house
5 o-- characters // <-- the new folder
6 -- rocket-mouse.json
7 -- rocket-mouse.png
8 o-- src
9 // other files...

Tommy Leung 14
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Loading a Sprite Sheet

We can load the sprite sheet created with TexturePacker like this in the Game Scene:

1 preload()
2 {
3 // previous code to load background
4 this.load.image('background', 'house/bg_repeat_340x640.png')
5
6 // load as an atlas
7 this.load.atlas(
8 'rocket-mouse',
9 'characters/rocket-mouse.png',
10 'characters/rocket-mouse.json'
11 )
12 }

Lines 7 - 11 is how we load the sprite sheet. We are using multiple lines for better book formatting. You
can have it as a single line like this.load.image above it.

Note on Sprite Sheet vs Sprite Atlas

The terms sprite sheet and sprite atlas are often used interchangeably.

But you’ll notice that we are using this.load.atlas instead of this.load.spritesheet in the
example above.

A sprite sheet or sprite atlas is one image that contains many smaller images.

Phaser considers sprite sheets to be images where each frame is a fixed size and can be referenced
using a numerical index.

On the other hand, sprite atlases have frames of different sizes and are referenced by alphanumeric
names like file names.

The sprite sheet generated by TexturePacker fits the second type and that’s why we are using this.
load.atlas.

Creating a Sprite from an Atlas

We can create either an Image or a Sprite from an atlas. We’ll be using Sprite because Rocket
Mouse has animations.

Let’s make sure the atlas loaded properly by creating a Sprite of a flying Rocket Mouse.

Tommy Leung 15
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 create()
2 {
3 const width = this.scale.width
4 const height = this.scale.height
5
6 // previous code...
7
8 this.add.sprite(
9 width * 0.5, // middle of screen
10 height * 0.5,
11 'rocket-mouse', // atlas key given in preload()
12 'rocketmouse_fly01.png'
13 )
14 }

Creating a Sprite is very similar to creating an Image or TileSprite.

We use this.add.sprite and then pass in an x and y value along with the key of the atlas and the
frame to use.

The key 'rocket-mouse' is the same value used to load the atlas in preload().

Then the frame name rocketmouse_fly01.png is from the JSON file. Open rocket-mouse.json to
see that the original file names of the individual images making up the atlas are used as the key for the
properties defining where in the atlas a particular image is.

1 {
2 // other keys above
3 "rocketmouse_fly01.png":
4 {
5 "frame": {"x":490,"y":0,"w":134,"h":126},
6 "rotated": false,
7 "trimmed": false,
8 "spriteSourceSize": {"x":0,"y":0,"w":134,"h":126},
9 "sourceSize": {"w":134,"h":126}
10 },
11 // other keys below
12 }

Save your changes and the browser should reload showing Rocket Mouse in the middle of the screen.

Tommy Leung 16
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Creating the Run Animation

The core difference between an Image and a Sprite in Phaser is that sprites can play animations and
images cannot.

So let’s make an animation to play!

Animations are global and created by the AnimationManager. Once created, it can be used by any
Sprite in any Scene.

Add this to the top of create():

Tommy Leung 17
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 create()
2 {
3 this.anims.create({
4 key: 'rocket-mouse-run', // name of this animation
5 // helper to generate frames
6 frames: this.anims.generateFrameNames('rocket-mouse', {
7 start: 1,
8 end: 4,
9 prefix: 'rocketmouse_run',
10 zeroPad: 2,
11 suffix: '.png'
12 }),
13 frameRate: 10,
14 repeat: -1 // -1 to loop forever
15 })
16
17 // previous code...
18 }

The run animation is created by accessing the AnimationManager with this.animas.create and
passing in a configuration.

The configuration needs a key which is the name of the animation: 'rocket-mouse-run'.

Then frames needs an Array of frame definitions that we generate using this.anims.
generateFrameNames().

We can also manually create the frame definitions without the helper method like this:

1 this.anims.create({
2 frames: [
3 { key: 'rocket-mouse', frame: 'rocketmouse_run01.png' },
4 { key: 'rocket-mouse', frame: 'rocketmouse_run02.png' },
5 { key: 'rocket-mouse', frame: 'rocketmouse_run03.png' },
6 { key: 'rocket-mouse', frame: 'rocketmouse_run04.png' }
7 ]
8 // other properties...
9 })

This is the same as what this.anims.generateFrameNames() will generate using the data it is
given.

The zeroPad property is not strictly necessary in this case because we only have 4 frames. Setting
prefix to rocketmouse_run0 would have also worked.

But zeroPad is necessary if the animation has more than 9 frames.

You can adjust the frameRate property to speed up or slow down the animation.

Lastly, repeat is set to -1 so that the animation will loop for as long as it is active.

Tommy Leung 18
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

To play this animation, call play('rocket-mouse-run') on the created Sprite:

1 create()
2 {
3 // other code...
4
5 this.add.sprite(
6 width * 0.5,
7 height * 0.5,
8 'rocket-mouse',
9 'rocketmouse_fly01.png'
10 )
11 .play('rocket-mouse-run')
12 }

Now, you should see a floating Rocket Mouse running in mid-air.

We haven’t written too much code yet but there are already a couple of things we see that will make
the code harder to read and more error-prone.

There will be at least 4 animations and creating them in the Game Scene’s create() method will create
clutter. Animations are global so we don’t need to create these animations in Game.

String literals for key names like 'rocket0mouse' will be used many times. Having to manually type
it out each time will lead to a greater chance of making a typo and introducing a bug.

In the next chapter, we will add a Preloader Scene and enums to avoid these problems!

Tommy Leung 19
Preloader and Enums

We mentioned in the last chapter that animations are global so we can create them outside of the Game
Scene.

This is also true of loaded assets like images and atlases. Once they are loaded, they can be accessed
by any Scene.

Those two things allow for a Preloader Scene to handle loading all assets and creating animations
when the game starts.

Creating a Preloader

Let’s start by creating a Preloader.ts file in the scenes folder with the following set up:

1 import Phaser from 'phaser'


2
3 export default class Preloader extends Phaser.Scene
4 {
5 constructor()
6 {
7 super('preloader')
8 }
9
10 preload()
11 {
12
13 }
14
15 create()
16 {
17
18 }
19 }

Very simple and looks just like how we started the Game Scene a couple of chapters ago.

Next, let’s copy the code for preload() in the Game Scene over to the Preloader Scene.

20
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 preload()
2 {
3 this.load.image('background', 'house/bg_repeat_340x640.png')
4
5 this.load.atlas(
6 'rocket-mouse',
7 'characters/rocket-mouse.png',
8 'characters/rocket-mouse.json'
9 )
10 }

Phaser will only call create() after all the assets specified in preload() have been loaded.

This means we can use create() to know when we can go to the Game Scene.

We’ll use the SceneManager to change Scenes once the assets are loaded like this:

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

You can delete the preload() method from the Game Scene since we won’t be needing it anymore.

Next, we need to add the Preloader Scene to the GameConfig in main.ts as we did for the Game
Scene earlier.

1 import Phaser from 'phaser'


2
3 // import Preloader scene
4 import Preloader from './scenes/Preloader'
5 import Game from './scenes/Game'
6
7 const config: Phaser.Types.Core.GameConfig = {
8 // other config...
9
10 // add Preloader as the first element in the Array
11 scene: [Preloader, Game]
12 }
13
14 export default new Phaser.Game(config)

The first Scene in the Array given to the scene property will be automatically started by Phaser.

We want the Preloader Scene to start first so we add it to the beginning of the list.

Now, your browser should reload and show the same thing we had at the beginning of the chapter.

No visual changes mean we did this code refactor properly!

Tommy Leung 21
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Creating Animations in the Preloader

The last thing we’ll bring over to the Preloader Scene is the animations.

Copy the code that creates the rocket-mouse-run animation from the Game Scene and add it to the
create() method of Preloader.

1 create()
2 {
3 // copied from Game Scene
4 this.anims.create({
5 key: 'rocket-mouse-run',
6 frames: this.anims.generateFrameNames('rocket-mouse', {
7 start: 1,
8 end: 4,
9 prefix: 'rocketmouse_run',
10 zeroPad: 2,
11 suffix: '.png'
12 }),
13 frameRate: 10,
14 repeat: -1
15 })
16
17 // now start the Game Secne
18 this.scene.start('game')
19 }

Everything should still look the same once your browser reloads. Rocket Mouse is floating mid-air as
he runs in place.

What are Enums?

Enums are a TypeScript language feature that can be replicated in JavaScript using constants. It can
also be found in other languages like C#.

For example, an enum is an enumeration of values like this:

1 enum Days
2 {
3 Sunday,
4 Monday,
5 Tuesday,
6 Wednesday,
7 Thursday,
8 Friday,
9 Saturday
10 }

Tommy Leung 22
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

This is an example of an enum called Days with values that are each day of the week. Enum values are
numbers by default so Days.Sunday is equal to 0 and Days.Saturday is equal to 6.

In the modern world, enums can help us avoid typos as VS Code will autocomplete it for us, and
TypeScript will give us an error if we type something like Days.Frday since it was not defined.

This alone makes enums a useful feature but it also makes code more readable in situations where you
need maximum space savings like coding in the 1980s, for an embedded device, or sending network
data in a real-time multiplayer game.

It is easier for humans to read Days.Sunday than it is to parse out that 0 means Sunday within the
specific context.

We don’t have any limited space requirements but we can still benefit from type checking and auto-
completion by using string values with enums.

Using Enums for Texture Keys

Let’s start by creating a new folder named consts in the src folder on the same level as scenes.

1 rocket-mouse-game
2 o-- node_modules
3 o-- public
4 o-- src
5 o-- consts // <-- create here
6 o-- scenes
7 // other files...

Then, create a file in consts named TextureKeys.ts with the following code:

1 enum TextureKeys
2 {
3 Background = 'background',
4 RocketMouse = 'rocket-mouse'
5 }
6
7 export default TextureKeys

This creates an enum called TextureKeys with 2 string values named Background and RocketMouse
.

We then export it as the default export on the last line. This is just like how we’ve been creating Scenes
when we use export default class Preloader in the declaration.

In this example, we specify the default export after declaring the enum.

Exporting is what allows us to use the import syntax later.

Tommy Leung 23
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Now, let’s go back to the Preloader Scene and replace the string literals when we load assets.

1 // import at top of Preloader.ts


2 import TextureKeys from '../consts/TextureKeys'
3
4 export default class Preloader extends Phaser.Scene
5 {
6 // other code...
7
8 preload()
9 {
10 this.load.image(
11 TextureKeys.Background, // <-- here
12 'house/bg_repeat_340x640.png'
13 )
14
15 this.load.atlas(
16 TextureKeys.RocketMouse, // <-- and here
17 'characters/rocket-mouse.png',
18 'characters/rocket-mouse.json'
19 )
20 }
21
22 // create()...
23 }

This is pretty straight forward so go do the same for the keys used to create the background and Rocket
Mouse in the Game Scene and anywhere else these string literals are being used.

Using Enums for Scene and Animation Keys

Now let’s apply what we did with TextureKeys to Scene and Animation keys.

For Scenes, we are using the strings 'game' and 'preloader'. An enum would look like this:

1 enum SceneKeys
2 {
3 Preloader = 'preloader',
4 Game = 'game'
5 }
6
7 export default SceneKeys

Put the above code in a file named SceneKeys.ts in the consts folder with TextureKeys.ts.

Then go and replace usages of 'game' in this.scene.start('game') and the Game Scene con-
structor as well as 'preloader' in the Preloader Scene constructor.

Tommy Leung 24
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Animation keys should have an AnimationKeys.ts file in the consts folder with the following
code:

1 enum AnimationKeys
2 {
3 RocketMouseRun = 'rocket-mouse-run'
4 }
5
6 export default AnimationKeys

Then replace usages of 'rocket-mouse-run' in the Preloader and Game Scenes.

You couldn’t tell that we did any work in this chapter by looking at the game but our code is now much
better than when we first started!

Next, we will start having Rocket Mouse run along the floor!

Tommy Leung 25
Running and Scrolling

The key action in any infinite runner is running.

We will add that in this chapter but first, we need to make Rocket Mouse a physics sprite and add a
physics world bounds so that he doesn’t fall into the abyss.

Adding Physics to Rocket Mouse

Make these updates to the Game Scene:

1 create()
2 {
3 // other code ...
4
5 // change this.add.sprite to this.physics.add.sprite
6 // and store the sprite in a mouse variable
7 const mouse = this.physics.add.sprite(
8 width * 0.5,
9 height * 0.5,
10 TextureKeys.RocketMouse
11 'rocketmouse_fly01.png'
12 )
13 .play(AnimationKeys.RocketMouseRun)
14
15 const body = mouse.body as Phaser.Physics.Arcade.Body
16 body.setCollideWorldBounds(true)
17
18 this.physics.world.setBounds(
19 0, 0, // x, y
20 Number.MAX_SAFE_INTEGER, height - 30 // width, height
21 )
22 }

Adding physics to Rocket Mouse is as easy as replacing this.add.sprite with this.physics.add


.sprite.

The created physics sprite is stored in the mouse variable that we use to access the physics body with
mouse.body.

26
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Notice that we create a local body variable to store mouse.body and then cast it as a Phaser.Physics
.Arcade.Body.

Type-casting is a concept in typed languages that lets the programmer tell the compiler that a certain
object should be considered a different or more specific type.

The body property of GameObjects can be a Phaser.Physics.Arcade.Body or Phaser.Physics


.Arcade.StaticBody. We use type-casting so that VS Code can continue to give us accurate auto-
complete options.

Then we call body.setColliderWorldBounds(true) so that Rocket Mouse will collide with the
world bounds set on the next line.

Notice that we use Number.MAX_SAFE_INTEGER for the width. We are making an infinite runner but
computer memory is not infinite so numbers have a maximum size.

We will go over how to create the feeling of infiniteness from a finite world in a later chapter.

Lastly, we set height as the height of the game screen minus 30 pixels. This will make it look like Rocket
Mouse is running on the floor of the background.

Once your browser reloads, Rocket Mouse should appear running in mid-air, fall to the floor, and
continue running in place.

Run with Velocity

The player doesn’t get to the control when Rocket Mouse runs in an infinite runner. He is always running
so we can use setVelocityX() as soon as we create him.

1 create()
2 {
3 // other code...
4
5 const body = mouse.body as Phaser.Physics.Arcade.Body
6 body.setCollideWorldBounds(true)
7
8 // set the x velocity to 200
9 body.setVelocityX(200)
10
11 // set physics world bounds...
12 }

We add body.setVelocityX(200) to make Rocket Mouse move right as soon as he is created.

You should see him run off screen after your browser reloads.

Tommy Leung 27
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

So let’s follow him by calling startFollow() on the main camera.

1 create()
2 {
3 // other code...
4
5 this.cameras.main.startFollow(mouse)
6 this.cameras.main.setBounds(0, 0, Number.MAX_SAFE_INTEGER, height)
7 }

The first line tells the camera to follow Rocket Mouse as he moves around the world.

The second line sets the bounds for the camera so that it doesn’t move past the top or bottom of the
screen.

This will keep the environment within the screen bounds as Rocket Mouse falls to the floor or flies to
the ceiling with his jetpack.

You’ll notice that the background scrolls off the screen quickly leaving our mouse to run in the dark!

Scrolling the Background

Phaser’s TileSprite is built with scrolling backgrounds in mind so we only need to make a few
changes.

1 create()
2 {
3 // width and height defined here...
4
5 this.add.tileSprite(0, 0, width, height, TextureKeys.Background)
6 .setOrigin(0, 0)
7 .setScrollFactor(0, 0) // <-- keep from scrolling
8
9 // other code...
10 }

We added setScrollFactor(0, 0) to stop the background from being scrolled by the camera.

But now it just looks like Rocket Mouse is running in place so we need to add code that will update the
tilePosition property of the background TileSprite.

The camera’s scroll value is changing every frame so we’ll need to make updates to tilePosition in
the Scene’s update() method. And to do that we’ll need to store the background TileSprite as a
class property.

Tommy Leung 28
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5 // create the background class property
6 private background!: Phaser.GameObjects.TileSprite
7
8 // constructor...
9
10 create()
11 {
12 // width and height...
13
14 // store the TileSprite in this.background
15 this.background = this.add.tileSprite(
16 0, 0,
17 width, height,
18 TextureKeys.Background
19 )
20 .setOrigin(0, 0)
21 .setScrollFactor(0, 0)
22
23 // other code...
24 }
25
26 update(t: number dt: number)
27 {
28 // scroll the background
29 this.background.setTilePosition(this.cameras.main.scrollX)
30 }
31 }

We create a private class property named background and set the type to Phaser.GameObjects
.TileSprite.

The ! is TypeScript’s non-null assertion operator. This lets us tell TypeScript that the background
property will never be undefined or null.

TypeScript thinks it can be null because we are not setting the background property in the construc-
tor. Phaser 3’s design doesn’t let us create a TileSprite in the constructor so we are using the !
operator as a workaround.

Other alternatives are to check that this.background exists before using it or using the optional
chaining operator (?). We feel using the non-null assertion operator when declaring a class property is
the cleanest.

Next, we store the created TileSprite in this.background and use it in update().

The two paramters in the update() method are the total time elapsed since the game started (t) and

Tommy Leung 29
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

the time elapsed since the last frame (dt).

The background is scrolled based on the main camera’s scrollX value by passing it to this.
background.setTilePosition().

Now the game looks like Rocket Mouse is running in a house!

Start Rocket Mouse on the Floor

The last thing for this chapter is to start Rocket Mouse on the floor instead of in mid-air.

A trick to make it easier to position characters that walk or run is to set their origin point to the bottom
where their feet are.

Then we can very simply set their y value to wherever the top of the floor is.

1 create()
2 {
3 // other code...
4
5 const mouse = this.physics.add.sprite(
6 width * 0.5,
7 height - 30, // set y to top of floor
8 TextureKeys.RocketMouse,
9 'rocketmouse_fly01.png'
10 )
11 .setOrigin(0.5, 1) // <-- set origin to feet
12 .play(AnimationKeys.RocketMouseRun)
13
14 // other code...
15 }

We set the origin to the bottom of Rocket Mouse’s feet by using setOrigin(0.5, 1).

Remember that (0, 0) is the top left and (1, 1) is the bottom right. Using (0.5, 1) sets it to the
bottom middle where the feet are.

We also set the y position value to be height - 30. This was defined as the floor when we set the
physics world bounds.

We’re making progress on this infinite runner!

In the next chapter, we will add in background decorations to spruce things up.

Tommy Leung 30
Decorating the House

Running infinitely in an empty house is pretty boring. The same flower patterned background for as
long as the eye can see.

Let’s make things more visually interesting by adding decorations.

The House 1 Background - Repeatable asset from Game Art Guppy includes a good variety that should
already be in your project’s public/house folder.

We’ll start by adding the mouse hole.

Adding a Mouse Hole Decorations

First, we need to load the object_mousehole.png image in the Preloader Scene as we did for the
background.

1 import Phaser from 'phaser'


2
3 import TextureKeys from '../consts/TextureKeys'
4 // other imports...
5
6 export default class Preloader extends Phaser.Scene
7 {
8 // constructor
9
10 preload()
11 {
12 // the background
13 this.load.image(
14 TextureKeys.Background,
15 'house/bg_repeat_340x640.png'
16 )
17
18 // load the mouse hole image
19 // notice we are using TextureKeys.MouseHole
20 this.load.image(
21 TextureKeys.MouseHole,
22 'house/object_mousehole.png'
23 )

31
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

24
25 // other code
26 }
27
28 // other code...
29 }

We add a new line to load the object_mousehole.png file right below loading the background from
Chapter 2.

Notice that we are using TextureKeys.MouseHole. Make sure you add this new value to the
TextureKeys enum.

1 enum TextureKeys
2 {
3 Background = 'background',
4 MouseHole = 'mouse-hole', // <-- like this
5 RocketMouse = 'rocket-mouse'
6 }
7
8 export default TextureKeys

Then in the Game Scene we can create an image using the mouse hole texture.

Add it after the background and before Rocket Mouse so that it will layer properly.

1 create()
2 {
3 // create background TileSprite...
4
5 this.add.image(
6 Phaser.Math.Between(900, 1500), // x value
7 501, // y value
8 TextureKeys.MouseHole
9 )
10
11 // other code...
12 }

The use of this.add.image should be familiar by now but we added something new.

The value passed in as the x position uses Phaser.Math.Between to generate a random number
between 900 and 1500.

This will add some variety to where the mouse hole is placed each time the game starts.

Give it a test and you’ll see that it looks good but notice that it only appears once.

We can solve this by picking a new random position ahead of Rocket Mouse each time the mouse hole
passes the left side of the screen.

Tommy Leung 32
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Looping the Mouse Hole

First, we need to store the mouse hole Image in a class property so that we can check when it has
scrolled off the left side of the screen.

Create a private class property named mouseHole and set it to the mouse hole image in create()
.

1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5 // other properties
6 private mouseHole!: Phaser.GameObjects.Image
7
8 create()
9 {
10 // create background TileSprite...
11
12 this.mouseHole = this.add.image(
13 Phaser.Math.Between(900, 1500),
14 501,
15 TextureKeys.MouseHole
16 )
17
18 // other code...
19 }
20
21 // other code
22 }

Next, create a private method called wrapMouseHole(). It will determine when the mouseHole
scrolls off the left side of the screen and give it a new position ahead of Rocket Mouse.

1 private wrapMouseHole()
2 {
3 const scrollX = this.cameras.main.scrollX
4 const rightEdge = scrollX + this.scale.width
5
6 if (this.mouseHole.x + this.mouseHole.width < scrollX)
7 {
8 this.mouseHole.x = Phaser.Math.Between(
9 rightEdge + 100,
10 rightEdge + 1000
11 )
12 }
13 }

First, we create a variable scrollX that simply stores the main camera’s scrollX value. Then we

Tommy Leung 33
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

create a rightEdge variable that represents the right side of the screen.

It is determined by adding the width of the game screen to scrollX.

Then to check when the mouse hole has scrolled off the left side of the screen, we check that this.
mouseHole.x plus its width is less than scrollX.

When that is true, we give this.mouseHole.x a new random value that is at least 100 pixels past the
right side of the screen and no further than 1,000 pixels.

Last step is to call this.wrapMouseHole() from the update() method like this:

1 update(t: number, dt: number)


2 {
3 this.wrapMouseHole()
4
5 // other code...
6 }

Watch Rocket Mouse run after your browser reloads and you’ll see the mouse hole show up repeatedly
at different intervals.

The environment is now much less dull putting aside the many mouse holes in this house–maybe call
pest control?

Let There be Windows!

This house is more like an abandoned warehouse with no windows and a rodent problem than a
home.

We’ll just have to live with the latter but we can add windows in a similar fashion to the mouse hole.

First, load the object_window1.png and object_window2.png files in the Preloader Scene like
we did before.

1 preload()
2 {
3 // other images
4 // load mouse holes
5 this.load.image(
6 TextureKeys.MouseHole,
7 'house/object_mousehole.png'
8 )
9
10 // load windows
11 this.load.image(TextureKeys.Window1, 'house/object_window1.png')
12 this.load.image(TextureKeys.Window2, 'house/object_window2.png')

Tommy Leung 34
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

13
14 // other code
15 }

Be sure to add the new enum values to TextureKeys as well.

1 enum TextureKeys
2 {
3 // other values...
4
5 Window1 = 'window-1',
6 Window2 = 'window-2'
7 }

Next, we can create the images in the Game Scene.

We know that we’ll need to store them in class properties for the wrapping logic as we did for the
mouse hole so add those as well.

1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5 // other properties...
6
7 private window1!: Phaser.GameObjects.Image
8 private window2!: Phaser.GameObjects.Image
9
10 // constructor...
11
12 create()
13 {
14 // create background and mouse hole...
15
16 this.window1 = this.add.image(
17 Phaser.Math.Between(900, 1300),
18 200,
19 TextureKeys.Window1
20 )
21
22 this.window2 = this.add.image(
23 Phaser.Math.Between(1600, 2000),
24 200,
25 TextureKeys.Window2
26 )
27
28 // other code...
29 }
30
31 // other code...

Tommy Leung 35
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

This code should look familiar! It is basically the same as the code for the mouse hole.

You’ll see that it also has the same problem where the windows will only show up once.

Let’s apply the same technique we used to wrap and repeat the mouse hole for these windows by
creating a wrapWindows() method:

1 private wrapWindows()
2 {
3 const scrollX = this.cameras.main.scrollX
4 const rightEdge = scrollX + this.scale.width
5
6 // multiply by 2 to add some more padding
7 let width = this.window1.width * 2
8 if (this.window1.x + width < scrollX)
9 {
10 this.window1.x = Phaser.Math.Between(
11 rightEdge + width,
12 rightEdge + width + 800
13 )
14 }
15
16 width = this.window2.width
17 if (this.window2.x + width < scrollX)
18 {
19 this.window2.x = Phaser.Math.Between(
20 this.window1.x + width,
21 this.window1.x + width + 800
22 )
23 }
24 }

The concepts here are the same as for the mouse hole except we have 2 windows and they should not
overlap each other. We ensure that by using this.window1.x as the starting value to generate the
random x position for this.window2.

Notice that rightEdge is used for this.window1 like the mouse hole but then this.window1.x is
used in place of rightEdge for this.window2.x.

Remember to call this.wrapWindows() from the update() method like we did for this.
wrapMouseHole().

1 update(t: number, dt: number)


2 {
3 // for mouse holes
4 this.wrapMouseHole()
5
6 // add for windows
7 this.wrapWindows()
8

Tommy Leung 36
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

9 // other code...
10 }

Now our jetpack-wearing rodent can see the sights on his infinite run!

Class it up with Bookcases

We’ve got 1 more set of decorations and they are 2 bookcases. One tall and one short.

Let’s start by adding them in the same way we added the windows. There were 2 windows and here we
have 2 bookcases. They need to wrap around in the same way and avoid overlapping each other.

We know how to handle that!

First, load the two bookcase PNG files in the Preloader Scene.

1 preload()
2 {
3 // other images
4
5 // the previous windows
6 this.load.image(TextureKeys.Window1, 'house/object_window1.png')
7 this.load.image(TextureKeys.Window2, 'house/object_window2.png')
8
9 // load the bookcases
10 this.load.image(TextureKeys.Bookcase1, 'house/object_bookcase1.png'
)
11 this.load.image(TextureKeys.Bookcase2, 'house/object_bookcase2.png'
)
12
13 // other code...
14 }

Remember to add the new enum values to TextureKeys!

Next, we can add them to the Game Scene with class properties and random positions.

Tommy Leung 37
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5 // other properties...
6
7 private bookcase1!: Phaser.GameObjects.Image
8 private bookcase2!: Phaser.GameObjects.Image
9
10 // constructor...
11
12 create()
13 {
14 // window and other decorations...
15
16 this.bookcase1 = this.add.image(
17 Phaser.Math.Between(2200, 2700),
18 580,
19 TextureKeys.Bookcase1
20 )
21 .setOrigin(0.5, 1)
22
23 this.bookcase2 = this.add.image(
24 Phaser.Math.Between(2900, 3400),
25 580,
26 TextureKeys.Bookcase2
27 )
28 .setOrigin(0.5, 1)
29
30 // other code...
31 }
32
33 // other code...

This all looks just like the code we used for windows in the last section.

Wrapping the bookcases back around to the front should be the same too so let’s add a wrapBookcases
() method:

Tommy Leung 38
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 private wrapBookcases()
2 {
3 const scrollX = this.cameras.main.scrollX
4 const rightEdge = scrollX + this.scale.width
5
6 let width = this.bookcase1.width * 2
7 if (this.bookcase1.x + width < scrollX)
8 {
9 this.bookcase1.x = Phaser.Math.Between(
10 rightEdge + width,
11 rightEdge + width + 800
12 )
13 }
14
15 width = this.bookcase2.width
16 if (this.bookcase2.x + width < scrollX)
17 {
18 this.bookcase2.x = Phaser.Math.Between(
19 this.bookcase1.x + width,
20 this.bookcase1.x + width + 800
21 )
22 }
23 }

And lastly, add a call to this.wrapBookcases() to the update() method like we’ve done before.

1 update(t: number, dt: number)


2 {
3 // wrapping other decorations...
4 this.wrapMouseHole()
5 this.wrapWindows()
6
7 // wrap bookcases
8 this.wrapBookcases()
9
10 // other code...
11 }

You’ll notice that the bookcases behave exactly like the windows except... now you get instances when
a bookcase overlaps a window!

Maybe that’s fine? Some people like books more than sunlight and do block their windows with
bookcases...

But if we wanted to solve this how would we do it?

Tommy Leung 39
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Avoid Overlapping Windows with Bookcases

There are many ways to solve this problem and we are going to use a simple method that has the
side-benefit of creating more environmental variety.

What we’ll do is check if a window or bookcase’s x position overlaps with an existing bookcase or
window and then hide that window or bookcase if there is an overlap.

First, we’ll need an array of bookcases and windows as class properties like this:

1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5 // other properties..
6
7 private bookcases: Phaser.GameObjects.Image[] = []
8 private windows: Phaser.GameObjects.Image[] = []
9
10 // other code...
11 }

Then we want to add each created window or bookcase to their respective array.

1 create()
2 {
3 // other decorations...
4
5 this.window1 = this.add.image(
6 Phaser.Math.Between(900, 1300),
7 200,
8 TextureKeys.Window1
9 )
10
11 this.window2 = this.add.image(
12 Phaser.Math.Between(1600, 2000),
13 200,
14 TextureKeys.Window2
15 )
16
17 // create a new array with the 2 windows
18 this.windows = [this.window1, this.window2]
19
20 this.bookcase1 = this.add.image(
21 Phaser.Math.Between(2200, 2700),
22 580,
23 TextureKeys.Bookcase1
24 )
25 .setOrigin(0.5, 1)
26

Tommy Leung 40
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

27 this.bookcase2 = this.add.image(
28 Phaser.Math.Between(2900, 3400),
29 580,
30 TextureKeys.Bookcase2
31 )
32 .setOrigin(0.5, 1)
33
34 // create a new array with the 2 bookcases
35 this.bookcases = [this.bookcase1, this.bookcase2]
36
37 // other code...
38 }

Now, each time we move a window in wrapWindows() we need to check for an overlap like this:

1 private wrapWindows()
2 {
3 // other code...
4
5 let width = this.window1.width * 2
6 if (this.window1.x + width < scrollX)
7 {
8 this.window1.x = // pick random position
9
10 // use find() to look for a bookcase that overlaps
11 // with the new window position
12 const overlap = this.bookcases.find(bc => {
13 return Math.abs(this.window1.x - bc.x) <= this.window1.
width
14 })
15
16 // then set visible to true if there is no overlap
17 // false if there is an overlap
18 this.window1.visible = !overlap
19 }
20
21 width = this.window2.width
22 if (this.window2.x + width < scrollX)
23 {
24 this.window2.x = // pick random position
25
26 // do the same thing for window2
27 const overlap = this.bookcases.find(bc => {
28 return Math.abs(this.window2.x - bc.x) <= this.window2.
width
29 })
30
31 this.window2.visible = !overlap
32 }
33 }

Tommy Leung 41
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

The code to pay attention to is the use of this.bookcases.find() to find a bookcase that overlaps
with a window.
We determine overlap using a simple calculation of distance along the x-axis. Any distance smaller
than the width of the window is considered an overlap.
Then we do the same logic for this.window2.
Now let’s do the same thing for bookcases except we will check for overlaps against windows.

1 private wrapBookcases()
2 {
3 // other code...
4
5 let width = this.bookcase1.width * 2
6 if (this.bookcase1.x + width < scrollX)
7 {
8 this.bookcase1.x = // pick random position
9
10 // use find() to look for a window that overlaps
11 // with the new bookcase position
12 const overlap = this.windows.find(win => {
13 return Math.abs(this.bookcase1.x - win.x) <= win.width
14 })
15
16 // then set visible to true if there is no overlap
17 // false if there is an overlap
18 this.bookcase1.visible = !overlap
19 }
20
21 width = this.bookcase2.width
22 if (this.bookcase2.x + width < scrollX)
23 {
24 this.bookcase2.x = // pick random position
25
26 // do the same as we did above for bookcase2
27 const overlap = this.windows.find(win => {
28 return Math.abs(this.bookcase2.x - win.x) <= win.width
29 })
30
31 this.bookcase2.visible = !overlap
32 }
33 }

The code is almost the same except we check the list of windows instead of the list of bookcases. We
still use the window’s width because it is larger than the bookcase’s width.
This code can be made more generic to reduce some of the repetition but we will leave it like this for
simplicity.
Watch Rocket Mouse run for a while and you should see that the windows and bookcases will never

Tommy Leung 42
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

overlap each other. They simply become hidden when they do!

The mouse hole will get overlapped by the bookcase but we are okay with that.

If you don’t want the bookcase to cover the mouse hole then use the techniques we’ve learned in this
chapter to come up with a solution!

Game development is problem-solving.

Our game is looking pretty good with all the new decor!

In the next chapter, we will implement the most exciting part of this game: the jetpack.

Tommy Leung 43
Adding a Jetpack

What’s more fun than flying around with a jetpack?

The technology isn’t quite available to us in real life, yet, but we can do anything in a video game!

Adding jetpack functionality to Rocket Mouse requires turning on a flame animation and applying
some upward thrust in the physics engine.

Let’s tackle the first part by creating a RocketMouse class that we can attach a flame animation to.

Creating a RocketMouse Class

Phaser 3 GameObjects do not have children. This means that you cannot attach an Image to another
Image or Sprite.

Unless you are using a Container.

We will need to attach a flame animation as a child of Rocket Mouse so let’s subclass Phaser.
GameObjects.Container in our RocketMouse class.

Create a new folder named game in the src folder on the same level as scenes. Then create a
RocketMouse.ts file in the new game folder.

1 rocket-mouse-game
2 o-- node_modules
3 o-- public
4 o-- src
5 o-- consts
6 o-- game // <-- new folder
7 -- RocketMouse.ts // <-- new file
8 o-- scenes
9 // other files...

44
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Then add this code to RocketMouse.ts:

1 import Phaser from 'phaser'


2
3 export default class RocketMouse extends Phaser.GameObjects.Container
4 {
5 constructor(scene: Phaser.Scene, x: number, y: number)
6 {
7 super(scene, x, y)
8 }
9 }

This should look somewhat familiar as it is similar to creating new Scenes except we subclass Phaser
.GameObjects.Container instead of Phaser.Scene.

A Container holds children so let’s add a Rocket Mouse sprite as we did in Game. The difference is
that we don’t need to add a physics sprite. We’ll add physics to the Container instead.

1 import Phaser from 'phaser'


2
3 // import these
4 import TextureKeys from '../consts/TextureKeys'
5 import AnimationKeys from '../consts/AnimationKeys'
6
7 export default class RocketMouse extends Phaser.GameObjects.Container
8 {
9 constructor(scene: Phaser.Scene, x: number, y: number)
10 {
11 super(scene, x, y)
12
13 // create the Rocket Mouse sprite
14 const mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
15 .setOrigin(0.5, 1)
16 .play(AnimationKeys.RocketMouseRun)
17
18 // add as child of Container
19 this.add(mouse)
20 }
21 }

We create a Sprite in the constructor similar to how we did it in the Game Scene. Then we add it to
the Container with this.add(mouse).

Now, let’s try using RocketMouse in the Game Scene.

Tommy Leung 45
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Using RocketMouse in the Game Scene

We will replace the call to this.physics.add.sprite() in create() by creating a new


RocketMouse instance.

1 // import RocketMouse
2 import RocketMouse from '../game/RocketMouse'
3
4 // then in create()...
5 create()
6 {
7 // decorations...
8
9 // remove line with const mouse = this.physics.add.sprite(...)
10
11 // add new RocketMouse
12 const mouse = new RocketMouse(this, width * 0.5, height - 30)
13 this.add.existing(mouse)
14
15 // error happens here
16 const body = mouse.body as Phaser.Physics.Arcade.Body
17 body.setCollideWorldBounds(true)
18 body.setVelocityX(200)
19
20 // other code...
21 }

We removed the previous way of creating a physics sprite that represented Rocket Mouse and replaced
it with new RocketMouse(...).

Then we use this.add.existing(mouse) to add the RocketMouse instance to the Scene.

You will get errors after making this change because the mouse variable no longer has a body prop-
erty.

Let’s fix that by adding a physics body to RocketMouse.

Adding Physics to a Container

Go back to RocketMouse.ts and add this line to the constructor:

Tommy Leung 46
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 constructor(scene: Phaser.Scene, x: number, y: number)


2 {
3 super(scene, x, y)
4
5 const mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
6 .setOrigin(0.5, 1)
7 .play(AnimationKeys.RocketMouseRun)
8
9 this.add(mouse)
10
11 // add a physics body
12 scene.physics.add.existing(this)
13 }

You’ll notice that Rocket Mouse now appears higher in the game. Let’s see what’s going on by turning
on physics debug draw.

Go to main.ts and add debug: true to the physics config:

1 const config: Phaser.Types.Core.GameConfig = {


2 // other config...
3 physics: {
4 default: 'arcade',
5 arcade: {
6 gravity: { y: 200 },
7 debug: true // <-- turn on debug draw
8 }
9 }
10 }

Now you should see that it looks like Rocket Mouse is standing on a box!

Tommy Leung 47
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

We will need to adjust the size and offset of the physics body to match where the Rocket Mouse sprite
is.

The top left corner of the box and the point by Rocket Mouse’s feet is the origin of the Container.
Recall that we set the Rocket Mouse sprite origin to (0.5, 1) which is the bottom center.

This means we will need to offset the physics body by the negative height and negative half-width of
Rocket Mouse.

We can do it like this:

Tommy Leung 48
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 constructor(scene: Phaser.Scene, x: number, y: number)


2 {
3 super(scene, x, y)
4
5 const mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
6 .setOrigin(0.5, 1)
7 .play(AnimationKeys.RocketMouseRun)
8
9 this.add(mouse)
10
11 scene.physics.add.existing(this)
12
13 // adjust physics body size and offset
14 const body = this.body as Phaser.Physics.Arcade.Body
15 body.setSize(mouse.width, mouse.height)
16 body.setOffset(mouse.width * -0.5, -mouse.height)
17 }

Rocket Mouse should be running on the floor like before with a box around him after these changes.

Add Some Flames

Now, we can add some flames for the jetpack. The flames are animated by switching between two
different images.

You can see them in the rocket-mouse.png file that we created using TexturePacker in Chapter 3.

Let’s create a flames animation in Preloader like we did for Rocket Mouse’s running animation so
that we can use it for the jetpack.

1 // Preloader.ts
2
3 create()
4 {
5 // previous run animation
6 this.anims.create({
7 key: AnimationKeys.RocketMouseRun,
8 frames: this.anims.generateFrameNames(TextureKeys.RocketMouse,
{ start: 1, end: 4, prefix: 'rocketmouse_run', zeroPad: 2,
suffix: '.png' }),
9 frameRate: 10,
10 repeat: -1
11 })
12
13 // create the flames animation
14 this.anims.create({
15 key: AnimationKeys.RocketFlamesOn,

Tommy Leung 49
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

16 frames: this.anims.generateFrameNames(TextureKeys.RocketMouse,
{ start: 1, end: 2, prefix: 'flame', suffix: '.png'}),
17 frameRate: 10,
18 repeat: -1
19 })
20
21 this.scene.start(SceneKeys.Game)
22 }

Notice that we are using AnimationKeys.RocketFlameOn so be sure to add that to AnimationKeys


.ts like this:

1 enum AnimationKeys
2 {
3 RocketMouseRun = 'rocket-mouse-run',
4 RocketFlamesOn = 'rocket-flames-on'
5 }
6
7 export default AnimationKeys

Now we can go back to RocketMouse.ts and create a flame sprite and play the RocketFlamesOn
animation.

Add this code to the constructor of RocketMouse:

1 constructor(scene: Phaser.Scene, x: number, y: number)


2 {
3 // create mouse sprite...
4
5 // create the flames and play the animation
6 const flames = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
7 .play(AnimationKeys.RocketFlamesOn)
8
9 // add this first so it is under the mouse sprite
10 this.add(flames)
11 this.add(mouse)
12
13 // other code...
14 }

Creating the flames sprite and playing an animation should look familiar. It is the same as what we did
for the mouse sprite.

Pay attention to the order in which we added the flames and mouse sprites to the Container. The
flames are added first so that it layers behind the mouse.

This will give us the intended effect after we position the flames under the jetpack.

After the browser reloads you’ll see the flame animation playing by the feet of Rocket Mouse.

Tommy Leung 50
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

We can fix this by changing the position of the flame from (0, 0) to (-63, -15) like this:

1 const flames = scene.add.sprite(-63, -15, TextureKeys.RocketMouse)

Now it should look better! You are welcome to adjust these values if it doesn’t look right to you.

Turn the Jetpack On and Off

We have the flame animation working but that’s just the first part of getting the jetpack to work.

The second part is to apply thrust.

But before we do that, let’s add a method to turn the jetpack on or off. We’ll need it when the player
wants to go back to the ground.

First, create a new flames class property so that we can access the flames sprite from a method.

Tommy Leung 51
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 export default class RocketMouse extends Phaser.GameObjects.Container


2 {
3 private flames: Phaser.GameObjects.Sprite
4
5 constructor(scene: Phaser.Scene, x: number, y: number)
6 {
7 // create mouse...
8
9 this.flames = scene.add.sprite(-63, -15, TextureKeys.
RocketMouse)
10 .play(AnimationKeys.RocketFlamesOn)
11
12 this.add(this.flames)
13
14 // other code...
15 }

All we did was replace the local const flames with the class property this.flames.

Next, create a method called enableJetpack(enabled: boolean) with the following implemen-
tation:

1 enableJetpack(enabled: boolean)
2 {
3 this.flames.setVisible(enabled)
4 }

This method simply sets the visible property of the Sprite to true or false depending on what
was passed in for enabled.

Now, let’s start with the jetpack turned off by calling this.enableJetpack(false) in the construc-
tor.

1 constructor(scene: Phaser.Scene, x: number, y: number)


2 {
3 // create mouse...
4
5 this.flames = scene.add.sprite(-63, -15, TextureKeys.RocketMouse)
6 .play(AnimationKeys.RocketFlamesOn)
7
8 this.enableJetpack(false)
9
10 // other code...
11 }

Rocket Mouse should now be running on the floor without the jetpack turned on!

Tommy Leung 52
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Adding Thrust

We will allow the player to turn on the jetpack when they press the Space bar.

That means we’ll set the upwards acceleration of Rocket Mouse and enable the jetpack flames animation
when the Space bar is down.

For simplicity, we’ll access the Space bar using Phaser’s CursorKeys.

Let’s start by creating a cursors class property to hold an instance of CursorKeys:

1 // imports...
2
3 export default class RocketMouse extends Phaser.GameObjects.Container
4 {
5 // other properties...
6
7 // create the cursors property
8 private cursors: Phaser.Types.Input.Keyboard.CursorKeys
9
10 constructor(scene: Phaser.Scene, x: number, y: number)
11 {
12 // previous code...
13
14 // get a CursorKeys instance
15 this.cursors = scene.input.keyboard.createCursorKeys()
16 }
17 }

Phaser’s CursorKeys is a convenient way to get access to the 4 arrow keys and the Space bar.

With this we can check if the Space bar is pressed in a preUpdate() method.

A Container does not normally implement a preUpdate() method but it will get called if we create
one.
Note: we can create a more traditional update() method and then call it from the Game Scene
instead. But for simplicity, we will just use preUpdate(). You are welcome to use update()
instead.

Tommy Leung 53
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 preUpdate()
2 {
3 const body = this.body as Phaser.Physics.Arcade.Body
4
5 // check is Space bar is down
6 if (this.cursors.space?.isDown)
7 {
8 // set y acceleration to -600 if so
9 body.setAccelerationY(-600)
10 this.enableJetpack(true)
11 }
12 else
13 {
14 // turn off acceleration otherwise
15 body.setAccelerationY(0)
16 this.enableJetpack(false)
17 }
18 }

In preUpdate() we set the y acceleration of Rocket Mouse to -600 if the Space bar is down. Then we
turn off the acceleration when the Space bar is no longer pressed.

We also enable and disable the jetpack flames animation at the appropriate times.

Try it out and you should see the jetpack turn on when you press and hold the Space bar as Rocket
Mouse takes flight.

He will still be running because we have not created his fly or fall animations yet.

Adding Fly and Fall Animations

Let’s put some finishing touches on our jetpack functionality by adding the fly and fall animations for
Rocket Mouse.

These animations will only be 1 frame long so they are animations in concept only but it sets you up to
add more complex animations with flapping ears or wiggling feet.

We can create these two animations in the Preloader Scene as we’ve done before:

Tommy Leung 54
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 // Preloader.ts
2
3 create()
4 {
5 // previous run animation
6 this.anims.create({
7 key: AnimationKeys.RocketMouseRun,
8 frames: this.anims.generateFrameNames(TextureKeys.RocketMouse,
{ start: 1, end: 4, prefix: 'rocketmouse_run', zeroPad: 2,
suffix: '.png' }),
9 frameRate: 10,
10 repeat: -1
11 })
12
13 // new fall animation
14 this.anims.create({
15 key: AnimationKeys.RocketMouseFall,
16 frames: [{
17 key: TextureKeys.RocketMouse,
18 frame: 'rocketmouse_fall01.png'
19 }]
20 })
21
22 // new fly animation
23 this.anims.create({
24 key: AnimationKeys.RocketMouseFly,
25 frames: [{
26 key: TextureKeys.RocketMouse,
27 frame: 'rocketmouse_fly01.png'
28 }]
29 })
30
31 // other animations...
32 }

These animations are created in the same way we explained in Chapter 3. Refer back to that chapter if
you are unsure of what we did.

Remember to add the new RocketMouseFall and RocketMouseFly enum values to AnimationKeys
.ts.

Now, we can play the fly animation when we enable the jetpack and then play the fall animation when
Rocket Mouse’s velocity is greater than 0.

First, head back to RocketMouse.ts and create a new class property called mouse to store the refer-
ence to the mouse sprite.

Tommy Leung 55
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 // imports...
2
3 export default class RocketMouse extends Phaser.GameObjects.Container
4 {
5 // other properties...
6 private mouse: Phaser.GameObjects.Sprite
7
8 constructor(scene: Phaser.Scene, x: number, y: number)
9 {
10 super(scene, x, y)
11
12 // replace const mouse with this.mouse
13 this.mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
14 .setOrigin(0.5, 1)
15 .play(AnimationKeys.RocketMouseRun)
16
17 // add flames...
18
19 // add this.mouse instead of mouse
20 this.add(this.mouse)
21
22 scene.physics.add.existing(this)
23
24 // change calls to mouse variable to this.mouse
25 const body = this.body as Phaser.Physics.Arcade.Body
26 body.setSize(this.mouse.width, this.mouse.height)
27 body.setOffset(this.mouse.width * -0.5, -this.mouse.height)
28
29 // cursors...
30 }
31
32 // other code...
33 }

This change is necessary so that we can access the mouse sprite in preUpdate() to play different
animations.

Next, we can play the fly and fall animations when appropriate:

Tommy Leung 56
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 preUpdate()
2 {
3 const body = this.body as Phaser.Physics.Arcade.Body
4
5 if (this.cursors.space?.isDown)
6 {
7 body.setAccelerationY(-600)
8 this.enableJetpack(true)
9
10 // play the fly animation
11 this.mouse.play(AnimationKeys.RocketMouseFly, true)
12 }
13 else
14 {
15 body.setAccelerationY(0)
16 this.enableJetpack(false)
17 }
18
19 // check if touching the ground
20 if (body.blocked.down)
21 {
22 // play run when touching the ground
23 this.mouse.play(AnimationKeys.RocketMouseRun, true)
24 }
25 else if (body.velocity.y > 0)
26 {
27 // play fall when no longer ascending
28 this.mouse.play(AnimationKeys.RocketMouseFall, true)
29 }
30 }

We play the fly animation right after we turn on the jetpack.

The newer block of code after that checks if Rocket Mouse is touching the ground using body.blocked
.down. If he is touching the ground then switch to the run animation.

If he is not touching the ground and has a y velocity greater than 0 then play the fall animation.

Give it a try and you should see Rocket Mouse fly, then fall, then land and run!

This was a meaty chapter!

We’ve got a lot of the game working but it isn’t particularly challenging since there’s nothing to do...

In the next chapter, we will add some laser obstacles to spice it up!

Tommy Leung 57
Creating a Laser Obstacle

You might be wondering what kind of house has lasers for security?

Perhaps, a house with as many mouse holes as this one does? Clearly, there’s a pest-control problem.

Lasers might be overkill but. . . go big or go home, right?

Preload the Laser Images

The first thing we need to do is load the 2 laser images in the Preloader Scene.

1 // Preloader.ts
2
3 preload()
4 {
5 // other images...
6
7 this.load.image(TextureKeys.LaserEnd, 'house/object_laser_end.png')
8 this.load.image(TextureKeys.LaserMiddle, 'house/object_laser.png')
9 }

Remember to add the new enum values to TextureKeys like this:

1 enum TextureKeys
2 {
3 // previous values...
4
5 LaserEnd = 'laser-end',
6 LaserMiddle = 'laser-middle'
7 }
8
9 export default TextureKeys

Now, we can compose the laser obstacle using the two images we loaded.

58
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Composing the Laser Obstacle

We will be creating our laser obstacle in a similar fashion to what we did in the last chapter with
RocketMouse.ts.

There will be 3 pieces to the laser obstacle including a top, middle, and bottom. You may have noticed
that we only have 2 images: an end and a middle.
The bottom will be created by flipping the end image vertically.
Let’s start by creating a LaserObstacle.ts file in the game folder on the same level as RocketMouse
.ts.

1 import Phaser from 'phaser'


2
3 export default class LaserObstacle extends Phaser.GameObjects.Container
4 {
5 constructor(scene: Phaser.Scene, x: number, y: number)
6 {
7 super(scene, x, y)
8 }
9 }

This starting point is basically the same as what we did in the last chapter for RocketMouse.
Next, we can create each of the 3 sections and place them appropriately to look like a laser beam
between two ends.

1 constructor(scene: Phaser.Scene, x: number, y: number)


2 {
3 super(scene, x, y)
4
5 // create a top
6 const top = scene.add.image(0, 0, TextureKeys.LaserEnd)
7 .setOrigin(0.5, 0)
8
9 // create a middle and set it below the top
10 const middle = scene.add.image(
11 0,
12 top.y + top.displayHeight,
13 TextureKeys.LaserMiddle
14 )
15 .setOrigin(0.5, 0)
16
17 // set height of middle laser to 200px
18 middle.setDisplaySize(middle.width, 200)
19
20 // create a bottom that is flipped and below the middle
21 const bottom = scene.add.image(0, middle.y + middle.displayHeight,
TextureKeys.LaserEnd)

Tommy Leung 59
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

22 .setOrigin(0.5, 0)
23 .setFlipY(true)
24
25 // add them all to the Container
26 this.add(top)
27 this.add(middle)
28 this.add(bottom)
29 }

There’s quite a bit of code here but it is all relatively simple.

First, we create a top which is just the LaserEnd image with the origin set to the middle top. This will
make it easier to calculate where the middle laser part should go.

Next, we create a middle which is the LaserMiddle image with the origin set to the middle top as well.
Notice that the y position value is set to top.y + top.displayHeight. This will set the middle
image right below the top image.

Then, we set the height of the middle to be 200. This is the laser part between the top and bottom
ends.

After that, we create a bottom which is the same LaserEnd image as the top but flipped vertically
with the same origin as everyone else. Its y position is set to middle.y + middle.displayHeight
just like how the y position for middle was set to top.y + top.displayHeight. This ensures the
bottom image is right below the middle image.

Lastly, we add all 3 parts to the Container.

Add LaserObstacle to Game Scene

Let’s see our handiwork in action by adding a LaserObstacle to the Game Scene.

It will be very similar to how we created the RocketMouse class instance.

1 // import LaserObstacle
2 import LaserObstacle from '../game/LaserObstacle'
3
4 // then in create()...
5 create()
6 {
7 // create decorations...
8
9 const laserObstacle = new LaserObstacle(this, 900, 100)
10 this.add.existing(laserObstacle)
11
12 // create RocketMouse and other code...
13 }

Tommy Leung 60
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

We’ve omitted the previous code but you’ll want to add the LaserObstacle after the last decoration
and before RocketMouse so that it layers properly.

This is what you should see as Rocket Mouse runs toward 900 pixels on the x-axis:

Looping the LaserObstacle

We’ll want this LaserObstacle to loop back around once it has scrolled off the left side of the screen
just like the decorations in Chapter 6.

That means we’ll need to create a class property and create a method called wrapLaserObstacle()
that gets called in the update() method.

We’ve created many class properties already so you should know what to do!

1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5 // other properties...
6
7 private laserObstacle!: LaserObstacle
8
9 create()
10 {
11 // create decorations...
12
13 this.laserObstacle = new LaserObstacle(this, 900, 100)
14 this.add.existing(this.laserObstacle)
15
16 // create RocketMouse and other code...
17 }
18
19 }

We create the class property laserObstacle and then replace uses of const laserObstacle with
this.laserObstacle.

Next, let’s add the wrapLaserObstacle() method:

Tommy Leung 61
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 private wrapLaserObtacle()
2 {
3 const scrollX = this.cameras.main.scrollX
4 const rightEdge = scrollX + this.scale.width
5
6 const width = this.laserObstacle.width
7 if (this.laserObstacle.x + width < scrollX)
8 {
9 this.laserObstacle.x = Phaser.Math.Between(
10 rightEdge + width,
11 rightEdge + width + 1000
12 )
13
14 this.laserObstacle.y = Phaser.Math.Between(0, 300)
15 }
16 }

You’ll notice that this is very similar to wrapMouseHole() except we also pick a random value for the
y position of this.laserObstacle.

This will put the laser higher or lower on the screen forcing the player to maneuver with the jetpack.

Next, add a call to this.wrapLaserObstacle() in the update() method:

1 update(t: number, dt: number)


2 {
3 // wrap decorations...
4
5 this.wrapLaserObtacle()
6
7 // scroll background...
8 }

You may notice that the laser will disappear before it has fully scrolled off the screen

This is because the width of the LaserObstacle is currently 0. This will be fixed once we set a physics
body and define a size as we did with RocketMouse in the last chapter.

For now, just check that the laser is looping back to the front and picking different y position values.

Making the Laser Dangerous

Let’s add a physics body to the LaserObstacle so that our jetpack-wearing hero can run into it and
get zapped.

Make the following changes to the constructor of LaserObstacle:

Tommy Leung 62
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 constructor(scene: Phaser.Scene, x: number, y: number)


2 {
3 // previous code...
4
5 scene.physics.add.existing(this, true)
6
7 const body = this.body as Phaser.Physics.Arcade.StaticBody
8 const width = top.displayWidth
9 const height = top.displayHeight + middle.displayHeight +
10 bottom.displayHeight
11
12 body.setSize(width, height)
13 body.setOffset(-width * 0.5, 0)
14
15 // reposition body
16 body.position.x = this.x + body.offset.x
17 body.position.y = this.y
18 }

The first thing we do is use scene.physics.add.existing(this, true) to add a static physics


body to the Container.

The second parameter given the true value designates that the physics body should be static. A static
physics body will collide with normal bodies but will not be moved by gravity or pushed by other
objects.

This is perfect for the LaserObstacle because it needs to collide with Rocket Mouse and stay where
we put it.

Next, we set the size by using the width of the top and the combined height of all the pieces. Then the
offset is set to negative half-width and 0 based on the values we used for setOrigin() earlier.

Now, you should see a box around the LaserObstacle like this:

Tommy Leung 63
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Let it scroll off the screen and loop back around. You’ll notice that the collision box disappears!

This is because the physics body is static. We’ll need to manually set the body’s position when we
move the LaserObstacle in the Game Scene.

Update wrapLaserObstacle() with the follow code:

Tommy Leung 64
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 private wrapLaserObtacle()
2 {
3 const scrollX = this.cameras.main.scrollX
4 const rightEdge = scrollX + this.scale.width
5
6 // body variable with specific physics body type
7 const body = this.laserObstacle.body as
8 Phaser.Physics.Arcade.StaticBody
9
10 // use the body's width
11 const width = body.width
12 if (this.laserObstacle.x + width < scrollX)
13 {
14 this.laserObstacle.x = Phaser.Math.Between(
15 rightEdge + width,
16 rightEdge + width + 1000
17 )
18 this.laserObstacle.y = Phaser.Math.Between(0, 300)
19
20 // set the physics body's position
21 // add body.offset.x to account for x offset
22 body.position.x = this.laserObstacle.x + body.offset.x
23 body.position.y = this.laserObstacle.y
24 }
25 }

Now the physics collision box should move with the LaserObstacle when we loop it around to the
front of Rocket Mouse.

Collide and Zap

A laser obstacle is only good if it stings!

Let’s add a check for overlap between Rocket Mouse and the LaserObstacle so that we can make it
dangerous.

Update the create() method and add handleOverlapLaser() in the Game Scene:

Tommy Leung 65
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 // Game.ts
2
3 create()
4 {
5 // previous code...
6
7 this.physics.add.overlap(
8 this.laserObstacle,
9 mouse,
10 this.handleOverlapLaser,
11 undefined,
12 this
13 )
14 }
15
16 private handleOverlapLaser(
17 obj1: Phaser.GameObjects.GameObject,
18 obj2: Phaser.GameObjects.GameObject
19 )
20 {
21 console.log('overlap!')
22 }

We use this.physics.add.overlap() between the mouse and this.laserObstacle so that the


handleOverlapLaser method will be called when the two GameObjects overlap.

Give it a try and you should see the message overlap! in the Console of the browser’s Developer
Tools. You can bring up up the DevTools by right-clicking and selecting “Inspect” or going to View >
Developer > Developer Tools.

Now that we know when Rocket Mouse collides with the laser, we can play the death animation and
launch his body forward.

First, for the death animation we need to create it in the Preloader class like this:

Tommy Leung 66
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 // Preloader.ts
2
3 create()
4 {
5 // other animations...
6
7 this.anims.create({
8 key: AnimationKeys.RocketMouseDead,
9 frames: this.anims.generateFrameNames(TextureKeys.RocketMouse,
{
10 start: 1,
11 end: 2,
12 prefix: 'rocketmouse_dead',
13 zeroPad: 2,
14 suffix: '.png'
15 }),
16 frameRate: 10
17 })
18
19 this.scene.start(SceneKeys.Game)
20 }

Remember to add the enum value RocketMouseDead to AnimationKeys.ts.

Next, let’s create a kill() method in the RocketMouse class that will play this death animation and
launch our furry friend.

1 kill()
2 {
3 this.mouse.play(AnimationKeys.RocketMouseDead)
4
5 const body = this.body as Phaser.Physics.Arcade.Body
6 body.setAccelerationY(0)
7 body.setVelocity(1000, 0)
8 this.enableJetpack(false)
9 }

Give it a try and you’ll see Rocket Mouse play the death animation, get launched, and then run faster
than he’s ever run before!

This looks more like a power-up than death. . .

We can fix this by applying specific logic depending on what state Rocket Mouse is in. For example, he
can be dead or alive and the logic to handle what happens in those two states would be different.

This is where a State Machine comes in handy.

Tommy Leung 67
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Poor Man’s State Machine

For simplicity’s sake we will use a very basic state machine: the swtich statement.

First, create an enum at the top of the RocketMouse.ts file. This enum will define the different states
Rocket Mouse can be in.

1 enum MouseState
2 {
3 Running,
4 Killed,
5 Dead
6 }

Next, add a class property called mouseState:

1 export default class RocketMouse extends Phaser.GameObjects.Container


2 {
3 private mouseState = MouseState.Running
4
5 // other code...
6 }

Our first use of this mouseState property is to only perform the kill logic if Rocket Mouse is in the
RUNNING state.

Update kill() with this change:

1 kill()
2 {
3 // don't do anything if not in RUNNING state
4 if (this.mouseState !== MouseState.Running)
5 {
6 return
7 }
8
9 // set state to KILLED
10 this.mouseState = MouseState.Killed
11
12 this.mouse.play(AnimationKeys.RocketMouseDead)
13
14 const body = this.body as Phaser.Physics.Arcade.Body
15 body.setAccelerationY(0)
16 body.setVelocity(1000, 0)
17 this.enableJetpack(false)
18 }

We add a check at the top to see if mouseState is not equal to MouseState.RUNNING and early exit if
that is true. That means all the code after the if statement is not run if mouseState is not RUNNING.

Tommy Leung 68
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

This will ensure that Rocket Mouse doesn’t get killed multiple times or when he is already dead.

It just isn’t right to kick a mouse when he’s down. Right?

Next, let’s add a switch statement to preUpdate() and move the logic we already have for the
MouseState.RUNNING state.

1 preUpdate()
2 {
3 // switch on this.mouseState
4 switch (this.mouseState)
5 {
6 // move all previous code into this case
7 case MouseState.Running:
8 {
9 const body = this.body as Phaser.Physics.Arcade.Body
10
11 if (this.cursors.space?.isDown)
12 {
13 body.setAccelerationY(-600)
14 this.enableJetpack(true)
15
16 this.mouse.play(AnimationKeys.RocketMouseFly, true)
17 }
18 else
19 {
20 body.setAccelerationY(0)
21 this.enableJetpack(false)
22 }
23
24 if (body.blocked.down)
25 {
26 this.mouse.play(AnimationKeys.RocketMouseRun, true)
27 }
28 else if (body.velocity.y > 0)
29 {
30 this.mouse.play(AnimationKeys.RocketMouseFall, true)
31 }
32
33 // don't forget the break statement
34 break
35 }
36 }
37 }

There’s a lot of code here but we only added about 6 lines and 4 of them are braces.

We add a swtich state for this.mouseState and then only execute the code we had previously for
the RUNNING state. Don’t forget the break at the end of the case block.

Now give it a try and you should see a dead Rocket Mouse sliding along the floor. . . forever.

Tommy Leung 69
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

We are making progress!

Next, we will add logic for the MouseState.KILLED and MouseState.DEAD states to reduce Rocket
Mouse’s acceleration over time until his limp body comes to a stop.

Add these two case blocks to the switch statement:

1 preUpdate()
2 {
3 // move this out of the run logic to the top of the method
4 const body = this.body as Phaser.Physics.Arcade.Body
5
6 switch (this.mouseState)
7 {
8 case MouseState.Running:
9 {
10 // run logic
11 break
12 }
13
14 case MouseState.Killed:
15 {
16 // reduce velocity to 99% of current value
17 body.velocity.x *= 0.99
18
19 // once less than 5 we can say stop
20 if (body.velocity.x <= 5)
21 {
22 this.mouseState = MouseState.Dead
23 }
24 break
25 }
26
27 case MouseState.Dead:
28 {
29 // make a complete stop
30 body.setVelocity(0, 0)
31 break
32 }
33 }
34 }

We set this.mouseState to MouseState.KILLED in the kill() method and then launch Rocket
Mouse forward at high speed.

Then in preUpdate() we perform logic for the KILLED state by reducing the velocity until it is less
than or equal to 5. Once that is true, we set the mouseState to MouseState.DEAD.

Finally, in MouseState.DEAD we bring Rocket Mouse to a complete stop by setting the velocity to 0.

Give it a try and the entire death animation will look a lot more believable now!

Tommy Leung 70
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

We went over a lot in this chapter and now the game actually has a challenge. It is, perhaps, even
fun?

In the next chapter, we will add a Game Over Scene that will let us play again so we don’t have to just
sit and stare at a dead Rocket Mouse.

Tommy Leung 71
Game Over and Play Again

After 3 beefy chapters, we are going to relax just a little and work on adding a Game Over screen with
the ability to play again.

We ended the last chapter by implementing a poor man’s State Machine and we will use that to
determine when we show the Game Over screen.

Create a GameOver Scene

Our Game Over screen is going to be a Scene that runs in parallel with the Game Scene.

This is a common way to organize code for persistent UI like showing a score or a pause button as well
as to show modal dialogs.

Let’s start by creating a GameOver.ts file in the scenes folder on the same level as Game.ts.

1 import Phaser from 'phaser'


2
3 import SceneKeys from '../consts/SceneKeys'
4
5 export default class GameOver extends Phaser.Scene
6 {
7 constructor()
8 {
9 super(SceneKeys.GameOver)
10 }
11
12 create()
13 {
14 }
15 }

Creating new Scenes should be fairly familiar by now. This is the third Scene we’ve made in this book.

Don’t forget to add a new enum member to SceneKeys.ts.

72
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 enum SceneKeys
2 {
3 Preloader = 'preloader',
4 Game = 'game',
5 GameOver = 'game-over' // <-- new member
6 }
7
8 export default SceneKeys

Next, we can create some text to tell the player that they can press the Space bar to play again.

Add this code to the create() method:

1 create()
2 {
3 // object destructuring
4 const { width, height } = this.scale
5
6 // x, y will be middle of screen
7 const x = width * 0.5
8 const y = height * 0.5
9
10 // add the text with some styling
11 this.add.text(x, y, 'Press SPACE to Play Again', {
12 fontSize: '32px',
13 color: '#FFFFFF',
14 backgroundColor: '#000000',
15 shadow: { fill: true, blur: 0, offsetY: 0 },
16 padding: { left: 15, right: 15, top: 10, bottom: 10 }
17 })
18 .setOrigin(0.5)
19 }

The first thing we do is create a width and height variable using a language feature called Object
Destructuring or a Destructuring Assignment. This is a TypeScript and modern JavaScript feature that
creates variables by taking the values of the properties in an object that have the same names.

In this example, the ScaleManager has the properties width and height. Object Destructuring lets
us take those properties and create 2 constant local variables from them.

It is the same as writing this:

1 const width = this.scale.width


2 const height = this.scale.height

After that we choose an x and y position and create a Text object with some styling to make it more
pleasing to look at.

We used the Text Styler for Phaser 3 tool that you can find here. It is a free web-based tool from Ourcade

Tommy Leung 73
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

that lets you quickly and visually design text styles.

It will generate the appropriate code as you make changes. Just copy the config code when you are
happy with the design.

Having to tweak designs in code and reload the browser each time can be frustratingly slow so we
recommend you try out the tool!

Show the Game Over Scene

Before we can use the GameOver Scene, we need to add it to the list of scenes in Phaser’s GameConfig
in main.ts.

1 // import the scene


2 import GameOver from './scenes/GameOver'
3
4 // add it to the config
5 const config: Phaser.Types.Core.GameConfig = {
6 // other code...
7 scene: [Preloader, Game, GameOver] // <-- add it at the end
8 }

Now, we can use this Scene by running it after Rocket Mouse hits a laser obstacle and comes to a stop.

There are a few ways to implement this and we have chosen the simplest one for this book. We’ll give
you a more advanced option later that you can try implementing yourself!

Let’s go to the poor man’s State Machine that we have in the RocketMouse class and make this change
to the DEAD state:

1 // import SceneKeys at the top of RocketMouse.ts


2 import SceneKeys from '../consts/SceneKeys'
3
4 // then in the preUpdate() method
5 switch (this.mouseState)
6 {
7 // other cases...
8
9 case MouseState.Dead:
10 {
11 body.setVelocity(0, 0)
12
13 // add this line
14 this.scene.scene.run(SceneKeys.GameOver)
15 break
16 }
17 }

Tommy Leung 74
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

This is a very simple one-liner that tells the SceneManager to run the GameOver Scene. Notice that
we are using run() and not start().

We will be calling run() every frame that we are in the DEAD state. Phaser appears to be okay with
this but you can also use this.scene.scene.isActive(SceneKeys.GameOver) to check
that the GameOver Scene is not currently running before trying to run it.

We are using the odd-looking this.scene.scene because the Container has a reference to the
Game Scene that is stored as a property named scene and then the Game Scene has a reference to the
SceneManager that is also named scene.

It may look confusing but it is just how Phaser 3 happens to be structured. Just know that using
this.scene.scene is intended and not a typo.

Give this a try and you’ll see a banner with the message Press SPACE to Play Again.

Pressing the Space bar won’t do anything because we haven’t implemented that yet!

Pressing Space to Play Again

We will listen for the Space bar being pressed in the GameOver Scene and then restart the game when
it does.

Add this to the bottom of create() in GameOver.ts:

1 create()
2 {
3 // previous code to create text
4
5 // listen for the Space bar getting pressed once
6 this.input.keyboard.once('keydown-SPACE', () => {
7 // stop the GameOver scene
8 this.scene.stop(SceneKeys.GameOver)
9
10 // stop and restart the Game scene
11 this.scene.stop(SceneKeys.Game)
12 this.scene.start(SceneKeys.Game)
13 })
14 }

We listen for the keydown-SPACE event emitted by the KeyboardManger one time by using .once()
instead of .on().

When this happens, we stop the GameOver and Game Scenes and then we start the Game Scene again.

Give it a try and you should now be able to play the game again and again!

Tommy Leung 75
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Another Way to Show GameOver Scene

The main issue with how we are showing the GameOver Scene is that the RocketMouse class is making
the call.

It is perfectly correct code given the way Phaser 3 is structured but a new developer looking at this
project–or you in a couple of months–might not expect RocketMouse to make the Scene change.

It is more fitting for a Scene to handle Scene changes like we did with pressing Space to play again.

One way to have the Game Scene control showing the GameOver Scene is to have RocketMouse emit
an event when it enters the DEAD state. A Phaser.Events.EventEmitter can be created as a class
property of RocketMouse and then wrapper methods can be created to allow the Game Scene to listen
for an event like this:

1 mouse.once('dead', () => {
2 // run GameOver scene
3 })

We will leave implementing something like this as an exercise for you!

Should the Game Over Scene Restart the Game Scene?

If we are saying that it is odd for RocketMouse to perform a Scene change then wouldn’t it also stand
to reason that it is odd for the GameOver Scene to control restarting the Game Scene?

It was the Game Scene that started the GameOver Scene so the responsibility to stop the GameOver
Scene should belong to the Game Scene. It would also make more sense if the Game Scene restarted
itself.

We opted for the simplest implementation given the simplicity of this game but it is good to be thinking
about these things!

If you were to give the responsibility of stopping the GameOver Scene and restarting itself to the Game
Scene, how would you do it?

We will leave that implementation as an exercise for you!

One tip is that you can use this.scene.restart() from the Game Scene instead of calling stop()
and start() like we are doing now.

This Rocket Mouse game is almost completely playable!

Tommy Leung 76
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

In the next chapter, we will look at adding coins to collect and display a score based on the number of
collected coins.

Tommy Leung 77
Add Coins to Collect

Every game needs something to count for scoring purposes. We can use the total distance ran as a
score but collecting coins is more fun.

Calculating a score from coins collected and distance ran is another option that we’ll leave as an
exercise for you!

Load the Coin Image

Let’s start by loading the object_coin.png file in the Preloader Scene.

1 preload()
2 {
3 // load other assets...
4
5 this.load.image(TextureKeys.Coin, 'house/object_coin.png')
6 }

Remember to add the new enum value to TextureKeys like this:

1 enum TextureKeys
2 {
3 // other members...
4 Coin = 'coin'
5 }
6
7 export default TextureKeys

Creating Coins with a Group

We will be spawning a random number of coins periodically and the best way to handle that is to use a
Phaser Group.

A Group has built-in support for recycling GameObjects and we’ll want to do that instead of creating
new coins as we need them and destroying coins as they scroll off the screen.

78
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Frequent or large amount of object creation and destruction can result in performance problems on
less capable devices.

Let’s create a class property to start so that we can put the coin spawning logic in a Game Scene
method.

1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5 // other properties...
6 private coins!: Phaser.Physics.Arcade.StaticGroup
7
8 create()
9 {
10 // decorations and laser obstacle...
11
12 this.coins = this.physics.add.staticGroup()
13 this.spawnCoins()
14
15 // create Rocket Mouse and other code...
16 }
17
18 private spawnCoins()
19 {
20 // code to spawn coins
21 }
22
23 // other code...
24 }

Most of this should look familiar! We create a coins property using the ! which is TypeScript’s non-null
assertion operator. This lets us tell TypeScript that this.coins will be set even though it doesn’t
happen in the constructor.

Then we set this.coins to a Phaser.Physics.Arcade.StaticGroup and call an empty method


named spawnCoins().

We are using a StaticGroup so that the physics objects created by the group will be static. This way
the coins will float in the air and not be affected by gravity.

The spawnCoins() method is where we will add code to create coins randomly.

Tommy Leung 79
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 private spawnCoins()
2 {
3 // make sure all coins are inactive and hidden
4 this.coins.children.each(child => {
5 const coin = child as Phaser.Physics.Arcade.Sprite
6 this.coins.killAndHide(coin)
7 coin.body.enable = false
8 })
9
10 const scrollX = this.cameras.main.scrollX
11 const rightEdge = scrollX + this.scale.width
12
13 // start at 100 pixels past the right side of the screen
14 let x = rightEdge + 100
15
16 // random number from 1 - 20
17 const numCoins = Phaser.Math.Between(1, 20)
18
19 // the coins based on random number
20 for (let i = 0; i < numCoins; ++i)
21 {
22 const coin = this.coins.get(
23 x,
24 Phaser.Math.Between(100, this.scale.height - 100),
25 TextureKeys.Coin
26 ) as Phaser.Physics.Arcade.Sprite
27
28 // make sure coin is active and visible
29 coin.setVisible(true)
30 coin.setActive(true)
31
32 // enable and adjust physics body to be a circle
33 const body = coin.body as Phaser.Physics.Arcade.StaticBody
34 body.setCircle(body.width * 0.5)
35 body.enable = true
36
37 // move x a random amount
38 x += coin.width * 1.5
39 }
40 }

There’s a lot of code to unpack here but most of it is fairly straight forward.

First, we go through all the children of the coins group and make sure they are all not visible, not
active, and have a disabled physics body.

Then we pick a starting x position that is 100 pixels past the right edge of the screen.

A random number of coins is generated between 1 and 20 that we then use to create that number of
coins.

Tommy Leung 80
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Each coin is given a random y value that is within 100 pixels of the top and bottom of the screen. Then
the coin is set to be visible and active and given a circle physics body. We also make sure the body is
enabled.

Lastly, we add 1.5x the coin’s width to the x position so that the next coin will be spaced away from the
previous one.

Give this a try and you should see a bunch of randomly placed coins as Rocket Mouse runs forward.

Other Coin Formations

We won’t cover spawning pre-made coin formations because it is outside the scope of this book.

However, you can try implementing that yourself by creating different functions or methods that place
coins in specific positions to display a message like “Hello!” or some other pattern.

Then instead of picking random positions, you can pick a random function or method to use.

Collecting Coins

Coin collection is implemented in a similar fashion to colliding with the LaserObstacle except nothing
bad happens when you do!

First, we need to detect an overlap between Rocket Mouse and any coin like this:

1 // Game.ts
2
3 create()
4 {
5 // previous code...
6
7 // create overlap detection
8 this.physics.add.overlap(
9 this.coins,
10 mouse,
11 this.handleCollectCoin,
12 undefined,
13 this
14 )
15 }
16
17 // this method will be called when an overlap happens
18 private handleCollectCoin(
19 obj1: Phaser.GameObjects.GameObject,
20 obj2: Phaser.GameObjects.GameObject
21 )

Tommy Leung 81
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

22 {
23 // obj2 will be the coin
24 const coin = obj2 as Phaser.Physics.Arcade.Sprite
25
26 // use the group to hide it
27 this.coins.killAndHide(coin)
28
29 // and turn off the physics body
30 coin.body.enable = false
31 }

Adding code to detect an overlap should be familiar.

The handleCollectCoin() callback method will be called when an overlap occurs. At that time, we
set the coin to invisible and inactive by calling killAndHide() on the group and passing in the coin
that overlapped with Rocket Mouse.

The coin’s physics body is also disabled so that it doesn’t register more overlaps.

Give it a try and you’ll see that the coins disappear as Rocket Mouse’s collision box overlaps with the
coin.

Displaying a Score

Now, let’s keep track of how many coins we’ve collected and display it as a UI element at the top left of
the screen.

Let’s start by creating 2 class properties to hold the score count and a reference to the Text object that
will display the score.

1 export default class Game extends Phaser.Scene


2 {
3 // class proeprties...
4
5 private scoreLabel!: Phaser.GameObjects.Text
6 private score = 0
7
8 init()
9 {
10 this.score = 0
11 }
12
13 // other code...
14 }

Notice that we added an init() method which is called by Phaser when the Scene is started. It is
called before preload() and create().

Tommy Leung 82
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

We use it here to reset this.score to 0 so that we start fresh each time we play the game again.

Next, create the Text instance in the create() method:

1 create()
2 {
3 // previous code...
4
5 this.scoreLabel = this.add.text(10, 10, Score: ${this.score } , {
6 fontSize: '24px',
7 color: '#080808',
8 backgroundColor: '#F8E71C',
9 shadow: { fill: true, blur: 0, offsetY: 0 },
10 padding: { left: 15, right: 15, top: 10, bottom: 10 }
11 })
12 .setScrollFactor(0)
13 }

The scoreLabel is created in a similar way to what we did in the GameOver Scene. Recall that we
used the Text Styler for Phaser 3 web-based tool to generate the TextStyle config options.

Then we use setScrollFactor(0) to keep it in place even as the camera scrolls to follow Rocket
Mouse.

You’ll see a yellow rectangle with Score: 0 at the top left corner of the screen once the browser
reloads.

To finish up, we just need to add 1 to this.score each time a coin is collected and update the
scoreLabel with new text.

1 private handleCollectCoin(
2 obj1: Phaser.GameObjects.GameObject,
3 obj2: Phaser.GameObjects.GameObject
4 )
5 {
6 // previous code
7
8 // increment by 1
9 this.score += 1
10
11 // change the text with new score
12 this.scoreLabel.text = Score: ${this.score }
13 }

The code is pretty straight forward. We add 1 to this.score and then update this.scoreLabel.
text with the new score value.

Give it a try and watch the score count change as you collect coins!

Tommy Leung 83
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Note on Collision Box Sizes

You’ll notice that Rocket Mouse has a pretty big physics box. Bigger than his actual size and way bigger
than a size that is actually fun.

We can adjust the size of Rocket Mouse’s physics box using setSize on the physics body.

Let’s do this for Rocket Mouse by updating the setSize and setOffset code in the constructor of
RocketMouse like this:

1 export default class RocketMouse extends Phaser.GameObjects.Container


2 {
3 constructor(scene: Phaser.Scene, x: number, y: number)
4 {
5 // other code...
6
7 const body = this.body as Phaser.Physics.Arcade.Body
8
9 // use half width and 70% of height
10 body.setSize(this.mouse.width * 0.5, this.mouse.height * 0.7)
11
12 // adjust offset to match
13 body.setOffset(this.mouse.width * -0.3, -this.mouse.height +
15)
14 }
15 }

Make these changes and you’ll notice that Rocket Mouse starts by falling as his collision box is now
smaller.

We can fix this by adjusting the physic’s world bounds in the Game Scene like this:

1 create()
2 {
3 // other code...
4
5 // change "- 30" to "- 55"
6 this.physics.world.setBounds(
7 0, 0,
8 Number.MAX_SAFE_INTEGER, height - 55
9 )
10 }

Now give it a try and fly around collecting coins.

The way we sized the collision box accounts for the feet tucking in during the fall animation which will
make it more accurate for barely avoiding lasers during a fall.

You should also notice that the LaserObstacle has a larger collision box than it needs as well. Use

Tommy Leung 84
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

what we did here as a guide for shrinking that collision box.

Creating Coins Periodically

The last thing we need to do is to create coins periodically.

There are a few ways we can do this including using Phaser’s Clock with this.time.addEvent to
spawn new coins every X seconds.

We are going to use a much simpler approach and simply call this.spawnCoins() when we wrap
the second bookcase.

Make this change to the wrapBookcases() method:

1 private wrapBookcases()
2 {
3 // other code...
4
5 if (this.bookcase2.x + width < scrollX)
6 {
7 // move this.bookcase2...
8
9 // call spawnCoins()
10 this.spawnCoins()
11 }
12 }

Give it a try and you’ll see the coins appear once and then appear again but the second wave of coins is
not collectible!

Let’s fix this by adjusting the spawnCoins() method when we set the coin’s physics body to be a
circle.

1 private spawnCoins()
2 {
3 // other code...
4
5 for (let i = 0; i < numCoins; ++i)
6 {
7 // create a coin from the group
8
9 const body = coin.body as Phaser.Physics.Arcade.StaticBody
10 body.setCircle(body.width * 0.5)
11 body.enable = true
12
13 // update the body x, y position from the GameObject
14 body.updateFromGameObject()
15

Tommy Leung 85
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

16 x += coin.width * 1.5
17 }
18 }

Notice the call to body.updateFromGameObject() after the physics body is enabled.

This will move the physics body to the same position as to where the GameObject is visually dis-
played.

Try it out and you’ll see that the coins are collectible after they are respawned!

The last thing that we’ll cover in this book is to make the game truly infinite and avoid the problem of a
player being so good that they scroll reach the maximum size of a Number as we mentioned in Chapter
5.

We will tackle this in the next chapter!

Tommy Leung 86
To Infinity and Beyond

We talked about how our scrolling system is not truly infinite in Chapter 5 but let’s go a little deeper on
why that is.

The game certainly looks infinite right now and you can leave it running for hours and hours and have
no problem.

It is so close to infinite that, for all intents and purposes, you can call it infinite and no one will know
the difference. Except for you, anyone who reads the code, or someone really good at dodging laser
beams with a jetpack.

Where Does our World End?

In Chapter 5, we made the width of our physics world bounds Number.MAX_SAFE_INTEGER which
means Rocket Mouse will run into a wall there and continue running in place.

But the camera will be stationary and no more scrolling will occur.

The game would appear frozen in the eyes of the player as they are no longer moving forward.

One way to fix this is to teleport everything back to the beginning. This will create the illusion of an
infinite world within finite world bounds.

However, we don’t have to wait until we get to MAX_SAFE_INTEGER before teleporting Rocket Mouse,
the decorations, coins, and laser obstacle back to the beginning.

We can do it at a number like 2500 pixels and keep the player in that range the entire time they are
playing the game. If we can make it seamless then they’ll never know that they are on a treadmill!

Creating a Seamless Teleport

A teleport consists of moving all the GameObjects we’ve created backward by the same amount once
we’ve scrolled past a specific position.

87
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

This will create the illusion that everything is still moving forward together even though they all moved
backward together.

First, create a new class property in the Game Scene to hold a reference Rocket Mouse. We’ll need to
adjust his x position when a teleport happens.

1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5 // other properties...
6
7 private mouse!: RocketMouse
8
9 // other code...
10 }

Next, we should create a teleportBackwards() method in the Game Scene that handles when and
how a teleport should happen:

1 private teleportBackwards()
2 {
3 const scrollX = this.cameras.main.scrollX
4 const maxX = 2500
5
6 // perform a teleport once scrolled beyond 2500
7 if (scrollX > maxX)
8 {
9 // teleport the mouse and mousehole
10 this.mouse.x -= maxX
11 this.mouseHole.x -= maxX
12
13 // teleport each window
14 this.windows.forEach(win => {
15 win.x -= maxX
16 })
17
18 // teleport each bookcase
19 this.bookcases.forEach(bc => {
20 bc.x -= maxX
21 })
22
23 // teleport the laser
24 this.laserObstacle.x -= maxX
25 const laserBody = this.laserObstacle.body
26 as Phaser.Physics.Arcade.StaticBody
27
28 // as well as the laser physics body
29 laserBody.x -= maxX
30

Tommy Leung 88
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

31 // teleport any spawned coins


32 this.coins.children.each(child => {
33 const coin = child as Phaser.Physics.Arcade.Sprite
34 if (!coin.active)
35 {
36 return
37 }
38
39 coin.x -= maxX
40 const body = coin.body as Phaser.Physics.Arcade.StaticBody
41 body.updateFromGameObject()
42 })
43 }
44 }

There is a bit of code here but it is all pretty straight forward.

We decide on 2500 pixels as the maxX value for when we perform the teleport. Then we move all the
GameObjects we’ve created backward by subtracting maxX from their x position.

Lastly, call teleportBackwards() at the end of the update() method like this:

1 update(t: number, dt: number)


2 {
3 // previous code...
4
5 this.teleportBackwards()
6 }

Give it a try and you’ll see that everything will work as expected except for the background and some
coins depending on how many were spawned.

Rocket Mouse, the decorations, and the laser obstacle should teleport backward seamlessly. You’ll
know when a teleport happens when the background changes abruptly.

Teleporting the Background Seamlessly

We are not fully in control of how the background scrolls because we are using the TileSprite.

The background is scrolled using the camera’s scrollX value and that is automatically adjusted after
we teleport Rocket Mouse. All we control is the position of Rocket Mouse.

Subtracting maxX from this.background.tilePosition.x doesn’t appear to work even though


it seems to make sense!

One way to resolve this is to handle the background scrolling manually by creating 4 background
images that we loop to the front as soon as one scrolls off the screen.

Tommy Leung 89
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

But that feels like a lot of work to replicate functionality that the TileSprite gives us for free.

There’s another approach that we discovered through trial and error: teleport backward when scrolled
beyond a multiple of the background texture width.

The background image has a width of 340 pixels so can we multiply that by 7 and use 2380 instead of
2500.

Try it out by changing the maxX value in teleportBackwards():

1 private teleportBackwards()
2 {
3 const scrollX = this.cameras.main.scrollX
4
5 // change to 2380 from 2500
6 const maxX = 2380
7
8 // previous code...
9 }

Play the game for a bit and the background should teleport seamlessly along with the other
GameObjects.

Fixing Coin Teleportation

If you play the game long enough you’ll sometimes notice that uncollected coins will disappear suddenly
before they’ve scrolled off the screen.

Recall that we are spawning coins when we wrap the second bookcase to a new position but this can
happen while the previous set of spawned coins is still on screen depending on how many random
coins were spawned.

We can fix this by moving the call to this.spawnCoins() from wrapBookcases() to teleportBackwards
(). This gives us a predictable size of 2380 pixels that should be enough for 20 spawned coins.

Add this change by deleting the call to this.spawnCoins() in wrapBookcases() and add it to
teleportBackwards() like this:

Tommy Leung 90
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

1 private teleportBackwards()
2 {
3 const scrollX = this.cameras.main.scrollX
4 const maxX = 2380
5
6 if (scrollX > maxX)
7 {
8 // previous code...
9
10 // spawn coins here
11 this.spawnCoins()
12
13 // continue to perform teleport logic for coins
14 this.coins.children.each(child => {
15 const coin = child as Phaser.Physics.Arcade.Sprite
16 if (!coin.active)
17 {
18 return
19 }
20
21 coin.x -= maxX
22 const body = coin.body as Phaser.Physics.Arcade.StaticBody
23 body.updateFromGameObject()
24 })
25 }
26 }

Test the game out for a bit and everything should now be working as one would expect. It’s a real
infinite runner with some challenge!

Optional Animation Updates

Phaser 3.50 introduced the idea of animations that can be specific or local to individual sprites.

So far we’ve created global animations that can be used by any Sprite. But you’ll notice that they
are only used by Rocket Mouse. While there’s nothing wrong with keeping these animations global it
would be convenient to have everything together in the RocketMouse class.

Let’s start by moving the animations in the Preloader Scene into a createAnimations() method
in the RocketMouse class.

1 // imports...
2
3 export default class RocketMouse extends Phaser.GameObjects.Container
4 {
5 // other code...
6

Tommy Leung 91
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

7 private createAnimations()
8 {
9 this.mouse.anims.create({
10 key: AnimationKeys.RocketMouseRun,
11 frames: this.mouse.anims.generateFrameNames(TextureKeys.
RocketMouse, { start: 1, end: 4, prefix: '
rocketmouse_run', zeroPad: 2, suffix: '.png' }),
12 frameRate: 10,
13 repeat: -1
14 })
15
16 this.mouse.anims.create({
17 key: AnimationKeys.RocketMouseFall,
18 frames: [{
19 key: TextureKeys.RocketMouse,
20 frame: 'rocketmouse_fall01.png'
21 }]
22 })
23
24 this.mouse.anims.create({
25 key: AnimationKeys.RocketMouseFly,
26 frames: [{
27 key: TextureKeys.RocketMouse,
28 frame: 'rocketmouse_fly01.png'
29 }]
30 })
31
32 this.mouse.anims.create({
33 key: AnimationKeys.RocketMouseDead,
34 frames: this.mouse.anims.generateFrameNames(TextureKeys.
RocketMouse, { start: 1, end: 2, prefix: '
rocketmouse_dead', zeroPad: 2, suffix: '.png' }),
35 frameRate: 10
36 })
37
38 this.flames.anims.create({
39 key: AnimationKeys.RocketFlamesOn,
40 frames: this.flames.anims.generateFrameNames(TextureKeys.
RocketMouse, { start: 1, end: 2, prefix: 'flame', suffix
: '.png'}),
41 frameRate: 10,
42 repeat: -1
43 })
44 }
45 }

Notice that instead of this.anims.create() we are using this.mouse.anims.create(). This


will create animations just for the this.mouse instance. We are also using this.mouse.anims.
generateFrameNames() instead of just this.anims.generateFrameNames().

Tommy Leung 92
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Next, we have to call createAnimations() from the constructor() like this:

1 constructor(scene: Phaser.Scene, x: number, y: number)


2 {
3 super(scene, x, y)
4
5 this.mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
6 .setOrigin(0.5, 1)
7
8 this.flames = scene.add.sprite(-63, -15, TextureKeys.RocketMouse)
9
10 this.createAnimations()
11
12 this.mouse.play(AnimationKeys.RocketMouseRun)
13 this.flames.play(AnimationKeys.RocketFlamesOn)
14
15 // other code
16 }

The createAnimations() method needs to be called after this.mouse and this.flames are cre-
ated but before play() is called. The key is to split the chained method calls of setOrigin() and
play() so that we can create the sprites, then the animations, and then play the starting animations.

Remember to delete the animation code from Preloader as we no longer need it!

Give these new changes a try to make sure that everything is still be working.

You now have a functional infinite runner game created in Phaser 3 using TypeScript.

There’s still more you can do like adding sound effects and music as well as custom fonts for the UI.

We won’t be covering that in this book but check out the Ourcade Blog and YouTube Channel for more
content to help you add some finishing touches to your infinite runner!

Tommy Leung 93
Epilogue

Thank you for reading this book!

I hope it will give you a head start into making more scalable games with Phaser 3 and TypeScript.

Perhaps you’ve also learned some things related to game development that can be taken with you to
other frameworks and platforms.

The basic gameplay and mechanics of Rocket Mouse were modeled after Jetpack Joyride and there’s a
lot more you can add like vehicles and missiles.

There are many resources online for art assets and programming tutorials to help you add those
features.

We used Game Art Guppy in this book but there’s also Kenney.nl, Open Game Art, and Itch.io. Just do a
Google search for even more options!

There’s never been a better time or a world with more resources for making games.

Our aim at Ourcade is to create an approachable community for game development whether you are a
beginner, expert, or anything in between.

The process of making games should be pleasant and enjoyable at every skill level.

That’s why Ourcade believes in 5 core principles:

• Playfulness
• Friendliness
• Helpfulness
• Open-mindedness
• Optimism

Join us if you find yourself nodding in agreement!

Find us on Twitter @ourcadehq.

Our YouTube Channel strives to show and explain the process of game development without editing
away the problem solving.

94
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Github is where we share source code for anyone to read, learn, or steal. We don’t believe that pro-
gramming code should be protected like nuclear launch codes.

And finally the Ourcade blog and website is the central hub. You’ll find everything we’re teaching,
sharing, and building there.

Best of luck with your game making,

Tommy Leung (aka supertommy)

Florida, USA
May 2020, revised December 2021, January 2021

Tommy Leung 95
About the Author

Tommy Leung, aka supertommy, has been making games professionally since 2007. He is a self-taught
programmer with a business degree from Pace University.

He has worked on games for brands like Cartoon Network, Nickelodeon, LEGO, and more.

Before founding Ourcade, he was Senior Engineering Manager at Zynga, the makers of Words with
Friends and FarmVille, for a hit mobile game played by tens of millions of people.

Tommy has developed games in Flash, C/C++, Objective-C, Java, Unity/C# HTML5/JavaScript, and
more.

Other than game development, he also enjoys fullstack web development and native iOS develop-
ment.

He believes in continuous learning, self improvement, and giving back by helping others because he
would not be where he is without the generocity of those who came before him.

Tommy is also an avid weight lifter.

He resides in Florida, USA and has 2 greyhounds.

96
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade

Tommy Leung 97

You might also like