Uploaded by Salvatore Ventrone

Infinite Runner Phaser3 TypeScript

advertisement
Infinite Runner in Phaser 3 with
TypeScript
Tommy Leung
© 2020 Ourcade
Contents
Introduction
Why TypeScript? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
How to Use this Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Example Source Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1
2
2
Getting Started
Installing Node.js and NPM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Creating the project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Code Editing and Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
4
5
6
Repeating Background
Where to put static assets? .
Adjusting Game size . . . .
Create a new Game Scene .
Understanding Origin
Repeating with a TileSprite .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Adding Rocket Mouse
Creating a Sprite Sheet . . . . . . . . . .
Loading a Sprite Sheet . . . . . . . . . .
Note on Sprite Sheet vs Sprite Atlas
Creating a Sprite from an Atlas . . . . . .
Creating the Run Animation . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Preloader and Enums
Creating a Preloader . . . . . . . . . . . . .
Creating Animations in the Preloader . . . .
What are Enums? . . . . . . . . . . . . . . .
Using Enums for Texture Keys . . . . . . . .
Using Enums for Scene and Animation Keys
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
i
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
8
9
11
12
.
.
.
.
.
13
13
15
15
15
17
.
.
.
.
.
20
20
22
22
23
24
Infinite Runner in Phaser 3 with TypeScript
Running and Scrolling
Adding Physics to Rocket Mouse
Run with Velocity . . . . . . . .
Scrolling the Background . . .
Start Rocket Mouse on the Floor
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
© 2020 Ourcade
.
.
.
.
.
.
.
.
.
.
.
.
Decorating the House
Adding a Mouse Hole Decorations . . . . . .
Looping the Mouse Hole . . . . . . . . . . .
Let There be Windows! . . . . . . . . . . . .
Class it up with Bookcases . . . . . . . . . .
Avoid Overlapping Windows with Bookcases
Adding a Jetpack
Creating a RocketMouse Class . . . . .
Using RocketMouse in the Game Scene
Adding Physics to a Container . . . . .
Add Some Flames . . . . . . . . . . . .
Turn the Jetpack On and Off . . . . . .
Adding Thrust . . . . . . . . . . . . . .
Adding Fly and Fall Animations . . . .
Creating a Laser Obstacle
Preload the Laser Images . . . . .
Composing the Laser Obstacle . .
Add LaserObstacle to Game Scene
Looping the LaserObstacle . . . .
Making the Laser Dangerous . . .
Collide and Zap . . . . . . . . . .
Poor Man’s State Machine . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Game Over and Play Again
Create a GameOver Scene . . . . . . . . . . . . . . . . . . .
Show the Game Over Scene . . . . . . . . . . . . . . . . . .
Pressing Space to Play Again . . . . . . . . . . . . . . . . . .
Another Way to Show GameOver Scene . . . . . . . . . . . .
Should the Game Over Scene Restart the Game Scene?
Tommy Leung
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
26
26
27
28
30
.
.
.
.
.
31
31
33
34
37
40
.
.
.
.
.
.
.
44
44
46
46
49
51
53
54
.
.
.
.
.
.
.
58
58
59
60
61
62
65
68
.
.
.
.
.
72
72
74
75
76
76
ii
Infinite Runner in Phaser 3 with TypeScript
Add Coins to Collect
Load the Coin Image . . . .
Creating Coins with a Group
Other Coin Formations
Collecting Coins . . . . . . .
Displaying a Score . . . . .
Note on Collision Box Sizes .
Creating Coins Periodically .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
© 2020 Ourcade
.
.
.
.
.
.
.
To Infinity and Beyond
Where Does our World End? . . . . . . .
Creating a Seamless Teleport . . . . . .
Teleporting the Background Seamlessly
Fixing Coin Teleportation . . . . . . . . .
Optional Animation Updates . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
78
78
78
81
81
82
84
85
.
.
.
.
.
87
87
87
89
90
91
Epilogue
94
About the Author
96
Tommy Leung
iii
Introduction
Thank you for downloading this book!
We will be going through 11 chapters of game development in Phaser 3 using TypeScript to create an
infinite runner like Jetpack Joyride.
Our hero will even have a jetpack!
This book is intended for developers who have dabbled in Phaser 3 by making at least one game or by
going through the Infinite Jumper in Phaser 3 with Modern JavaScript book.
We won’t be going through the language basics of TypeScript but you can use the TypeScript Handbook
for that.
Instead, we will take you through a step-by-step process of making a Phaser 3 game using TypeScript
instead of JavaScript.
Phaser 3 comes with official TypeScript support but the TypeScript experience is sometimes wonky
and even confusing for those new to the framework or language.
There are certain situations that may be awkward or don’t fit cleanly with TypeScript expectations but
are completely normal in the greater JavaScript ecosystem.
We’ll show you ways around those issues while maintaining the static analysis and other features that
make TypeScript so useful and popular in larger codebases.
Why TypeScript?
TypeScript was first released in 2012 as an open-source project by Microsoft. It has slowly gained
popularity over the years and appears to be really taking off as of 2019.
The fact that more and more professional teams and open-source projects are using TypeScript is a
sure sign that we should be paying attention.
Phaser 4, currently in early development, is one of these modern projects being written with TypeScript.
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 others–including a future version of yourself!
If you’ve ever had the experience of opening up an old JavaScript project to make a change only to
spend hours trying to figure out what the data being passed around looks then TypeScript could be
your new best friend.
TypeScript’s use static types help solve this problem by providing information about what the data is or
how it is structured without having to rely on having comments or trusting that they are up to date.
There are more benefits to using TypeScript for making games in Phaser 3 but we won’t list them all.
The fact that you picked up this book probably means you are already sold on TypeScript so let’s start
using it!
How to Use this Book
This book is designed for you to read and follow along from start to finish.
It is not a reference book that you can easily jump around in.
Each chapter builds on the previous and the level of complexity increases as you progress.
The goal is to build a working game that you can feel comfortable modifying and adding features on
your own.
Example Source Code
You can download the full source code of the infinite runner game we’ll be making on Github at:
https://github.com/ourcade/infinite-runner-template-phaser3
The repository uses git-lfs so be sure to have it installed before you use git clone to pull down
the image assets as well.
Alternatively, you can just download a zip from the releases tab.
Tommy Leung
2
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
We intend to improve this book with updates. Feel free to let me know of any errors, unclear instructions,
or any other problems by sending an email to tommy@ourcade.co or letting us know on Twitter
@ourcadehq.
Now, let’s make a game, shall we?
Ourcade is a playful game development community for open-minded and optimistic learners and
developers. Visit us at http://ourcade.co
Tommy Leung
3
Getting Started
This book will go over making an infinite runner game like Jetpack Joyride called Rocket Mouse. The
goal is to keep our jetpack-wearing mouse alive for as long as possible collecting coins and avoiding
obstacles.
We recommend that you check out our Infinite Jumper in Phaser 3 with Modern JavaScript book if you
are new to Phaser 3 and web development.
This book will be more advanced and use common web development tools like Node.js, the Node
Package Manager (npm), and the command line.
As the title of the book suggests, we will be using TypeScript to help write code that is easier to reason
about and maintain over months or years.
Installing Node.js and NPM
The simplest way to get Node.js is to install it from https://nodejs.org.
There are other ways to install it as well but we won’t go into those options here.
If you already have Node.js and npm installed then you can skip this section. Check out this blog post
instead.
Once you get to the Node.js download page, select the version recommended for most users. At the
time of this writing it is the option on the left for v12.16.x.
The exact version does not matter for what we’ll be doing.
Download the installer and follow the on-screen instructions to install Node.js on your computer.
Then open a terminal or command prompt and run this command:
1
node --version
It will respond with a version of Node.js if installed properly. This should mean that the Node Package
Manager or npm is also installed.
Let’s check that by running this command:
4
Infinite Runner in Phaser 3 with TypeScript
1
© 2020 Ourcade
npm --version
This will also respond with a version. Note that the versions for Node.js and npm will not match each
other and that is to be expected.
Creating the project
We will be using the phaser3-typescript-parcel-template to bootstrap our project.
You can find it on Github here.
If you are familiar with git then feel free to clone it.
If not, you can download it as a zip by clicking the green “Clone or download” button.
Feel free to change the name of the folder from ‘phaser3-typescript-parcel-template’ to something like
‘rocket-mouse-game’.
This project template uses Parcel as the application bundler and development web server.
If you’ve heard of webpack or rollup then parcel is similar to those tools except it prides itself on
being zero-config to start and blazingly fast.
You’ll need to have Parcel installed so open a command line window and run this command:
1
npm install -g parcel-bundler
To confirm that Parcel was installed successfully, run this command:
1
parcel --version
It will respond with a version number like 1.12.4.
Now let’s get this project template ready for use! Open the project folder in the command line and run
the command:
1
npm install
This will install dependencies like Phaser 3 and a couple of Parcel plugins to automate some mundane
tasks.
Next, run this command to start the development server:
1
npm run start
You’ll know that the development server is ready when you see a message like “Built in 3.25s”.
There will also be a line above it that says:
Tommy Leung
5
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Server running at http://localhost:8000.
Open a new browser window and go to http://localhost:8000.
You should see a black window with the Phaser logo bouncing around!
Code Editing and Testing
This book assumes you are using Visual Studio Code as your code editor and a modern web browser
like Google Chrome.
If you don’t have an attachment to another editor like Atom, Brackets, or Webstorm then we highly
recommend you download and install VS Code here.
Now open the project in your code editor and we’ll start writing some code in the next chapter!
Tommy Leung
6
Repeating Background
The first thing we need with an infinite runner is background scenery that repeats forever.
We will be using assets from Game Art Guppy.
There’s a lot of different art assets that you can download from there for free.
We’ll be using the House 1 Background - Repeatable asset that comes with a laser obstacle, coins to
collect, and various decor.
There’s also a sleeping dog and cat but we won’t be using them in this book. You are welcome to find
creative uses for them!
Where to put static assets?
Our project template is set up to serve static files like images in a folder named public.
Create one at your project root on the same level as the src folder.
It should look like this:
1 rocket-mouse-game
2
o-- node_modules
3
o-- public // <-- here
4
o-- src
5
o-- scenes
6
-- HelloWorldScene.ts
7
-- index.html
8
-- main.ts
9
-- .eslintignore
10
-- .eslintrc.js
11
-- .gitignore
12
-- package.json
13
-- tsconfig.json
Then create a folder named house inside the public folder. This is where we will put all the background
and environment images.
The Game Art Guppy assets have a standard resolution and @2x high-resolution versions. We will be
using the standard resolution versions in this book.
7
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Copy those from the asset’s “Object” folder into house as well as the “bg_repeat_340x640.png” file
from the “Background” folder.
Your public folder should look like this:
1 rocket-mouse-game
2
o-- node_modules
3
o-- public
4
o-- house
5
-- bg_repeat_340x640.png
6
-- object_bookcase1.png
7
-- object_bookcase2.png
8
-- object_coin.png
9
-- object_laser_end.png
10
-- object_laser.png
11
-- object_mousehole.png
12
-- object_window1.png
13
-- object_window2.png
14
// other files...
Files and folders in public will be served from the root of our web server.
For example, you can load the bookcase at http://localhost:8000/house/object_bookcase1.png.
Try restarting the web server by terminating the process in the command line with ctrl + c and
running npm run start again if the image does not load.
If it still doesn’t work then double-check the directory structure to make sure it matches.
Adjusting Game size
You’ll notice that our background asset is 640 pixels tall but our game is currently 600 pixels tall. Let’s
adjust the height of our game to match.
Open main.ts and change the height property of the config variable from 600 to 640.
1
2
3
4
5
6
7
8
9
10
11
12
import Phaser from 'phaser'
import HelloWorldScene from './scenes/HelloWorldScene'
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 800,
height: 640,
// <-- here
// other code...
}
export default new Phaser.Game(config)
Tommy Leung
8
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Save main.js and your browser window should automatically reload with the updates. It might be
hard to tell that 40 extra pixels were added but you can confirm by right-clicking on the game and
selecting “Inspect”.
The DevTools window will show up and highlight the canvas element being used to render the game.
It should look like this:
1
<canvas width="800" height="640">
Create a new Game Scene
Let’s create a new Scene called Game in the scenes folder where HelloWorldScene.ts is.
Make a new file named Game.ts with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Phaser from 'phaser'
export default class Game extends Phaser.Scene
{
constructor()
{
super('game')
}
preload()
{
}
create()
{
}
}
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
1
2
3
4
5
6
7
8
9
10
© 2020 Ourcade
preload()
{
this.load.image('background', 'house/bg_repeat_340x640.png')
}
create()
{
this.add.image(0, 0, 'background')
.setOrigin(0, 0)
}
The preload() method now loads the bg_repeat_340x640.png file and assigns it to the key '
background'.
Then we create an image in create() using that key. We place it at (0, 0) and then use setOrigin
(0) to change the origin–it is also known as a pivot or anchor point–to the top left of the image.
By default, most GameObjects like images and sprites have an origin of (0.5, 0.5) which is the
middle of the object.
Next, we’ll need to add this Game Scene to the scene property of our GameConfig in main.ts to see it
in the browser.
Go to main.ts and makes these updates:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Phaser from 'phaser'
// we can delete this line
// import HelloWorldScene from './scenes/HelloWorldScene'
// add this line to import Game
import Game from './scenes/Game'
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 800,
height: 640,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 200 }
}
},
// replace HelloWorldScene with Game in the 'scene' property
scene: [Game]
}
export default new Phaser.Game(config)
Notice that we import Game at the top and then add it to the scene property as an Array.
Tommy Leung
10
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
The scene property does not have to be an Array but it often is because your game will likely have
more than one Scene.
Feel free to delete HelloWorldScene.ts as we will not be using it anymore.
After these changes, your game should look something like this:
Understanding Origin
Play around with setOrigin(x, y) to get a feel for how it works.
The origin designates where (0, 0) should be local to the image or sprite.
The values are from 0 - 1 where 0 is the left or top and 1 is the right or bottom for x and y respectively.
This is different than the Scene coordinates of (0, 0) for placing the background image in the game.
Tommy Leung
11
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Adjusting the origin of an image or sprite is useful to help us more easily reason about transforming
them.
For example, rotating an image is usually done with the origin set to (0.5, 0.5) because most things
spin from the center like a wheel.
It would take a lot more mental work to emulate a rotation from the center with an origin set to (0, 0).
You’d have to set the rotation and then figure out what the x and y offset should be for it to look
accurate.
Repeating with a TileSprite
Phaser has a built-in TileSprite that will conveniently repeat the same image over a given width.
We can achieve the same effect manually by creating 3 images and setting the x value to be next to the
previous image.
But why do that when Phaser can handle it for us?
Here’s how to make a TileSprite:
1 create()
2 {
3
// store the width and height of the game screen
4
const width = this.scale.width
5
const height = this.scale.height
6
7
// change this.add.image to this.add.tileSprite
8
// notice the changed parameters
9
this.add.tileSprite(0, 0, width, height, 'background')
10
.setOrigin(0)
11 }
This is fairly similar to adding an image except we use this.add.tileSprite and pass in width and
height to let the TileSprite know how much to repeat the background image.
The width and height values are retrieved from Phaser’s ScaleManager.
Now, your browser should reload and show you the background repeated across the game screen.
There should be no more black space.
Next, we will add our jetpack-wearing hero: Rocket Mouse!
Tommy Leung
12
Adding Rocket Mouse
First, download the Rocket Mouse assets from Game Art Guppy here.
Our intrepid hero will have these animations:
•
•
•
•
running
flying
falling
dead
He will be running whenever he is on the ground. Flying when the jetpack is engaged. Falling when
he’s in the air but no longer ascending. And dead when he runs into the deadly home security laser.
This must be a house owned by spies. . . or a supervillain.
Creating a Sprite Sheet
Animations usually use sprite sheets or atlases because it is more efficient to pick a different frame
from a single texture than to switch textures constantly.
The art did not come with a sprite sheet or atlas so we will have to make one.
We recommend using TexturePacker by CodeAndWeb.
TexturePacker’s free version will do everything we need.
There are other options as well but
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 “rocketmouse_animations” folder into TexturePacker’s Sprite panel on the left.
Note: the flying animation image at standard resolution is missing from the download. Go to this issue
from our Github repository for this project and grab rocketmouse_fly01.png.
In the Settings panel on the right side of TexturePacker select “Phaser (JSONHash)” for Framework.
Then set the Data file and Texture file paths to a characters folder inside this project’s public
folder.
13
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
We are using the file name rocket-mouse.
You will have to create this folder before being able to select it in TexturePacker. Make it in the same
way we did for the house assets.
The rest of the settings you can leave alone. This is what TexturePacker should look like:
You can also choose “Phaser 3” as the Framework if you have TexturePacker Pro. Both options will
work for this project.
Click the “Publish sprite sheet” button near the middle top of TexturePacker to generate the PNG and
JSON files required for Phaser.
Your project should look like this:
1 rocket-mouse-game
2
o-- node_modules
3
o-- public
4
o-- house
5
o-- characters // <-- the new folder
6
-- rocket-mouse.json
7
-- rocket-mouse.png
8
o-- src
9
// other files...
Tommy Leung
14
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Loading a Sprite Sheet
We can load the sprite sheet created with TexturePacker like this in the Game Scene:
1 preload()
2 {
3
// previous code to load background
4
this.load.image('background', 'house/bg_repeat_340x640.png')
5
6
// load as an atlas
7
this.load.atlas(
8
'rocket-mouse',
9
'characters/rocket-mouse.png',
10
'characters/rocket-mouse.json'
11
)
12 }
Lines 7 - 11 is how we load the sprite sheet. We are using multiple lines for better book formatting. You
can have it as a single line like this.load.image above it.
Note on Sprite Sheet vs Sprite Atlas
The terms sprite sheet and sprite atlas are often used interchangeably.
But you’ll notice that we are using this.load.atlas instead of this.load.spritesheet in the
example above.
A sprite sheet or sprite atlas is one image that contains many smaller images.
Phaser considers sprite sheets to be images where each frame is a fixed size and can be referenced
using a numerical index.
On the other hand, sprite atlases have frames of different sizes and are referenced by alphanumeric
names like file names.
The sprite sheet generated by TexturePacker fits the second type and that’s why we are using this.
load.atlas.
Creating a Sprite from an Atlas
We can create either an Image or a Sprite from an atlas. We’ll be using Sprite because Rocket
Mouse has animations.
Let’s make sure the atlas loaded properly by creating a Sprite of a flying Rocket Mouse.
Tommy Leung
15
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 create()
2 {
3
const width = this.scale.width
4
const height = this.scale.height
5
6
// previous code...
7
8
this.add.sprite(
9
width * 0.5,
// middle of screen
10
height * 0.5,
11
'rocket-mouse', // atlas key given in preload()
12
'rocketmouse_fly01.png'
13
)
14 }
Creating a Sprite is very similar to creating an Image or TileSprite.
We use this.add.sprite and then pass in an x and y value along with the key of the atlas and the
frame to use.
The key 'rocket-mouse' is the same value used to load the atlas in preload().
Then the frame name rocketmouse_fly01.png is from the JSON file. Open rocket-mouse.json to
see that the original file names of the individual images making up the atlas are used as the key for the
properties defining where in the atlas a particular image is.
1 {
2
3
4
5
6
7
8
9
10
11
12 }
// other keys above
"rocketmouse_fly01.png":
{
"frame": {"x":490,"y":0,"w":134,"h":126},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":134,"h":126},
"sourceSize": {"w":134,"h":126}
},
// other keys below
Save your changes and the browser should reload showing Rocket Mouse in the middle of the screen.
Tommy Leung
16
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Creating the Run Animation
The core difference between an Image and a Sprite in Phaser is that sprites can play animations and
images cannot.
So let’s make an animation to play!
Animations are global and created by the AnimationManager. Once created, it can be used by any
Sprite in any Scene.
Add this to the top of create():
Tommy Leung
17
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 create()
2 {
3
this.anims.create({
4
key: 'rocket-mouse-run',
// name of this animation
5
// helper to generate frames
6
frames: this.anims.generateFrameNames('rocket-mouse', {
7
start: 1,
8
end: 4,
9
prefix: 'rocketmouse_run',
10
zeroPad: 2,
11
suffix: '.png'
12
}),
13
frameRate: 10,
14
repeat: -1 // -1 to loop forever
15
})
16
17
// previous code...
18 }
The run animation is created by accessing the AnimationManager with this.animas.create and
passing in a configuration.
The configuration needs a key which is the name of the animation: 'rocket-mouse-run'.
Then frames needs an Array of frame definitions that we generate using this.anims.
generateFrameNames().
We can also manually create the frame definitions without the helper method like this:
1 this.anims.create({
2
frames: [
3
{ key: 'rocket-mouse',
4
{ key: 'rocket-mouse',
5
{ key: 'rocket-mouse',
6
{ key: 'rocket-mouse',
7
]
8
// other properties...
9 })
frame:
frame:
frame:
frame:
'rocketmouse_run01.png'
'rocketmouse_run02.png'
'rocketmouse_run03.png'
'rocketmouse_run04.png'
},
},
},
}
This is the same as what this.anims.generateFrameNames() will generate using the data it is
given.
The zeroPad property is not strictly necessary in this case because we only have 4 frames. Setting
prefix to rocketmouse_run0 would have also worked.
But zeroPad is necessary if the animation has more than 9 frames.
You can adjust the frameRate property to speed up or slow down the animation.
Lastly, repeat is set to -1 so that the animation will loop for as long as it is active.
Tommy Leung
18
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
To play this animation, call play('rocket-mouse-run') on the created Sprite:
1 create()
2 {
3
// other code...
4
5
this.add.sprite(
6
width * 0.5,
7
height * 0.5,
8
'rocket-mouse',
9
'rocketmouse_fly01.png'
10
)
11
.play('rocket-mouse-run')
12 }
Now, you should see a floating Rocket Mouse running in mid-air.
We haven’t written too much code yet but there are already a couple of things we see that will make
the code harder to read and more error-prone.
There will be at least 4 animations and creating them in the Game Scene’s create() method will create
clutter. Animations are global so we don’t need to create these animations in Game.
String literals for key names like 'rocket0mouse' will be used many times. Having to manually type
it out each time will lead to a greater chance of making a typo and introducing a bug.
In the next chapter, we will add a Preloader Scene and enums to avoid these problems!
Tommy Leung
19
Preloader and Enums
We mentioned in the last chapter that animations are global so we can create them outside of the Game
Scene.
This is also true of loaded assets like images and atlases. Once they are loaded, they can be accessed
by any Scene.
Those two things allow for a Preloader Scene to handle loading all assets and creating animations
when the game starts.
Creating a Preloader
Let’s start by creating a Preloader.ts file in the scenes folder with the following set up:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Phaser from 'phaser'
export default class Preloader extends Phaser.Scene
{
constructor()
{
super('preloader')
}
preload()
{
}
create()
{
}
}
Very simple and looks just like how we started the Game Scene a couple of chapters ago.
Next, let’s copy the code for preload() in the Game Scene over to the Preloader Scene.
20
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 preload()
2 {
3
this.load.image('background', 'house/bg_repeat_340x640.png')
4
5
this.load.atlas(
6
'rocket-mouse',
7
'characters/rocket-mouse.png',
8
'characters/rocket-mouse.json'
9
)
10 }
Phaser will only call create() after all the assets specified in preload() have been loaded.
This means we can use create() to know when we can go to the Game Scene.
We’ll use the SceneManager to change Scenes once the assets are loaded like this:
1 create()
2 {
3
this.scene.start('game')
4 }
You can delete the preload() method from the Game Scene since we won’t be needing it anymore.
Next, we need to add the Preloader Scene to the GameConfig in main.ts as we did for the Game
Scene earlier.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Phaser from 'phaser'
// import Preloader scene
import Preloader from './scenes/Preloader'
import Game from './scenes/Game'
const config: Phaser.Types.Core.GameConfig = {
// other config...
}
// add Preloader as the first element in the Array
scene: [Preloader, Game]
export default new Phaser.Game(config)
The first Scene in the Array given to the scene property will be automatically started by Phaser.
We want the Preloader Scene to start first so we add it to the beginning of the list.
Now, your browser should reload and show the same thing we had at the beginning of the chapter.
No visual changes mean we did this code refactor properly!
Tommy Leung
21
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Creating Animations in the Preloader
The last thing we’ll bring over to the Preloader Scene is the animations.
Copy the code that creates the rocket-mouse-run animation from the Game Scene and add it to the
create() method of Preloader.
1 create()
2 {
3
// copied from Game Scene
4
this.anims.create({
5
key: 'rocket-mouse-run',
6
frames: this.anims.generateFrameNames('rocket-mouse', {
7
start: 1,
8
end: 4,
9
prefix: 'rocketmouse_run',
10
zeroPad: 2,
11
suffix: '.png'
12
}),
13
frameRate: 10,
14
repeat: -1
15
})
16
17
// now start the Game Secne
18
this.scene.start('game')
19 }
Everything should still look the same once your browser reloads. Rocket Mouse is floating mid-air as
he runs in place.
What are Enums?
Enums are a TypeScript language feature that can be replicated in JavaScript using constants. It can
also be found in other languages like C#.
For example, an enum is an enumeration of values like this:
1 enum Days
2 {
3
Sunday,
4
Monday,
5
Tuesday,
6
Wednesday,
7
Thursday,
8
Friday,
9
Saturday
10 }
Tommy Leung
22
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
This is an example of an enum called Days with values that are each day of the week. Enum values are
numbers by default so Days.Sunday is equal to 0 and Days.Saturday is equal to 6.
In the modern world, enums can help us avoid typos as VS Code will autocomplete it for us, and
TypeScript will give us an error if we type something like Days.Frday since it was not defined.
This alone makes enums a useful feature but it also makes code more readable in situations where you
need maximum space savings like coding in the 1980s, for an embedded device, or sending network
data in a real-time multiplayer game.
It is easier for humans to read Days.Sunday than it is to parse out that 0 means Sunday within the
specific context.
We don’t have any limited space requirements but we can still benefit from type checking and autocompletion by using string values with enums.
Using Enums for Texture Keys
Let’s start by creating a new folder named consts in the src folder on the same level as scenes.
1 rocket-mouse-game
2
o-- node_modules
3
o-- public
4
o-- src
5
o-- consts // <-- create here
6
o-- scenes
7
// other files...
Then, create a file in consts named TextureKeys.ts with the following code:
1
2
3
4
5
6
7
enum TextureKeys
{
Background = 'background',
RocketMouse = 'rocket-mouse'
}
export default TextureKeys
This creates an enum called TextureKeys with 2 string values named Background and RocketMouse
.
We then export it as the default export on the last line. This is just like how we’ve been creating Scenes
when we use export default class Preloader in the declaration.
In this example, we specify the default export after declaring the enum.
Exporting is what allows us to use the import syntax later.
Tommy Leung
23
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Now, let’s go back to the Preloader Scene and replace the string literals when we load assets.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// import at top of Preloader.ts
import TextureKeys from '../consts/TextureKeys'
export default class Preloader extends Phaser.Scene
{
// other code...
preload()
{
this.load.image(
TextureKeys.Background, // <-- here
'house/bg_repeat_340x640.png'
)
}
}
this.load.atlas(
TextureKeys.RocketMouse,
// <-- and here
'characters/rocket-mouse.png',
'characters/rocket-mouse.json'
)
// create()...
This is pretty straight forward so go do the same for the keys used to create the background and Rocket
Mouse in the Game Scene and anywhere else these string literals are being used.
Using Enums for Scene and Animation Keys
Now let’s apply what we did with TextureKeys to Scene and Animation keys.
For Scenes, we are using the strings 'game' and 'preloader'. An enum would look like this:
1
2
3
4
5
6
7
enum SceneKeys
{
Preloader = 'preloader',
Game = 'game'
}
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 constructor 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
2
3
4
5
6
enum AnimationKeys
{
RocketMouseRun = 'rocket-mouse-run'
}
export default AnimationKeys
Then replace usages of 'rocket-mouse-run' in the Preloader and Game Scenes.
You couldn’t tell that we did any work in this chapter by looking at the game but our code is now much
better than when we first started!
Next, we will start having Rocket Mouse run along the floor!
Tommy Leung
25
Running and Scrolling
The key action in any infinite runner is running.
We will add that in this chapter but first, we need to make Rocket Mouse a physics sprite and add a
physics world bounds so that he doesn’t fall into the abyss.
Adding Physics to Rocket Mouse
Make these updates to the Game Scene:
1 create()
2 {
3
// other code ...
4
5
// change this.add.sprite to this.physics.add.sprite
6
// and store the sprite in a mouse variable
7
const mouse = this.physics.add.sprite(
8
width * 0.5,
9
height * 0.5,
10
TextureKeys.RocketMouse
11
'rocketmouse_fly01.png'
12
)
13
.play(AnimationKeys.RocketMouseRun)
14
15
const body = mouse.body as Phaser.Physics.Arcade.Body
16
body.setCollideWorldBounds(true)
17
18
this.physics.world.setBounds(
19
0, 0,
// x, y
20
Number.MAX_SAFE_INTEGER, height - 30
// width, height
21
)
22 }
Adding physics to Rocket Mouse is as easy as replacing this.add.sprite with this.physics.add
.sprite.
The created physics sprite is stored in the mouse variable that we use to access the physics body with
mouse.body.
26
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Notice that we create a local body variable to store mouse.body and then cast it as a Phaser.Physics
.Arcade.Body.
Type-casting is a concept in typed languages that lets the programmer tell the compiler that a certain
object should be considered a different or more specific type.
The body property of GameObjects can be a Phaser.Physics.Arcade.Body or Phaser.Physics
.Arcade.StaticBody. We use type-casting so that VS Code can continue to give us accurate autocomplete options.
Then we call body.setColliderWorldBounds(true) so that Rocket Mouse will collide with the
world bounds set on the next line.
Notice that we use Number.MAX_SAFE_INTEGER for the width. We are making an infinite runner but
computer memory is not infinite so numbers have a maximum size.
We will go over how to create the feeling of infiniteness from a finite world in a later chapter.
Lastly, we set height as the height of the game screen minus 30 pixels. This will make it look like Rocket
Mouse is running on the floor of the background.
Once your browser reloads, Rocket Mouse should appear running in mid-air, fall to the floor, and
continue running in place.
Run with Velocity
The player doesn’t get to the control when Rocket Mouse runs in an infinite runner. He is always running
so we can use setVelocityX() as soon as we create him.
1 create()
2 {
3
// other code...
4
5
const body = mouse.body as Phaser.Physics.Arcade.Body
6
body.setCollideWorldBounds(true)
7
8
// set the x velocity to 200
9
body.setVelocityX(200)
10
11
// set physics world bounds...
12 }
We add body.setVelocityX(200) to make Rocket Mouse move right as soon as he is created.
You should see him run off screen after your browser reloads.
Tommy Leung
27
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
So let’s follow him by calling startFollow() on the main camera.
1 create()
2 {
3
// other code...
4
5
this.cameras.main.startFollow(mouse)
6
this.cameras.main.setBounds(0, 0, Number.MAX_SAFE_INTEGER, height)
7 }
The first line tells the camera to follow Rocket Mouse as he moves around the world.
The second line sets the bounds for the camera so that it doesn’t move past the top or bottom of the
screen.
This will keep the environment within the screen bounds as Rocket Mouse falls to the floor or flies to
the ceiling with his jetpack.
You’ll notice that the background scrolls off the screen quickly leaving our mouse to run in the dark!
Scrolling the Background
Phaser’s TileSprite is built with scrolling backgrounds in mind so we only need to make a few
changes.
1 create()
2 {
3
// width and height defined here...
4
5
this.add.tileSprite(0, 0, width, height, TextureKeys.Background)
6
.setOrigin(0, 0)
7
.setScrollFactor(0, 0) // <-- keep from scrolling
8
9
// other code...
10 }
We added setScrollFactor(0, 0) to stop the background from being scrolled by the camera.
But now it just looks like Rocket Mouse is running in place so we need to add code that will update the
tilePosition property of the background TileSprite.
The camera’s scroll value is changing every frame so we’ll need to make updates to tilePosition in
the Scene’s update() method. And to do that we’ll need to store the background TileSprite as a
class property.
Tommy Leung
28
Infinite Runner in Phaser 3 with TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
© 2020 Ourcade
// imports...
export default class Game extends Phaser.Scene
{
// create the background class property
private background!: Phaser.GameObjects.TileSprite
// constructor...
create()
{
// width and height...
// store the TileSprite in this.background
this.background = this.add.tileSprite(
0, 0,
width, height,
TextureKeys.Background
)
.setOrigin(0, 0)
.setScrollFactor(0, 0)
}
}
// other code...
update(t: number dt: number)
{
// scroll the background
this.background.setTilePosition(this.cameras.main.scrollX)
}
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 constructor. Phaser 3’s design doesn’t let us create a TileSprite in the constructor so we are using the !
operator as a workaround.
Other alternatives are to check that this.background exists before using it or using the optional
chaining operator (?). We feel using the non-null assertion operator when declaring a class property is
the cleanest.
Next, we store the created TileSprite in this.background and use it in update().
The two paramters in the update() method are the total time elapsed since the game started (t) and
Tommy Leung
29
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
the time elapsed since the last frame (dt).
The background is scrolled based on the main camera’s scrollX value by passing it to this.
background.setTilePosition().
Now the game looks like Rocket Mouse is running in a house!
Start Rocket Mouse on the Floor
The last thing for this chapter is to start Rocket Mouse on the floor instead of in mid-air.
A trick to make it easier to position characters that walk or run is to set their origin point to the bottom
where their feet are.
Then we can very simply set their y value to wherever the top of the floor is.
1 create()
2 {
3
// other code...
4
5
const mouse = this.physics.add.sprite(
6
width * 0.5,
7
height - 30,
// set y to top of floor
8
TextureKeys.RocketMouse,
9
'rocketmouse_fly01.png'
10
)
11
.setOrigin(0.5, 1) // <-- set origin to feet
12
.play(AnimationKeys.RocketMouseRun)
13
14
// other code...
15 }
We set the origin to the bottom of Rocket Mouse’s feet by using setOrigin(0.5, 1).
Remember that (0, 0) is the top left and (1, 1) is the bottom right. Using (0.5, 1) sets it to the
bottom middle where the feet are.
We also set the y position value to be height - 30. This was defined as the floor when we set the
physics world bounds.
We’re making progress on this infinite runner!
In the next chapter, we will add in background decorations to spruce things up.
Tommy Leung
30
Decorating the House
Running infinitely in an empty house is pretty boring. The same flower patterned background for as
long as the eye can see.
Let’s make things more visually interesting by adding decorations.
The House 1 Background - Repeatable asset from Game Art Guppy includes a good variety that should
already be in your project’s public/house folder.
We’ll start by adding the mouse hole.
Adding a Mouse Hole Decorations
First, we need to load the object_mousehole.png image in the Preloader Scene as we did for the
background.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Phaser from 'phaser'
import TextureKeys from '../consts/TextureKeys'
// other imports...
export default class Preloader extends Phaser.Scene
{
// constructor
preload()
{
// the background
this.load.image(
TextureKeys.Background,
'house/bg_repeat_340x640.png'
)
// load the mouse hole image
// notice we are using TextureKeys.MouseHole
this.load.image(
TextureKeys.MouseHole,
'house/object_mousehole.png'
)
31
Infinite Runner in Phaser 3 with TypeScript
24
25
26
27
28
29 }
}
© 2020 Ourcade
// other code
// other code...
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
2
3
4
5
6
7
8
enum TextureKeys
{
Background = 'background',
MouseHole = 'mouse-hole',
// <-- like this
RocketMouse = 'rocket-mouse'
}
export default TextureKeys
Then in the Game Scene we can create an image using the mouse hole texture.
Add it after the background and before Rocket Mouse so that it will layer properly.
1 create()
2 {
3
// create background TileSprite...
4
5
this.add.image(
6
Phaser.Math.Between(900, 1500), // x value
7
501,
// y value
8
TextureKeys.MouseHole
9
)
10
11
// other code...
12 }
The use of this.add.image should be familiar by now but we added something new.
The value passed in as the x position uses Phaser.Math.Between to generate a random number
between 900 and 1500.
This will add some variety to where the mouse hole is placed each time the game starts.
Give it a test and you’ll see that it looks good but notice that it only appears once.
We can solve this by picking a new random position ahead of Rocket Mouse each time the mouse hole
passes the left side of the screen.
Tommy Leung
32
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Looping the Mouse Hole
First, we need to store the mouse hole Image in a class property so that we can check when it has
scrolled off the left side of the screen.
Create a private class property named mouseHole and set it to the mouse hole image in create()
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// imports...
export default class Game extends Phaser.Scene
{
// other properties
private mouseHole!: Phaser.GameObjects.Image
create()
{
// create background TileSprite...
this.mouseHole = this.add.image(
Phaser.Math.Between(900, 1500),
501,
TextureKeys.MouseHole
)
}
}
// other code...
// other code
Next, create a private method called wrapMouseHole(). It will determine when the mouseHole
scrolls off the left side of the screen and give it a new position ahead of Rocket Mouse.
1 private wrapMouseHole()
2 {
3
const scrollX = this.cameras.main.scrollX
4
const rightEdge = scrollX + this.scale.width
5
6
if (this.mouseHole.x + this.mouseHole.width < scrollX)
7
{
8
this.mouseHole.x = Phaser.Math.Between(
9
rightEdge + 100,
10
rightEdge + 1000
11
)
12
}
13 }
First, we create a variable scrollX that simply stores the main camera’s scrollX value. Then we
Tommy Leung
33
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
create a rightEdge variable that represents the right side of the screen.
It is determined by adding the width of the game screen to scrollX.
Then to check when the mouse hole has scrolled off the left side of the screen, we check that this.
mouseHole.x plus its width is less than scrollX.
When that is true, we give this.mouseHole.x a new random value that is at least 100 pixels past the
right side of the screen and no further than 1,000 pixels.
Last step is to call this.wrapMouseHole() from the update() method like this:
1 update(t: number, dt: number)
2 {
3
this.wrapMouseHole()
4
5
// other code...
6 }
Watch Rocket Mouse run after your browser reloads and you’ll see the mouse hole show up repeatedly
at different intervals.
The environment is now much less dull putting aside the many mouse holes in this house–maybe call
pest control?
Let There be Windows!
This house is more like an abandoned warehouse with no windows and a rodent problem than a
home.
We’ll just have to live with the latter but we can add windows in a similar fashion to the mouse hole.
First, load the object_window1.png and object_window2.png files in the Preloader Scene like
we did before.
1 preload()
2 {
3
// other images
4
// load mouse holes
5
this.load.image(
6
TextureKeys.MouseHole,
7
'house/object_mousehole.png'
8
)
9
10
// load windows
11
this.load.image(TextureKeys.Window1, 'house/object_window1.png')
12
this.load.image(TextureKeys.Window2, 'house/object_window2.png')
Tommy Leung
34
Infinite Runner in Phaser 3 with TypeScript
13
14
15 }
© 2020 Ourcade
// other code
Be sure to add the new enum values to TextureKeys as well.
1 enum TextureKeys
2 {
3
// other values...
4
5
Window1 = 'window-1',
6
Window2 = 'window-2'
7 }
Next, we can create the images in the Game Scene.
We know that we’ll need to store them in class properties for the wrapping logic as we did for the
mouse hole so add those as well.
1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5
// other properties...
6
7
private window1!: Phaser.GameObjects.Image
8
private window2!: Phaser.GameObjects.Image
9
10
// constructor...
11
12
create()
13
{
14
// create background and mouse hole...
15
16
this.window1 = this.add.image(
17
Phaser.Math.Between(900, 1300),
18
200,
19
TextureKeys.Window1
20
)
21
22
this.window2 = this.add.image(
23
Phaser.Math.Between(1600, 2000),
24
200,
25
TextureKeys.Window2
26
)
27
28
// other code...
29
}
30
31
// other code...
Tommy Leung
35
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
This code should look familiar! It is basically the same as the code for the mouse hole.
You’ll see that it also has the same problem where the windows will only show up once.
Let’s apply the same technique we used to wrap and repeat the mouse hole for these windows by
creating a wrapWindows() method:
1 private wrapWindows()
2 {
3
const scrollX = this.cameras.main.scrollX
4
const rightEdge = scrollX + this.scale.width
5
6
// multiply by 2 to add some more padding
7
let width = this.window1.width * 2
8
if (this.window1.x + width < scrollX)
9
{
10
this.window1.x = Phaser.Math.Between(
11
rightEdge + width,
12
rightEdge + width + 800
13
)
14
}
15
16
width = this.window2.width
17
if (this.window2.x + width < scrollX)
18
{
19
this.window2.x = Phaser.Math.Between(
20
this.window1.x + width,
21
this.window1.x + width + 800
22
)
23
}
24 }
The concepts here are the same as for the mouse hole except we have 2 windows and they should not
overlap each other. We ensure that by using this.window1.x as the starting value to generate the
random x position for this.window2.
Notice that rightEdge is used for this.window1 like the mouse hole but then this.window1.x is
used in place of rightEdge for this.window2.x.
Remember to call this.wrapWindows() from the update() method like we did for this.
wrapMouseHole().
1 update(t: number, dt: number)
2 {
3
// for mouse holes
4
this.wrapMouseHole()
5
6
// add for windows
7
this.wrapWindows()
8
Tommy Leung
36
Infinite Runner in Phaser 3 with TypeScript
9
10
}
© 2020 Ourcade
// other code...
Now our jetpack-wearing rodent can see the sights on his infinite run!
Class it up with Bookcases
We’ve got 1 more set of decorations and they are 2 bookcases. One tall and one short.
Let’s start by adding them in the same way we added the windows. There were 2 windows and here we
have 2 bookcases. They need to wrap around in the same way and avoid overlapping each other.
We know how to handle that!
First, load the two bookcase PNG files in the Preloader Scene.
1 preload()
2 {
3
// other images
4
5
// the previous windows
6
this.load.image(TextureKeys.Window1, 'house/object_window1.png')
7
this.load.image(TextureKeys.Window2, 'house/object_window2.png')
8
9
// load the bookcases
10
this.load.image(TextureKeys.Bookcase1, 'house/object_bookcase1.png'
)
11
this.load.image(TextureKeys.Bookcase2, 'house/object_bookcase2.png'
)
12
13
// other code...
14 }
Remember to add the new enum values to TextureKeys!
Next, we can add them to the Game Scene with class properties and random positions.
Tommy Leung
37
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 // imports...
2
3 export default class Game extends Phaser.Scene
4 {
5
// other properties...
6
7
private bookcase1!: Phaser.GameObjects.Image
8
private bookcase2!: Phaser.GameObjects.Image
9
10
// constructor...
11
12
create()
13
{
14
// window and other decorations...
15
16
this.bookcase1 = this.add.image(
17
Phaser.Math.Between(2200, 2700),
18
580,
19
TextureKeys.Bookcase1
20
)
21
.setOrigin(0.5, 1)
22
23
this.bookcase2 = this.add.image(
24
Phaser.Math.Between(2900, 3400),
25
580,
26
TextureKeys.Bookcase2
27
)
28
.setOrigin(0.5, 1)
29
30
// other code...
31
}
32
33
// other code...
This all looks just like the code we used for windows in the last section.
Wrapping the bookcases back around to the front should be the same too so let’s add a wrapBookcases
() method:
Tommy Leung
38
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 private wrapBookcases()
2 {
3
const scrollX = this.cameras.main.scrollX
4
const rightEdge = scrollX + this.scale.width
5
6
let width = this.bookcase1.width * 2
7
if (this.bookcase1.x + width < scrollX)
8
{
9
this.bookcase1.x = Phaser.Math.Between(
10
rightEdge + width,
11
rightEdge + width + 800
12
)
13
}
14
15
width = this.bookcase2.width
16
if (this.bookcase2.x + width < scrollX)
17
{
18
this.bookcase2.x = Phaser.Math.Between(
19
this.bookcase1.x + width,
20
this.bookcase1.x + width + 800
21
)
22
}
23 }
And lastly, add a call to this.wrapBookcases() to the update() method like we’ve done before.
1 update(t: number, dt: number)
2 {
3
// wrapping other decorations...
4
this.wrapMouseHole()
5
this.wrapWindows()
6
7
// wrap bookcases
8
this.wrapBookcases()
9
10
// other code...
11 }
You’ll notice that the bookcases behave exactly like the windows except... now you get instances when
a bookcase overlaps a window!
Maybe that’s fine? Some people like books more than sunlight and do block their windows with
bookcases...
But if we wanted to solve this how would we do it?
Tommy Leung
39
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Avoid Overlapping Windows with Bookcases
There are many ways to solve this problem and we are going to use a simple method that has the
side-benefit of creating more environmental variety.
What we’ll do is check if a window or bookcase’s x position overlaps with an existing bookcase or
window and then hide that window or bookcase if there is an overlap.
First, we’ll need an array of bookcases and windows as class properties like this:
1
2
3
4
5
6
7
8
9
10
11
// imports...
export default class Game extends Phaser.Scene
{
// other properties..
private bookcases: Phaser.GameObjects.Image[] = []
private windows: Phaser.GameObjects.Image[] = []
}
// other code...
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
27
28
29
30
31
32
33
34
35
36
37
38 }
© 2020 Ourcade
this.bookcase2 = this.add.image(
Phaser.Math.Between(2900, 3400),
580,
TextureKeys.Bookcase2
)
.setOrigin(0.5, 1)
// create a new array with the 2 bookcases
this.bookcases = [this.bookcase1, this.bookcase2]
// other code...
Now, each time we move a window in wrapWindows() we need to check for an overlap like this:
1 private wrapWindows()
2 {
3
// other code...
4
5
let width = this.window1.width * 2
6
if (this.window1.x + width < scrollX)
7
{
8
this.window1.x = // pick random position
9
10
// use find() to look for a bookcase that overlaps
11
// with the new window position
12
const overlap = this.bookcases.find(bc => {
13
return Math.abs(this.window1.x - bc.x) <= this.window1.
width
14
})
15
16
// then set visible to true if there is no overlap
17
// false if there is an overlap
18
this.window1.visible = !overlap
19
}
20
21
width = this.window2.width
22
if (this.window2.x + width < scrollX)
23
{
24
this.window2.x = // pick random position
25
26
// do the same thing for window2
27
const overlap = this.bookcases.find(bc => {
28
return Math.abs(this.window2.x - bc.x) <= this.window2.
width
29
})
30
31
this.window2.visible = !overlap
32
}
33 }
Tommy Leung
41
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
The code to pay attention to is the use of this.bookcases.find() to find a bookcase that overlaps
with a window.
We determine overlap using a simple calculation of distance along the x-axis. Any distance smaller
than the width of the window is considered an overlap.
Then we do the same logic for this.window2.
Now let’s do the same thing for bookcases except we will check for overlaps against windows.
1 private wrapBookcases()
2 {
3
// other code...
4
5
let width = this.bookcase1.width * 2
6
if (this.bookcase1.x + width < scrollX)
7
{
8
this.bookcase1.x = // pick random position
9
10
// use find() to look for a window that overlaps
11
// with the new bookcase position
12
const overlap = this.windows.find(win => {
13
return Math.abs(this.bookcase1.x - win.x) <= win.width
14
})
15
16
// then set visible to true if there is no overlap
17
// false if there is an overlap
18
this.bookcase1.visible = !overlap
19
}
20
21
width = this.bookcase2.width
22
if (this.bookcase2.x + width < scrollX)
23
{
24
this.bookcase2.x = // pick random position
25
26
// do the same as we did above for bookcase2
27
const overlap = this.windows.find(win => {
28
return Math.abs(this.bookcase2.x - win.x) <= win.width
29
})
30
31
this.bookcase2.visible = !overlap
32
}
33 }
The code is almost the same except we check the list of windows instead of the list of bookcases. We
still use the window’s width because it is larger than the bookcase’s width.
This code can be made more generic to reduce some of the repetition but we will leave it like this for
simplicity.
Watch Rocket Mouse run for a while and you should see that the windows and bookcases will never
Tommy Leung
42
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
overlap each other. They simply become hidden when they do!
The mouse hole will get overlapped by the bookcase but we are okay with that.
If you don’t want the bookcase to cover the mouse hole then use the techniques we’ve learned in this
chapter to come up with a solution!
Game development is problem-solving.
Our game is looking pretty good with all the new decor!
In the next chapter, we will implement the most exciting part of this game: the jetpack.
Tommy Leung
43
Adding a Jetpack
What’s more fun than flying around with a jetpack?
The technology isn’t quite available to us in real life, yet, but we can do anything in a video game!
Adding jetpack functionality to Rocket Mouse requires turning on a flame animation and applying
some upward thrust in the physics engine.
Let’s tackle the first part by creating a RocketMouse class that we can attach a flame animation to.
Creating a RocketMouse Class
Phaser 3 GameObjects do not have children. This means that you cannot attach an Image to another
Image or Sprite.
Unless you are using a Container.
We will need to attach a flame animation as a child of Rocket Mouse so let’s subclass Phaser.
GameObjects.Container in our RocketMouse class.
Create a new folder named game in the src folder on the same level as scenes. Then create a
RocketMouse.ts file in the new game folder.
1 rocket-mouse-game
2
o-- node_modules
3
o-- public
4
o-- src
5
o-- consts
6
o-- game
// <-- new folder
7
-- RocketMouse.ts
// <-- new file
8
o-- scenes
9
// other files...
44
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Then add this code to RocketMouse.ts:
1
2
3
4
5
6
7
8
9
import Phaser from 'phaser'
export default class RocketMouse extends Phaser.GameObjects.Container
{
constructor(scene: Phaser.Scene, x: number, y: number)
{
super(scene, x, y)
}
}
This should look somewhat familiar as it is similar to creating new Scenes except we subclass Phaser
.GameObjects.Container instead of Phaser.Scene.
A Container holds children so let’s add a Rocket Mouse sprite as we did in Game. The difference is
that we don’t need to add a physics sprite. We’ll add physics to the Container instead.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Phaser from 'phaser'
// import these
import TextureKeys from '../consts/TextureKeys'
import AnimationKeys from '../consts/AnimationKeys'
export default class RocketMouse extends Phaser.GameObjects.Container
{
constructor(scene: Phaser.Scene, x: number, y: number)
{
super(scene, x, y)
// create the Rocket Mouse sprite
const mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
.setOrigin(0.5, 1)
.play(AnimationKeys.RocketMouseRun)
// add as child of Container
this.add(mouse)
}
}
We create a Sprite in the constructor similar to how we did it in the Game Scene. Then we add it to
the Container with this.add(mouse).
Now, let’s try using RocketMouse in the Game Scene.
Tommy Leung
45
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Using RocketMouse in the Game Scene
We will replace the call to this.physics.add.sprite() in create() by creating a new
RocketMouse instance.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// import RocketMouse
import RocketMouse from '../game/RocketMouse'
// then in create()...
create()
{
// decorations...
// remove line with const mouse = this.physics.add.sprite(...)
// add new RocketMouse
const mouse = new RocketMouse(this, width * 0.5, height - 30)
this.add.existing(mouse)
// error happens here
const body = mouse.body as Phaser.Physics.Arcade.Body
body.setCollideWorldBounds(true)
body.setVelocityX(200)
}
// other code...
We removed the previous way of creating a physics sprite that represented Rocket Mouse and replaced
it with new RocketMouse(...).
Then we use this.add.existing(mouse) to add the RocketMouse instance to the Scene.
You will get errors after making this change because the mouse variable no longer has a body property.
Let’s fix that by adding a physics body to RocketMouse.
Adding Physics to a Container
Go back to RocketMouse.ts and add this line to the constructor:
Tommy Leung
46
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 constructor(scene: Phaser.Scene, x: number, y: number)
2 {
3
super(scene, x, y)
4
5
const mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
6
.setOrigin(0.5, 1)
7
.play(AnimationKeys.RocketMouseRun)
8
9
this.add(mouse)
10
11
// add a physics body
12
scene.physics.add.existing(this)
13 }
You’ll notice that Rocket Mouse now appears higher in the game. Let’s see what’s going on by turning
on physics debug draw.
Go to main.ts and add debug: true to the physics config:
1 const config: Phaser.Types.Core.GameConfig = {
2
// other config...
3
physics: {
4
default: 'arcade',
5
arcade: {
6
gravity: { y: 200 },
7
debug: true
// <-- turn on debug draw
8
}
9
}
10 }
Now you should see that it looks like Rocket Mouse is standing on a box!
Tommy Leung
47
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
We will need to adjust the size and offset of the physics body to match where the Rocket Mouse sprite
is.
The top left corner of the box and the point by Rocket Mouse’s feet is the origin of the Container.
Recall that we set the Rocket Mouse sprite origin to (0.5, 1) which is the bottom center.
This means we will need to offset the physics body by the negative height and negative half-width of
Rocket Mouse.
We can do it like this:
Tommy Leung
48
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 constructor(scene: Phaser.Scene, x: number, y: number)
2 {
3
super(scene, x, y)
4
5
const mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
6
.setOrigin(0.5, 1)
7
.play(AnimationKeys.RocketMouseRun)
8
9
this.add(mouse)
10
11
scene.physics.add.existing(this)
12
13
// adjust physics body size and offset
14
const body = this.body as Phaser.Physics.Arcade.Body
15
body.setSize(mouse.width, mouse.height)
16
body.setOffset(mouse.width * -0.5, -mouse.height)
17 }
Rocket Mouse should be running on the floor like before with a box around him after these changes.
Add Some Flames
Now, we can add some flames for the jetpack. The flames are animated by switching between two
different images.
You can see them in the rocket-mouse.png file that we created using TexturePacker in Chapter 3.
Let’s create a flames animation in Preloader like we did for Rocket Mouse’s running animation so
that we can use it for the jetpack.
1 // Preloader.ts
2
3 create()
4 {
5
// previous run animation
6
this.anims.create({
7
key: AnimationKeys.RocketMouseRun,
8
frames: this.anims.generateFrameNames(TextureKeys.RocketMouse,
{ start: 1, end: 4, prefix: 'rocketmouse_run', zeroPad: 2,
suffix: '.png' }),
9
frameRate: 10,
10
repeat: -1
11
})
12
13
// create the flames animation
14
this.anims.create({
15
key: AnimationKeys.RocketFlamesOn,
Tommy Leung
49
Infinite Runner in Phaser 3 with TypeScript
16
17
18
19
20
21
22 }
})
© 2020 Ourcade
frames: this.anims.generateFrameNames(TextureKeys.RocketMouse,
{ start: 1, end: 2, prefix: 'flame', suffix: '.png'}),
frameRate: 10,
repeat: -1
this.scene.start(SceneKeys.Game)
Notice that we are using AnimationKeys.RocketFlameOn so be sure to add that to AnimationKeys
.ts like this:
1
2
3
4
5
6
7
enum AnimationKeys
{
RocketMouseRun = 'rocket-mouse-run',
RocketFlamesOn = 'rocket-flames-on'
}
export default AnimationKeys
Now we can go back to RocketMouse.ts and create a flame sprite and play the RocketFlamesOn
animation.
Add this code to the constructor of RocketMouse:
1 constructor(scene: Phaser.Scene, x: number, y: number)
2 {
3
// create mouse sprite...
4
5
// create the flames and play the animation
6
const flames = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
7
.play(AnimationKeys.RocketFlamesOn)
8
9
// add this first so it is under the mouse sprite
10
this.add(flames)
11
this.add(mouse)
12
13
// other code...
14 }
Creating the flames sprite and playing an animation should look familiar. It is the same as what we did
for the mouse sprite.
Pay attention to the order in which we added the flames and mouse sprites to the Container. The
flames are added first so that it layers behind the mouse.
This will give us the intended effect after we position the flames under the jetpack.
After the browser reloads you’ll see the flame animation playing by the feet of Rocket Mouse.
Tommy Leung
50
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
We can fix this by changing the position of the flame from (0, 0) to (-63, -15) like this:
1
const flames = scene.add.sprite(-63, -15, TextureKeys.RocketMouse)
Now it should look better! You are welcome to adjust these values if it doesn’t look right to you.
Turn the Jetpack On and Off
We have the flame animation working but that’s just the first part of getting the jetpack to work.
The second part is to apply thrust.
But before we do that, let’s add a method to turn the jetpack on or off. We’ll need it when the player
wants to go back to the ground.
First, create a new flames class property so that we can access the flames sprite from a method.
Tommy Leung
51
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 export default class RocketMouse extends Phaser.GameObjects.Container
2 {
3
private flames: Phaser.GameObjects.Sprite
4
5
constructor(scene: Phaser.Scene, x: number, y: number)
6
{
7
// create mouse...
8
9
this.flames = scene.add.sprite(-63, -15, TextureKeys.
RocketMouse)
10
.play(AnimationKeys.RocketFlamesOn)
11
12
this.add(this.flames)
13
14
// other code...
15
}
All we did was replace the local const flames with the class property this.flames.
Next, create a method called enableJetpack(enabled: boolean) with the following implementation:
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 constructor.
1 constructor(scene: Phaser.Scene, x: number, y: number)
2 {
3
// create mouse...
4
5
this.flames = scene.add.sprite(-63, -15, TextureKeys.RocketMouse)
6
.play(AnimationKeys.RocketFlamesOn)
7
8
this.enableJetpack(false)
9
10
// other code...
11 }
Rocket Mouse should now be running on the floor without the jetpack turned on!
Tommy Leung
52
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Adding Thrust
We will allow the player to turn on the jetpack when they press the Space bar.
That means we’ll set the upwards acceleration of Rocket Mouse and enable the jetpack flames animation
when the Space bar is down.
For simplicity, we’ll access the Space bar using Phaser’s CursorKeys.
Let’s start by creating a cursors class property to hold an instance of CursorKeys:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// imports...
export default class RocketMouse extends Phaser.GameObjects.Container
{
// other properties...
// create the cursors property
private cursors: Phaser.Types.Input.Keyboard.CursorKeys
constructor(scene: Phaser.Scene, x: number, y: number)
{
// previous code...
}
}
// get a CursorKeys instance
this.cursors = scene.input.keyboard.createCursorKeys()
Phaser’s CursorKeys is a convenient way to get access to the 4 arrow keys and the Space bar.
With this we can check if the Space bar is pressed in a preUpdate() method.
A Container does not normally implement a preUpdate() method but it will get called if we create
one.
Note: we can create a more traditional update() method and then call it from the Game Scene
instead. But for simplicity, we will just use preUpdate(). You are welcome to use update()
instead.
Tommy Leung
53
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 preUpdate()
2 {
3
const body = this.body as Phaser.Physics.Arcade.Body
4
5
// check is Space bar is down
6
if (this.cursors.space?.isDown)
7
{
8
// set y acceleration to -600 if so
9
body.setAccelerationY(-600)
10
this.enableJetpack(true)
11
}
12
else
13
{
14
// turn off acceleration otherwise
15
body.setAccelerationY(0)
16
this.enableJetpack(false)
17
}
18 }
In preUpdate() we set the y acceleration of Rocket Mouse to -600 if the Space bar is down. Then we
turn off the acceleration when the Space bar is no longer pressed.
We also enable and disable the jetpack flames animation at the appropriate times.
Try it out and you should see the jetpack turn on when you press and hold the Space bar as Rocket
Mouse takes flight.
He will still be running because we have not created his fly or fall animations yet.
Adding Fly and Fall Animations
Let’s put some finishing touches on our jetpack functionality by adding the fly and fall animations for
Rocket Mouse.
These animations will only be 1 frame long so they are animations in concept only but it sets you up to
add more complex animations with flapping ears or wiggling feet.
We can create these two animations in the Preloader Scene as we’ve done before:
Tommy Leung
54
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 // Preloader.ts
2
3 create()
4 {
5
// previous run animation
6
this.anims.create({
7
key: AnimationKeys.RocketMouseRun,
8
frames: this.anims.generateFrameNames(TextureKeys.RocketMouse,
{ start: 1, end: 4, prefix: 'rocketmouse_run', zeroPad: 2,
suffix: '.png' }),
9
frameRate: 10,
10
repeat: -1
11
})
12
13
// new fall animation
14
this.anims.create({
15
key: AnimationKeys.RocketMouseFall,
16
frames: [{
17
key: TextureKeys.RocketMouse,
18
frame: 'rocketmouse_fall01.png'
19
}]
20
})
21
22
// new fly animation
23
this.anims.create({
24
key: AnimationKeys.RocketMouseFly,
25
frames: [{
26
key: TextureKeys.RocketMouse,
27
frame: 'rocketmouse_fly01.png'
28
}]
29
})
30
31
// other animations...
32 }
These animations are created in the same way we explained in Chapter 3. Refer back to that chapter if
you are unsure of what we did.
Remember to add the new RocketMouseFall and RocketMouseFly enum values to AnimationKeys
.ts.
Now, we can play the fly animation when we enable the jetpack and then play the fall animation when
Rocket Mouse’s velocity is greater than 0.
First, head back to RocketMouse.ts and create a new class property called mouse to store the reference to the mouse sprite.
Tommy Leung
55
Infinite Runner in Phaser 3 with TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
© 2020 Ourcade
// imports...
export default class RocketMouse extends Phaser.GameObjects.Container
{
// other properties...
private mouse: Phaser.GameObjects.Sprite
constructor(scene: Phaser.Scene, x: number, y: number)
{
super(scene, x, y)
// replace const mouse with this.mouse
this.mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
.setOrigin(0.5, 1)
.play(AnimationKeys.RocketMouseRun)
// add flames...
// add this.mouse instead of mouse
this.add(this.mouse)
scene.physics.add.existing(this)
// change calls to mouse variable to this.mouse
const body = this.body as Phaser.Physics.Arcade.Body
body.setSize(this.mouse.width, this.mouse.height)
body.setOffset(this.mouse.width * -0.5, -this.mouse.height)
}
}
// cursors...
// other code...
This change is necessary so that we can access the mouse sprite in preUpdate() to play different
animations.
Next, we can play the fly and fall animations when appropriate:
Tommy Leung
56
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 preUpdate()
2 {
3
const body = this.body as Phaser.Physics.Arcade.Body
4
5
if (this.cursors.space?.isDown)
6
{
7
body.setAccelerationY(-600)
8
this.enableJetpack(true)
9
10
// play the fly animation
11
this.mouse.play(AnimationKeys.RocketMouseFly, true)
12
}
13
else
14
{
15
body.setAccelerationY(0)
16
this.enableJetpack(false)
17
}
18
19
// check if touching the ground
20
if (body.blocked.down)
21
{
22
// play run when touching the ground
23
this.mouse.play(AnimationKeys.RocketMouseRun, true)
24
}
25
else if (body.velocity.y > 0)
26
{
27
// play fall when no longer ascending
28
this.mouse.play(AnimationKeys.RocketMouseFall, true)
29
}
30 }
We play the fly animation right after we turn on the jetpack.
The newer block of code after that checks if Rocket Mouse is touching the ground using body.blocked
.down. If he is touching the ground then switch to the run animation.
If he is not touching the ground and has a y velocity greater than 0 then play the fall animation.
Give it a try and you should see Rocket Mouse fly, then fall, then land and run!
This was a meaty chapter!
We’ve got a lot of the game working but it isn’t particularly challenging since there’s nothing to do...
In the next chapter, we will add some laser obstacles to spice it up!
Tommy Leung
57
Creating a Laser Obstacle
You might be wondering what kind of house has lasers for security?
Perhaps, a house with as many mouse holes as this one does? Clearly, there’s a pest-control problem.
Lasers might be overkill but. . . go big or go home, right?
Preload the Laser Images
The first thing we need to do is load the 2 laser images in the Preloader Scene.
1
2
3
4
5
6
7
8
9
// Preloader.ts
preload()
{
// other images...
}
this.load.image(TextureKeys.LaserEnd, 'house/object_laser_end.png')
this.load.image(TextureKeys.LaserMiddle, 'house/object_laser.png')
Remember to add the new enum values to TextureKeys like this:
1
2
3
4
5
6
7
8
9
enum TextureKeys
{
// previous values...
}
LaserEnd = 'laser-end',
LaserMiddle = 'laser-middle'
export default TextureKeys
Now, we can compose the laser obstacle using the two images we loaded.
58
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Composing the Laser Obstacle
We will be creating our laser obstacle in a similar fashion to what we did in the last chapter with
RocketMouse.ts.
There will be 3 pieces to the laser obstacle including a top, middle, and bottom. You may have noticed
that we only have 2 images: an end and a middle.
The bottom will be created by flipping the end image vertically.
Let’s start by creating a LaserObstacle.ts file in the game folder on the same level as RocketMouse
.ts.
1
2
3
4
5
6
7
8
9
import Phaser from 'phaser'
export default class LaserObstacle extends Phaser.GameObjects.Container
{
constructor(scene: Phaser.Scene, x: number, y: number)
{
super(scene, x, y)
}
}
This starting point is basically the same as what we did in the last chapter for RocketMouse.
Next, we can create each of the 3 sections and place them appropriately to look like a laser beam
between two ends.
1 constructor(scene: Phaser.Scene, x: number, y: number)
2 {
3
super(scene, x, y)
4
5
// create a top
6
const top = scene.add.image(0, 0, TextureKeys.LaserEnd)
7
.setOrigin(0.5, 0)
8
9
// create a middle and set it below the top
10
const middle = scene.add.image(
11
0,
12
top.y + top.displayHeight,
13
TextureKeys.LaserMiddle
14
)
15
.setOrigin(0.5, 0)
16
17
// set height of middle laser to 200px
18
middle.setDisplaySize(middle.width, 200)
19
20
// create a bottom that is flipped and below the middle
21
const bottom = scene.add.image(0, middle.y + middle.displayHeight,
TextureKeys.LaserEnd)
Tommy Leung
59
Infinite Runner in Phaser 3 with TypeScript
22
23
24
25
26
27
28
29 }
© 2020 Ourcade
.setOrigin(0.5, 0)
.setFlipY(true)
// add them all to the Container
this.add(top)
this.add(middle)
this.add(bottom)
There’s quite a bit of code here but it is all relatively simple.
First, we create a top which is just the LaserEnd image with the origin set to the middle top. This will
make it easier to calculate where the middle laser part should go.
Next, we create a middle which is the LaserMiddle image with the origin set to the middle top as well.
Notice that the y position value is set to top.y + top.displayHeight. This will set the middle
image right below the top image.
Then, we set the height of the middle to be 200. This is the laser part between the top and bottom
ends.
After that, we create a bottom which is the same LaserEnd image as the top but flipped vertically
with the same origin as everyone else. Its y position is set to middle.y + middle.displayHeight
just like how the y position for middle was set to top.y + top.displayHeight. This ensures the
bottom image is right below the middle image.
Lastly, we add all 3 parts to the Container.
Add LaserObstacle to Game Scene
Let’s see our handiwork in action by adding a LaserObstacle to the Game Scene.
It will be very similar to how we created the RocketMouse class instance.
1
2
3
4
5
6
7
8
9
10
11
12
13
// import LaserObstacle
import LaserObstacle from '../game/LaserObstacle'
// then in create()...
create()
{
// create decorations...
const laserObstacle = new LaserObstacle(this, 900, 100)
this.add.existing(laserObstacle)
}
// create RocketMouse and other code...
Tommy Leung
60
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
We’ve omitted the previous code but you’ll want to add the LaserObstacle after the last decoration
and before RocketMouse so that it layers properly.
This is what you should see as Rocket Mouse runs toward 900 pixels on the x-axis:
‘
Looping the LaserObstacle
We’ll want this LaserObstacle to loop back around once it has scrolled off the left side of the screen
just like the decorations in Chapter 6.
That means we’ll need to create a class property and create a method called wrapLaserObstacle()
that gets called in the update() method.
We’ve created many class properties already so you should know what to do!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// imports...
export default class Game extends Phaser.Scene
{
// other properties...
private laserObstacle!: LaserObstacle
create()
{
// create decorations...
this.laserObstacle = new LaserObstacle(this, 900, 100)
this.add.existing(this.laserObstacle)
}
// create RocketMouse and other code...
}
We create the class property laserObstacle and then replace uses of const laserObstacle with
this.laserObstacle.
Next, let’s add the wrapLaserObstacle() method:
Tommy Leung
61
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 private wrapLaserObtacle()
2 {
3
const scrollX = this.cameras.main.scrollX
4
const rightEdge = scrollX + this.scale.width
5
6
const width = this.laserObstacle.width
7
if (this.laserObstacle.x + width < scrollX)
8
{
9
this.laserObstacle.x = Phaser.Math.Between(
10
rightEdge + width,
11
rightEdge + width + 1000
12
)
13
14
this.laserObstacle.y = Phaser.Math.Between(0, 300)
15
}
16 }
You’ll notice that this is very similar to wrapMouseHole() except we also pick a random value for the
y position of this.laserObstacle.
This will put the laser higher or lower on the screen forcing the player to maneuver with the jetpack.
Next, add a call to this.wrapLaserObstacle() in the update() method:
1 update(t: number, dt: number)
2 {
3
// wrap decorations...
4
5
this.wrapLaserObtacle()
6
7
// scroll background...
8 }
You may notice that the laser will disappear before it has fully scrolled off the screen
This is because the width of the LaserObstacle is currently 0. This will be fixed once we set a physics
body and define a size as we did with RocketMouse in the last chapter.
For now, just check that the laser is looping back to the front and picking different y position values.
Making the Laser Dangerous
Let’s add a physics body to the LaserObstacle so that our jetpack-wearing hero can run into it and
get zapped.
Make the following changes to the constructor of LaserObstacle:
Tommy Leung
62
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 constructor(scene: Phaser.Scene, x: number, y: number)
2 {
3
// previous code...
4
5
scene.physics.add.existing(this, true)
6
7
const body = this.body as Phaser.Physics.Arcade.StaticBody
8
const width = top.displayWidth
9
const height = top.displayHeight + middle.displayHeight +
10
bottom.displayHeight
11
12
body.setSize(width, height)
13
body.setOffset(-width * 0.5, 0)
14
15
// reposition body
16
body.position.x = this.x + body.offset.x
17
body.position.y = this.y
18 }
The first thing we do is use scene.physics.add.existing(this, true) to add a static physics
body to the Container.
The second parameter given the true value designates that the physics body should be static. A static
physics body will collide with normal bodies but will not be moved by gravity or pushed by other
objects.
This is perfect for the LaserObstacle because it needs to collide with Rocket Mouse and stay where
we put it.
Next, we set the size by using the width of the top and the combined height of all the pieces. Then the
offset is set to negative half-width and 0 based on the values we used for setOrigin() earlier.
Now, you should see a box around the LaserObstacle like this:
Tommy Leung
63
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Let it scroll off the screen and loop back around. You’ll notice that the collision box disappears!
This is because the physics body is static. We’ll need to manually set the body’s position when we
move the LaserObstacle in the Game Scene.
Update wrapLaserObstacle() with the follow code:
Tommy Leung
64
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 private wrapLaserObtacle()
2 {
3
const scrollX = this.cameras.main.scrollX
4
const rightEdge = scrollX + this.scale.width
5
6
// body variable with specific physics body type
7
const body = this.laserObstacle.body as
8
Phaser.Physics.Arcade.StaticBody
9
10
// use the body's width
11
const width = body.width
12
if (this.laserObstacle.x + width < scrollX)
13
{
14
this.laserObstacle.x = Phaser.Math.Between(
15
rightEdge + width,
16
rightEdge + width + 1000
17
)
18
this.laserObstacle.y = Phaser.Math.Between(0, 300)
19
20
// set the physics body's position
21
// add body.offset.x to account for x offset
22
body.position.x = this.laserObstacle.x + body.offset.x
23
body.position.y = this.laserObstacle.y
24
}
25 }
Now the physics collision box should move with the LaserObstacle when we loop it around to the
front of Rocket Mouse.
Collide and Zap
A laser obstacle is only good if it stings!
Let’s add a check for overlap between Rocket Mouse and the LaserObstacle so that we can make it
dangerous.
Update the create() method and add handleOverlapLaser() in the Game Scene:
Tommy Leung
65
Infinite Runner in Phaser 3 with TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
© 2020 Ourcade
// Game.ts
create()
{
// previous code...
}
this.physics.add.overlap(
this.laserObstacle,
mouse,
this.handleOverlapLaser,
undefined,
this
)
private handleOverlapLaser(
obj1: Phaser.GameObjects.GameObject,
obj2: Phaser.GameObjects.GameObject
)
{
console.log('overlap!')
}
We use this.physics.add.overlap() between the mouse and this.laserObstacle so that the
handleOverlapLaser method will be called when the two GameObjects overlap.
Give it a try and you should see the message overlap! in the Console of the browser’s Developer
Tools. You can bring up up the DevTools by right-clicking and selecting “Inspect” or going to View >
Developer > Developer Tools.
Now that we know when Rocket Mouse collides with the laser, we can play the death animation and
launch his body forward.
First, for the death animation we need to create it in the Preloader class like this:
Tommy Leung
66
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 // Preloader.ts
2
3 create()
4 {
5
// other animations...
6
7
this.anims.create({
8
key: AnimationKeys.RocketMouseDead,
9
frames: this.anims.generateFrameNames(TextureKeys.RocketMouse,
{
10
start: 1,
11
end: 2,
12
prefix: 'rocketmouse_dead',
13
zeroPad: 2,
14
suffix: '.png'
15
}),
16
frameRate: 10
17
})
18
19
this.scene.start(SceneKeys.Game)
20 }
Remember to add the enum value RocketMouseDead to AnimationKeys.ts.
Next, let’s create a kill() method in the RocketMouse class that will play this death animation and
launch our furry friend.
1 kill()
2 {
3
this.mouse.play(AnimationKeys.RocketMouseDead)
4
5
const body = this.body as Phaser.Physics.Arcade.Body
6
body.setAccelerationY(0)
7
body.setVelocity(1000, 0)
8
this.enableJetpack(false)
9 }
Give it a try and you’ll see Rocket Mouse play the death animation, get launched, and then run faster
than he’s ever run before!
This looks more like a power-up than death. . .
We can fix this by applying specific logic depending on what state Rocket Mouse is in. For example, he
can be dead or alive and the logic to handle what happens in those two states would be different.
This is where a State Machine comes in handy.
Tommy Leung
67
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Poor Man’s State Machine
For simplicity’s sake we will use a very basic state machine: the swtich statement.
First, create an enum at the top of the RocketMouse.ts file. This enum will define the different states
Rocket Mouse can be in.
1 enum MouseState
2 {
3
Running,
4
Killed,
5
Dead
6 }
Next, add a class property called mouseState:
1 export default class RocketMouse extends Phaser.GameObjects.Container
2 {
3
private mouseState = MouseState.Running
4
5
// other code...
6 }
Our first use of this mouseState property is to only perform the kill logic if Rocket Mouse is in the
RUNNING state.
Update kill() with this change:
1 kill()
2 {
3
// don't do anything if not in RUNNING state
4
if (this.mouseState !== MouseState.Running)
5
{
6
return
7
}
8
9
// set state to KILLED
10
this.mouseState = MouseState.Killed
11
12
this.mouse.play(AnimationKeys.RocketMouseDead)
13
14
const body = this.body as Phaser.Physics.Arcade.Body
15
body.setAccelerationY(0)
16
body.setVelocity(1000, 0)
17
this.enableJetpack(false)
18 }
We add a check at the top to see if mouseState is not equal to MouseState.RUNNING and early exit if
that is true. That means all the code after the if statement is not run if mouseState is not RUNNING.
Tommy Leung
68
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
This will ensure that Rocket Mouse doesn’t get killed multiple times or when he is already dead.
It just isn’t right to kick a mouse when he’s down. Right?
Next, let’s add a switch statement to preUpdate() and move the logic we already have for the
MouseState.RUNNING state.
1 preUpdate()
2 {
3
// switch on this.mouseState
4
switch (this.mouseState)
5
{
6
// move all previous code into this case
7
case MouseState.Running:
8
{
9
const body = this.body as Phaser.Physics.Arcade.Body
10
11
if (this.cursors.space?.isDown)
12
{
13
body.setAccelerationY(-600)
14
this.enableJetpack(true)
15
16
this.mouse.play(AnimationKeys.RocketMouseFly, true)
17
}
18
else
19
{
20
body.setAccelerationY(0)
21
this.enableJetpack(false)
22
}
23
24
if (body.blocked.down)
25
{
26
this.mouse.play(AnimationKeys.RocketMouseRun, true)
27
}
28
else if (body.velocity.y > 0)
29
{
30
this.mouse.play(AnimationKeys.RocketMouseFall, true)
31
}
32
33
// don't forget the break statement
34
break
35
}
36
}
37 }
There’s a lot of code here but we only added about 6 lines and 4 of them are braces.
We add a swtich state for this.mouseState and then only execute the code we had previously for
the RUNNING state. Don’t forget the break at the end of the case block.
Now give it a try and you should see a dead Rocket Mouse sliding along the floor. . . forever.
Tommy Leung
69
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
We are making progress!
Next, we will add logic for the MouseState.KILLED and MouseState.DEAD states to reduce Rocket
Mouse’s acceleration over time until his limp body comes to a stop.
Add these two case blocks to the switch statement:
1 preUpdate()
2 {
3
// move this out of the run logic to the top of the method
4
const body = this.body as Phaser.Physics.Arcade.Body
5
6
switch (this.mouseState)
7
{
8
case MouseState.Running:
9
{
10
// run logic
11
break
12
}
13
14
case MouseState.Killed:
15
{
16
// reduce velocity to 99% of current value
17
body.velocity.x *= 0.99
18
19
// once less than 5 we can say stop
20
if (body.velocity.x <= 5)
21
{
22
this.mouseState = MouseState.Dead
23
}
24
break
25
}
26
27
case MouseState.Dead:
28
{
29
// make a complete stop
30
body.setVelocity(0, 0)
31
break
32
}
33
}
34 }
We set this.mouseState to MouseState.KILLED in the kill() method and then launch Rocket
Mouse forward at high speed.
Then in preUpdate() we perform logic for the KILLED state by reducing the velocity until it is less
than or equal to 5. Once that is true, we set the mouseState to MouseState.DEAD.
Finally, in MouseState.DEAD we bring Rocket Mouse to a complete stop by setting the velocity to 0.
Give it a try and the entire death animation will look a lot more believable now!
Tommy Leung
70
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
We went over a lot in this chapter and now the game actually has a challenge. It is, perhaps, even
fun?
In the next chapter, we will add a Game Over Scene that will let us play again so we don’t have to just
sit and stare at a dead Rocket Mouse.
Tommy Leung
71
Game Over and Play Again
After 3 beefy chapters, we are going to relax just a little and work on adding a Game Over screen with
the ability to play again.
We ended the last chapter by implementing a poor man’s State Machine and we will use that to
determine when we show the Game Over screen.
Create a GameOver Scene
Our Game Over screen is going to be a Scene that runs in parallel with the Game Scene.
This is a common way to organize code for persistent UI like showing a score or a pause button as well
as to show modal dialogs.
Let’s start by creating a GameOver.ts file in the scenes folder on the same level as Game.ts.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Phaser from 'phaser'
import SceneKeys from '../consts/SceneKeys'
export default class GameOver extends Phaser.Scene
{
constructor()
{
super(SceneKeys.GameOver)
}
}
create()
{
}
Creating new Scenes should be fairly familiar by now. This is the third Scene we’ve made in this book.
Don’t forget to add a new enum member to SceneKeys.ts.
72
Infinite Runner in Phaser 3 with TypeScript
1
2
3
4
5
6
7
8
© 2020 Ourcade
enum SceneKeys
{
Preloader = 'preloader',
Game = 'game',
GameOver = 'game-over' // <-- new member
}
export default SceneKeys
Next, we can create some text to tell the player that they can press the Space bar to play again.
Add this code to the create() method:
1 create()
2 {
3
// object destructuring
4
const { width, height } = this.scale
5
6
// x, y will be middle of screen
7
const x = width * 0.5
8
const y = height * 0.5
9
10
// add the text with some styling
11
this.add.text(x, y, 'Press SPACE to Play Again', {
12
fontSize: '32px',
13
color: '#FFFFFF',
14
backgroundColor: '#000000',
15
shadow: { fill: true, blur: 0, offsetY: 0 },
16
padding: { left: 15, right: 15, top: 10, bottom: 10 }
17
})
18
.setOrigin(0.5)
19 }
The first thing we do is create a width and height variable using a language feature called Object
Destructuring or a Destructuring Assignment. This is a TypeScript and modern JavaScript feature that
creates variables by taking the values of the properties in an object that have the same names.
In this example, the ScaleManager has the properties width and height. Object Destructuring lets
us take those properties and create 2 constant local variables from them.
It is the same as writing this:
1
2
const width = this.scale.width
const height = this.scale.height
After that we choose an x and y position and create a Text object with some styling to make it more
pleasing to look at.
We used the Text Styler for Phaser 3 tool that you can find here. It is a free web-based tool from Ourcade
Tommy Leung
73
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
that lets you quickly and visually design text styles.
It will generate the appropriate code as you make changes. Just copy the config code when you are
happy with the design.
Having to tweak designs in code and reload the browser each time can be frustratingly slow so we
recommend you try out the tool!
Show the Game Over Scene
Before we can use the GameOver Scene, we need to add it to the list of scenes in Phaser’s GameConfig
in main.ts.
1
2
3
4
5
6
7
8
// import the scene
import GameOver from './scenes/GameOver'
// add it to the config
const config: Phaser.Types.Core.GameConfig = {
// other code...
scene: [Preloader, Game, GameOver] // <-- add it at the end
}
Now, we can use this Scene by running it after Rocket Mouse hits a laser obstacle and comes to a stop.
There are a few ways to implement this and we have chosen the simplest one for this book. We’ll give
you a more advanced option later that you can try implementing yourself!
Let’s go to the poor man’s State Machine that we have in the RocketMouse class and make this change
to the DEAD state:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// import SceneKeys at the top of RocketMouse.ts
import SceneKeys from '../consts/SceneKeys'
// then in the preUpdate() method
switch (this.mouseState)
{
// other cases...
case MouseState.Dead:
{
body.setVelocity(0, 0)
// add this line
this.scene.scene.run(SceneKeys.GameOver)
break
}
}
Tommy Leung
74
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
This is a very simple one-liner that tells the SceneManager to run the GameOver Scene. Notice that
we are using run() and not start().
We will be calling run() every frame that we are in the DEAD state. Phaser appears to be okay with
this but you can also use this.scene.scene.isActive(SceneKeys.GameOver) to check
that the GameOver Scene is not currently running before trying to run it.
We are using the odd-looking this.scene.scene because the Container has a reference to the
Game Scene that is stored as a property named scene and then the Game Scene has a reference to the
SceneManager that is also named scene.
It may look confusing but it is just how Phaser 3 happens to be structured. Just know that using
this.scene.scene is intended and not a typo.
Give this a try and you’ll see a banner with the message Press SPACE to Play Again.
Pressing the Space bar won’t do anything because we haven’t implemented that yet!
Pressing Space to Play Again
We will listen for the Space bar being pressed in the GameOver Scene and then restart the game when
it does.
Add this to the bottom of create() in GameOver.ts:
1 create()
2 {
3
// previous code to create text
4
5
// listen for the Space bar getting pressed once
6
this.input.keyboard.once('keydown-SPACE', () => {
7
// stop the GameOver scene
8
this.scene.stop(SceneKeys.GameOver)
9
10
// stop and restart the Game scene
11
this.scene.stop(SceneKeys.Game)
12
this.scene.start(SceneKeys.Game)
13
})
14 }
We listen for the keydown-SPACE event emitted by the KeyboardManger one time by using .once()
instead of .on().
When this happens, we stop the GameOver and Game Scenes and then we start the Game Scene again.
Give it a try and you should now be able to play the game again and again!
Tommy Leung
75
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Another Way to Show GameOver Scene
The main issue with how we are showing the GameOver Scene is that the RocketMouse class is making
the call.
It is perfectly correct code given the way Phaser 3 is structured but a new developer looking at this
project–or you in a couple of months–might not expect RocketMouse to make the Scene change.
It is more fitting for a Scene to handle Scene changes like we did with pressing Space to play again.
One way to have the Game Scene control showing the GameOver Scene is to have RocketMouse emit
an event when it enters the DEAD state. A Phaser.Events.EventEmitter can be created as a class
property of RocketMouse and then wrapper methods can be created to allow the Game Scene to listen
for an event like this:
1 mouse.once('dead', () => {
2
// run GameOver scene
3 })
We will leave implementing something like this as an exercise for you!
Should the Game Over Scene Restart the Game Scene?
If we are saying that it is odd for RocketMouse to perform a Scene change then wouldn’t it also stand
to reason that it is odd for the GameOver Scene to control restarting the Game Scene?
It was the Game Scene that started the GameOver Scene so the responsibility to stop the GameOver
Scene should belong to the Game Scene. It would also make more sense if the Game Scene restarted
itself.
We opted for the simplest implementation given the simplicity of this game but it is good to be thinking
about these things!
If you were to give the responsibility of stopping the GameOver Scene and restarting itself to the Game
Scene, how would you do it?
We will leave that implementation as an exercise for you!
One tip is that you can use this.scene.restart() from the Game Scene instead of calling stop()
and start() like we are doing now.
This Rocket Mouse game is almost completely playable!
Tommy Leung
76
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
In the next chapter, we will look at adding coins to collect and display a score based on the number of
collected coins.
Tommy Leung
77
Add Coins to Collect
Every game needs something to count for scoring purposes. We can use the total distance ran as a
score but collecting coins is more fun.
Calculating a score from coins collected and distance ran is another option that we’ll leave as an
exercise for you!
Load the Coin Image
Let’s start by loading the object_coin.png file in the Preloader Scene.
1 preload()
2 {
3
// load other assets...
4
5
this.load.image(TextureKeys.Coin, 'house/object_coin.png')
6 }
Remember to add the new enum value to TextureKeys like this:
1
2
3
4
5
6
7
enum TextureKeys
{
// other members...
Coin = 'coin'
}
export default TextureKeys
Creating Coins with a Group
We will be spawning a random number of coins periodically and the best way to handle that is to use a
Phaser Group.
A Group has built-in support for recycling GameObjects and we’ll want to do that instead of creating
new coins as we need them and destroying coins as they scroll off the screen.
78
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Frequent or large amount of object creation and destruction can result in performance problems on
less capable devices.
Let’s create a class property to start so that we can put the coin spawning logic in a Game Scene
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// imports...
export default class Game extends Phaser.Scene
{
// other properties...
private coins!: Phaser.Physics.Arcade.StaticGroup
create()
{
// decorations and laser obstacle...
this.coins = this.physics.add.staticGroup()
this.spawnCoins()
}
// create Rocket Mouse and other code...
private spawnCoins()
{
// code to spawn coins
}
}
// other code...
Most of this should look familiar! We create a coins property using the ! which is TypeScript’s non-null
assertion operator. This lets us tell TypeScript that this.coins will be set even though it doesn’t
happen in the constructor.
Then we set this.coins to a Phaser.Physics.Arcade.StaticGroup and call an empty method
named spawnCoins().
We are using a StaticGroup so that the physics objects created by the group will be static. This way
the coins will float in the air and not be affected by gravity.
The spawnCoins() method is where we will add code to create coins randomly.
Tommy Leung
79
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 private spawnCoins()
2 {
3
// make sure all coins are inactive and hidden
4
this.coins.children.each(child => {
5
const coin = child as Phaser.Physics.Arcade.Sprite
6
this.coins.killAndHide(coin)
7
coin.body.enable = false
8
})
9
10
const scrollX = this.cameras.main.scrollX
11
const rightEdge = scrollX + this.scale.width
12
13
// start at 100 pixels past the right side of the screen
14
let x = rightEdge + 100
15
16
// random number from 1 - 20
17
const numCoins = Phaser.Math.Between(1, 20)
18
19
// the coins based on random number
20
for (let i = 0; i < numCoins; ++i)
21
{
22
const coin = this.coins.get(
23
x,
24
Phaser.Math.Between(100, this.scale.height - 100),
25
TextureKeys.Coin
26
) as Phaser.Physics.Arcade.Sprite
27
28
// make sure coin is active and visible
29
coin.setVisible(true)
30
coin.setActive(true)
31
32
// enable and adjust physics body to be a circle
33
const body = coin.body as Phaser.Physics.Arcade.StaticBody
34
body.setCircle(body.width * 0.5)
35
body.enable = true
36
37
// move x a random amount
38
x += coin.width * 1.5
39
}
40 }
There’s a lot of code to unpack here but most of it is fairly straight forward.
First, we go through all the children of the coins group and make sure they are all not visible, not
active, and have a disabled physics body.
Then we pick a starting x position that is 100 pixels past the right edge of the screen.
A random number of coins is generated between 1 and 20 that we then use to create that number of
coins.
Tommy Leung
80
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Each coin is given a random y value that is within 100 pixels of the top and bottom of the screen. Then
the coin is set to be visible and active and given a circle physics body. We also make sure the body is
enabled.
Lastly, we add 1.5x the coin’s width to the x position so that the next coin will be spaced away from the
previous one.
Give this a try and you should see a bunch of randomly placed coins as Rocket Mouse runs forward.
Other Coin Formations
We won’t cover spawning pre-made coin formations because it is outside the scope of this book.
However, you can try implementing that yourself by creating different functions or methods that place
coins in specific positions to display a message like “Hello!” or some other pattern.
Then instead of picking random positions, you can pick a random function or method to use.
Collecting Coins
Coin collection is implemented in a similar fashion to colliding with the LaserObstacle except nothing
bad happens when you do!
First, we need to detect an overlap between Rocket Mouse and any coin like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Game.ts
create()
{
// previous code...
}
// create overlap detection
this.physics.add.overlap(
this.coins,
mouse,
this.handleCollectCoin,
undefined,
this
)
// this method will be called when an overlap happens
private handleCollectCoin(
obj1: Phaser.GameObjects.GameObject,
obj2: Phaser.GameObjects.GameObject
)
Tommy Leung
81
Infinite Runner in Phaser 3 with TypeScript
22 {
23
24
25
26
27
28
29
30
31 }
© 2020 Ourcade
// obj2 will be the coin
const coin = obj2 as Phaser.Physics.Arcade.Sprite
// use the group to hide it
this.coins.killAndHide(coin)
// and turn off the physics body
coin.body.enable = false
Adding code to detect an overlap should be familiar.
The handleCollectCoin() callback method will be called when an overlap occurs. At that time, we
set the coin to invisible and inactive by calling killAndHide() on the group and passing in the coin
that overlapped with Rocket Mouse.
The coin’s physics body is also disabled so that it doesn’t register more overlaps.
Give it a try and you’ll see that the coins disappear as Rocket Mouse’s collision box overlaps with the
coin.
Displaying a Score
Now, let’s keep track of how many coins we’ve collected and display it as a UI element at the top left of
the screen.
Let’s start by creating 2 class properties to hold the score count and a reference to the Text object that
will display the score.
1 export default class Game extends Phaser.Scene
2 {
3
// class proeprties...
4
5
private scoreLabel!: Phaser.GameObjects.Text
6
private score = 0
7
8
init()
9
{
10
this.score = 0
11
}
12
13
// other code...
14 }
Notice that we added an init() method which is called by Phaser when the Scene is started. It is
called before preload() and create().
Tommy Leung
82
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
We use it here to reset this.score to 0 so that we start fresh each time we play the game again.
Next, create the Text instance in the create() method:
1 create()
2 {
3
// previous code...
4
5
this.scoreLabel = this.add.text(10, 10, Score: ${this.score } , {
6
fontSize: '24px',
7
color: '#080808',
8
backgroundColor: '#F8E71C',
9
shadow: { fill: true, blur: 0, offsetY: 0 },
10
padding: { left: 15, right: 15, top: 10, bottom: 10 }
11
})
12
.setScrollFactor(0)
13 }
The scoreLabel is created in a similar way to what we did in the GameOver Scene. Recall that we
used the Text Styler for Phaser 3 web-based tool to generate the TextStyle config options.
Then we use setScrollFactor(0) to keep it in place even as the camera scrolls to follow Rocket
Mouse.
You’ll see a yellow rectangle with Score: 0 at the top left corner of the screen once the browser
reloads.
To finish up, we just need to add 1 to this.score each time a coin is collected and update the
scoreLabel with new text.
1
2
3
4
5
6
7
8
9
10
11
12
13
private handleCollectCoin(
obj1: Phaser.GameObjects.GameObject,
obj2: Phaser.GameObjects.GameObject
)
{
// previous code
// increment by 1
this.score += 1
}
// change the text with new score
this.scoreLabel.text = Score: ${this.score }
The code is pretty straight forward. We add 1 to this.score and then update this.scoreLabel.
text with the new score value.
Give it a try and watch the score count change as you collect coins!
Tommy Leung
83
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Note on Collision Box Sizes
You’ll notice that Rocket Mouse has a pretty big physics box. Bigger than his actual size and way bigger
than a size that is actually fun.
We can adjust the size of Rocket Mouse’s physics box using setSize on the physics body.
Let’s do this for Rocket Mouse by updating the setSize and setOffset code in the constructor of
RocketMouse like this:
1 export default class RocketMouse extends Phaser.GameObjects.Container
2 {
3
constructor(scene: Phaser.Scene, x: number, y: number)
4
{
5
// other code...
6
7
const body = this.body as Phaser.Physics.Arcade.Body
8
9
// use half width and 70% of height
10
body.setSize(this.mouse.width * 0.5, this.mouse.height * 0.7)
11
12
// adjust offset to match
13
body.setOffset(this.mouse.width * -0.3, -this.mouse.height +
15)
14
}
15 }
Make these changes and you’ll notice that Rocket Mouse starts by falling as his collision box is now
smaller.
We can fix this by adjusting the physic’s world bounds in the Game Scene like this:
1 create()
2 {
3
// other code...
4
5
// change "- 30" to "- 55"
6
this.physics.world.setBounds(
7
0, 0,
8
Number.MAX_SAFE_INTEGER, height - 55
9
)
10 }
Now give it a try and fly around collecting coins.
The way we sized the collision box accounts for the feet tucking in during the fall animation which will
make it more accurate for barely avoiding lasers during a fall.
You should also notice that the LaserObstacle has a larger collision box than it needs as well. Use
Tommy Leung
84
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
what we did here as a guide for shrinking that collision box.
Creating Coins Periodically
The last thing we need to do is to create coins periodically.
There are a few ways we can do this including using Phaser’s Clock with this.time.addEvent to
spawn new coins every X seconds.
We are going to use a much simpler approach and simply call this.spawnCoins() when we wrap
the second bookcase.
Make this change to the wrapBookcases() method:
1 private wrapBookcases()
2 {
3
// other code...
4
5
if (this.bookcase2.x + width < scrollX)
6
{
7
// move this.bookcase2...
8
9
// call spawnCoins()
10
this.spawnCoins()
11
}
12 }
Give it a try and you’ll see the coins appear once and then appear again but the second wave of coins is
not collectible!
Let’s fix this by adjusting the spawnCoins() method when we set the coin’s physics body to be a
circle.
1 private spawnCoins()
2 {
3
// other code...
4
5
for (let i = 0; i < numCoins; ++i)
6
{
7
// create a coin from the group
8
9
const body = coin.body as Phaser.Physics.Arcade.StaticBody
10
body.setCircle(body.width * 0.5)
11
body.enable = true
12
13
// update the body x, y position from the GameObject
14
body.updateFromGameObject()
15
Tommy Leung
85
Infinite Runner in Phaser 3 with TypeScript
16
17
18 }
}
© 2020 Ourcade
x += coin.width * 1.5
Notice the call to body.updateFromGameObject() after the physics body is enabled.
This will move the physics body to the same position as to where the GameObject is visually displayed.
Try it out and you’ll see that the coins are collectible after they are respawned!
The last thing that we’ll cover in this book is to make the game truly infinite and avoid the problem of a
player being so good that they scroll reach the maximum size of a Number as we mentioned in Chapter
5.
We will tackle this in the next chapter!
Tommy Leung
86
To Infinity and Beyond
We talked about how our scrolling system is not truly infinite in Chapter 5 but let’s go a little deeper on
why that is.
The game certainly looks infinite right now and you can leave it running for hours and hours and have
no problem.
It is so close to infinite that, for all intents and purposes, you can call it infinite and no one will know
the difference. Except for you, anyone who reads the code, or someone really good at dodging laser
beams with a jetpack.
Where Does our World End?
In Chapter 5, we made the width of our physics world bounds Number.MAX_SAFE_INTEGER which
means Rocket Mouse will run into a wall there and continue running in place.
But the camera will be stationary and no more scrolling will occur.
The game would appear frozen in the eyes of the player as they are no longer moving forward.
One way to fix this is to teleport everything back to the beginning. This will create the illusion of an
infinite world within finite world bounds.
However, we don’t have to wait until we get to MAX_SAFE_INTEGER before teleporting Rocket Mouse,
the decorations, coins, and laser obstacle back to the beginning.
We can do it at a number like 2500 pixels and keep the player in that range the entire time they are
playing the game. If we can make it seamless then they’ll never know that they are on a treadmill!
Creating a Seamless Teleport
A teleport consists of moving all the GameObjects we’ve created backward by the same amount once
we’ve scrolled past a specific position.
87
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
This will create the illusion that everything is still moving forward together even though they all moved
backward together.
First, create a new class property in the Game Scene to hold a reference Rocket Mouse. We’ll need to
adjust his x position when a teleport happens.
1
2
3
4
5
6
7
8
9
10
// imports...
export default class Game extends Phaser.Scene
{
// other properties...
private mouse!: RocketMouse
}
// other code...
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44 }
© 2020 Ourcade
// teleport any spawned coins
this.coins.children.each(child => {
const coin = child as Phaser.Physics.Arcade.Sprite
if (!coin.active)
{
return
}
}
})
coin.x -= maxX
const body = coin.body as Phaser.Physics.Arcade.StaticBody
body.updateFromGameObject()
There is a bit of code here but it is all pretty straight forward.
We decide on 2500 pixels as the maxX value for when we perform the teleport. Then we move all the
GameObjects we’ve created backward by subtracting maxX from their x position.
Lastly, call teleportBackwards() at the end of the update() method like this:
1 update(t: number, dt: number)
2 {
3
// previous code...
4
5
this.teleportBackwards()
6 }
Give it a try and you’ll see that everything will work as expected except for the background and some
coins depending on how many were spawned.
Rocket Mouse, the decorations, and the laser obstacle should teleport backward seamlessly. You’ll
know when a teleport happens when the background changes abruptly.
Teleporting the Background Seamlessly
We are not fully in control of how the background scrolls because we are using the TileSprite.
The background is scrolled using the camera’s scrollX value and that is automatically adjusted after
we teleport Rocket Mouse. All we control is the position of Rocket Mouse.
Subtracting maxX from this.background.tilePosition.x doesn’t appear to work even though
it seems to make sense!
One way to resolve this is to handle the background scrolling manually by creating 4 background
images that we loop to the front as soon as one scrolls off the screen.
Tommy Leung
89
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
But that feels like a lot of work to replicate functionality that the TileSprite gives us for free.
There’s another approach that we discovered through trial and error: teleport backward when scrolled
beyond a multiple of the background texture width.
The background image has a width of 340 pixels so can we multiply that by 7 and use 2380 instead of
2500.
Try it out by changing the maxX value in teleportBackwards():
1 private teleportBackwards()
2 {
3
const scrollX = this.cameras.main.scrollX
4
5
// change to 2380 from 2500
6
const maxX = 2380
7
8
// previous code...
9 }
Play the game for a bit and the background should teleport seamlessly along with the other
GameObjects.
Fixing Coin Teleportation
If you play the game long enough you’ll sometimes notice that uncollected coins will disappear suddenly
before they’ve scrolled off the screen.
Recall that we are spawning coins when we wrap the second bookcase to a new position but this can
happen while the previous set of spawned coins is still on screen depending on how many random
coins were spawned.
We can fix this by moving the call to this.spawnCoins() from wrapBookcases() to teleportBackwards
(). This gives us a predictable size of 2380 pixels that should be enough for 20 spawned coins.
Add this change by deleting the call to this.spawnCoins() in wrapBookcases() and add it to
teleportBackwards() like this:
Tommy Leung
90
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
1 private teleportBackwards()
2 {
3
const scrollX = this.cameras.main.scrollX
4
const maxX = 2380
5
6
if (scrollX > maxX)
7
{
8
// previous code...
9
10
// spawn coins here
11
this.spawnCoins()
12
13
// continue to perform teleport logic for coins
14
this.coins.children.each(child => {
15
const coin = child as Phaser.Physics.Arcade.Sprite
16
if (!coin.active)
17
{
18
return
19
}
20
21
coin.x -= maxX
22
const body = coin.body as Phaser.Physics.Arcade.StaticBody
23
body.updateFromGameObject()
24
})
25
}
26 }
Test the game out for a bit and everything should now be working as one would expect. It’s a real
infinite runner with some challenge!
Optional Animation Updates
Phaser 3.50 introduced the idea of animations that can be specific or local to individual sprites.
So far we’ve created global animations that can be used by any Sprite. But you’ll notice that they
are only used by Rocket Mouse. While there’s nothing wrong with keeping these animations global it
would be convenient to have everything together in the RocketMouse class.
Let’s start by moving the animations in the Preloader Scene into a createAnimations() method
in the RocketMouse class.
1 // imports...
2
3 export default class RocketMouse extends Phaser.GameObjects.Container
4 {
5
// other code...
6
Tommy Leung
91
Infinite Runner in Phaser 3 with TypeScript
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private createAnimations()
{
this.mouse.anims.create({
key: AnimationKeys.RocketMouseRun,
frames: this.mouse.anims.generateFrameNames(TextureKeys.
RocketMouse, { start: 1, end: 4, prefix: '
rocketmouse_run', zeroPad: 2, suffix: '.png' }),
frameRate: 10,
repeat: -1
})
this.mouse.anims.create({
key: AnimationKeys.RocketMouseFall,
frames: [{
key: TextureKeys.RocketMouse,
frame: 'rocketmouse_fall01.png'
}]
})
this.mouse.anims.create({
key: AnimationKeys.RocketMouseFly,
frames: [{
key: TextureKeys.RocketMouse,
frame: 'rocketmouse_fly01.png'
}]
})
this.mouse.anims.create({
key: AnimationKeys.RocketMouseDead,
frames: this.mouse.anims.generateFrameNames(TextureKeys.
RocketMouse, { start: 1, end: 2, prefix: '
rocketmouse_dead', zeroPad: 2, suffix: '.png' }),
frameRate: 10
})
35
36
37
38
39
40
41
42
43
44
45 }
© 2020 Ourcade
}
this.flames.anims.create({
key: AnimationKeys.RocketFlamesOn,
frames: this.flames.anims.generateFrameNames(TextureKeys.
RocketMouse, { start: 1, end: 2, prefix: 'flame', suffix
: '.png'}),
frameRate: 10,
repeat: -1
})
Notice that instead of this.anims.create() we are using this.mouse.anims.create(). This
will create animations just for the this.mouse instance. We are also using this.mouse.anims.
generateFrameNames() instead of just this.anims.generateFrameNames().
Tommy Leung
92
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Next, we have to call createAnimations() from the constructor() like this:
1 constructor(scene: Phaser.Scene, x: number, y: number)
2 {
3
super(scene, x, y)
4
5
this.mouse = scene.add.sprite(0, 0, TextureKeys.RocketMouse)
6
.setOrigin(0.5, 1)
7
8
this.flames = scene.add.sprite(-63, -15, TextureKeys.RocketMouse)
9
10
this.createAnimations()
11
12
this.mouse.play(AnimationKeys.RocketMouseRun)
13
this.flames.play(AnimationKeys.RocketFlamesOn)
14
15
// other code
16 }
The createAnimations() method needs to be called after this.mouse and this.flames are created but before play() is called. The key is to split the chained method calls of setOrigin() and
play() so that we can create the sprites, then the animations, and then play the starting animations.
Remember to delete the animation code from Preloader as we no longer need it!
Give these new changes a try to make sure that everything is still be working.
You now have a functional infinite runner game created in Phaser 3 using TypeScript.
There’s still more you can do like adding sound effects and music as well as custom fonts for the UI.
We won’t be covering that in this book but check out the Ourcade Blog and YouTube Channel for more
content to help you add some finishing touches to your infinite runner!
Tommy Leung
93
Epilogue
Thank you for reading this book!
I hope it will give you a head start into making more scalable games with Phaser 3 and TypeScript.
Perhaps you’ve also learned some things related to game development that can be taken with you to
other frameworks and platforms.
The basic gameplay and mechanics of Rocket Mouse were modeled after Jetpack Joyride and there’s a
lot more you can add like vehicles and missiles.
There are many resources online for art assets and programming tutorials to help you add those
features.
We used Game Art Guppy in this book but there’s also Kenney.nl, Open Game Art, and Itch.io. Just do a
Google search for even more options!
There’s never been a better time or a world with more resources for making games.
Our aim at Ourcade is to create an approachable community for game development whether you are a
beginner, expert, or anything in between.
The process of making games should be pleasant and enjoyable at every skill level.
That’s why Ourcade believes in 5 core principles:
•
•
•
•
•
Playfulness
Friendliness
Helpfulness
Open-mindedness
Optimism
Join us if you find yourself nodding in agreement!
Find us on Twitter @ourcadehq.
Our YouTube Channel strives to show and explain the process of game development without editing
away the problem solving.
94
Infinite Runner in Phaser 3 with TypeScript
© 2020 Ourcade
Github is where we share source code for anyone to read, learn, or steal. We don’t believe that programming code should be protected like nuclear launch codes.
And finally the Ourcade blog and website is the central hub. You’ll find everything we’re teaching,
sharing, and building there.
Best of luck with your game making,
Tommy Leung (aka supertommy)
Florida, USA
May 2020, revised December 2021, January 2021
Tommy Leung
95
About the Author
Tommy Leung, aka supertommy, has been making games professionally since 2007. He is a self-taught
programmer with a business degree from Pace University.
He has worked on games for brands like Cartoon Network, Nickelodeon, LEGO, and more.
Before founding Ourcade, he was Senior Engineering Manager at Zynga, the makers of Words with
Friends and FarmVille, for a hit mobile game played by tens of millions of people.
Tommy has developed games in Flash, C/C++, Objective-C, Java, Unity/C# HTML5/JavaScript, and
more.
Other than game development, he also enjoys fullstack web development and native iOS development.
He believes in continuous learning, self improvement, and giving back by helping others because he
would not be where he is without the generocity of those who came before him.
Tommy is also an avid weight lifter.
He resides in Florida, USA and has 2 greyhounds.
96
Infinite Runner in Phaser 3 with TypeScript
Tommy Leung
© 2020 Ourcade
97
Download