Phaser3 TypeScript
Phaser3 TypeScript
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
i
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
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
Tommy Leung ii
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
Epilogue 94
We will be going through 11 chapters of game development in Phaser 3 using TypeScript to create an
infinite runner like Jetpack Joyride.
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!
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.
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.
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.
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.
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.
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.
1 parcel --version
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:
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
You should see a black window with the Phaser logo bouncing around!
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!
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.
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.
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.
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.
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.
Let’s create a new Scene called Game in the scenes folder where HelloWorldScene.ts is.
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:
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.
After these changes, your game should look something like this:
Understanding Origin
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.
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.
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.
Tommy Leung 12
Adding Rocket Mouse
First, download the Rocket Mouse assets from Game Art Guppy here.
• 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.
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
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.
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
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.
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.
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 }
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
The core difference between an Image and a Sprite in Phaser is that sprites can play animations and
images cannot.
Animations are global and created by the AnimationManager. Once created, it can be used by any
Sprite in any Scene.
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.
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
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 }
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:
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.
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.
Tommy Leung 21
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
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.
Enums are a TypeScript language feature that can be replicated in JavaScript using constants. It can
also be found in other languages like C#.
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.
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.
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.
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.
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
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
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.
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 }
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.
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.
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 }
You should see him run off screen after your browser reloads.
Tommy Leung 27
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
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!
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.
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 background is scrolled based on the main camera’s scrollX value by passing it to this.
background.setTilePosition().
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.
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.
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.
First, we need to load the object_mousehole.png image in the Preloader Scene as we did for the
background.
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
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.
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:
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?
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 }
1 enum TextureKeys
2 {
3 // other values...
4
5 Window1 = 'window-1',
6 Window2 = 'window-2'
7 }
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().
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!
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.
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 }
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.
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...
Tommy Leung 39
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
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!
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
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.
Phaser 3 GameObjects do not have children. This means that you cannot attach an Image to another
Image or Sprite.
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
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.
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).
Tommy Leung 45
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
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(...).
You will get errors after making this change because the mouse variable no longer has a body prop-
erty.
Tommy Leung 46
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
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.
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.
Tommy Leung 48
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
Rocket Mouse should be running on the floor like before with a box around him after these changes.
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 }
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.
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:
Now it should look better! You are welcome to adjust these values if it doesn’t look right to you.
We have the flame animation working but that’s just the first part of getting the jetpack to work.
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
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.
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.
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.
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 }
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!
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.
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 }
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
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.
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.
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 }
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.
Let’s see our handiwork in action by adding a LaserObstacle to the Game Scene.
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:
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.
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.
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.
Let’s add a physics body to the LaserObstacle so that our jetpack-wearing hero can run into it and
get zapped.
Tommy Leung 62
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
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.
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.
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 }
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 }
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!
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.
Tommy Leung 67
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
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 }
Our first use of this mouseState property is to only perform the kill logic if Rocket Mouse is in the
RUNNING state.
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.
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
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.
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.
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.
Creating new Scenes should be fairly familiar by now. This is the third Scene we’ve made in this book.
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.
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.
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
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!
Before we can use the GameOver Scene, we need to add it to the list of scenes in Phaser’s GameConfig
in main.ts.
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:
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!
We will listen for the Space bar being pressed in the GameOver Scene and then restart the game when
it does.
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
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 })
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?
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.
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!
1 preload()
2 {
3 // load other assets...
4
5 this.load.image(TextureKeys.Coin, 'house/object_coin.png')
6 }
1 enum TextureKeys
2 {
3 // other members...
4 Coin = 'coin'
5 }
6
7 export default TextureKeys
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.
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.
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 }
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.
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.
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
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:
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 }
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
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.
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 }
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.
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.
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!
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
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:
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.
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.
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.
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.
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!
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 }
Tommy Leung 92
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
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
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.
• Playfulness
• Friendliness
• Helpfulness
• Open-mindedness
• Optimism
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.
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.
96
Infinite Runner in Phaser 3 with TypeScript © 2020 Ourcade
Tommy Leung 97