0% found this document useful (0 votes)
75 views107 pages

PhaserByExample v2 1

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
75 views107 pages

PhaserByExample v2 1

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 107

Table of Contents

Authors 8

Foreword by Richard Davey, creator of Phaser 9

Introduction 10
Phaser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
How the book is organized . . . . . . . . . . . . . . . . . . . . . . . 11

1. Basic game: runner 12


Init project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Player code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Generating obstacles, coins and clouds . . . . . . . . . . . . . . . . 14
The game! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Gameover . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Other runners or similar games . . . . . . . . . . . . . . . . . . . . 26

2. Space Shooter: Starshake 28


Init project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Loader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Light Particle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Generating foes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Foes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Foes shot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Explosion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Player . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Shot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
Shooting pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Power Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
The game! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Scene Effect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Splash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Transition page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Outro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Other shooter or similar games . . . . . . . . . . . . . . . . . . . . 85

3. Platformer: WallHammer 86

1
Init project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Loader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Bat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Zombie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Turn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Lunchbox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Particle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Player code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
Blow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
Platform . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Brick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
Coin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
The game! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Splash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
Transition page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Outro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Other platformers or similar games . . . . . . . . . . . . . . . . . . 144

4. Puzzles: PushPull 146


Init project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Loader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Exit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
BlockGroup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
The game! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
Splash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
Transition page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
Outro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Other puzzles or similar games . . . . . . . . . . . . . . . . . . . . 177

5. Roguelike: Bobble Dungeon 179


Init project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Loader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
Bat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Bubble . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
Coin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
Key . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
Dust . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195

2
Fireball . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Wizard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
Matter Gravity Fix . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
Dungeon generator . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
See-Saw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Player . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
The game! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
Splash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
Transition page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Outro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
Other roguelike or similar games . . . . . . . . . . . . . . . . . . . 240
Other games using matter physics . . . . . . . . . . . . . . . . . . . 240

6. Tell me a story: Marstranded 241


Init project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
Loader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
Hole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
Drone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
Utils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Player . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
Braun . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
Splash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Transition page . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Outro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288
Other roguelike or similar games . . . . . . . . . . . . . . . . . . . 290

7. Multiplayer games 291


Blastemup: multiplayer with websockets. . . . . . . . . . . . . . . . 291
Zenbaki: a game for Twitch chat. . . . . . . . . . . . . . . . . . . . 302
Other Twitch games . . . . . . . . . . . . . . . . . . . . . . . . . . 325

8. 3D: Fate 328


Init . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
Bootloader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Story . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
Bullet Hell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341

3
Lightning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
Utils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
Splash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
GameOver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
Outro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371

9. Deep Dive Into Phaser 372


What is Phaser? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
The Core Concepts of Phaser . . . . . . . . . . . . . . . . . . . . . 373

10. Detailed Look Into Game Objects 397

Game Objects 398


Alpha Component . . . . . . . . . . . . . . . . . . . . . . . . . . . 398

Game Objects 401


Blend Mode Component . . . . . . . . . . . . . . . . . . . . . . . . 401

Game Objects 403


Bounds Component . . . . . . . . . . . . . . . . . . . . . . . . . . . 403

Game Objects 406


Crop Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

Data Manager 408


Data Manager Parents . . . . . . . . . . . . . . . . . . . . . . . . . 409
Data Manager Methods . . . . . . . . . . . . . . . . . . . . . . . . 411
Data Manager Events . . . . . . . . . . . . . . . . . . . . . . . . . 423
Destroying the Data Manager . . . . . . . . . . . . . . . . . . . . . 424

Game Objects 425


Depth Component . . . . . . . . . . . . . . . . . . . . . . . . . . . 425

Game Object Creator 428


How to set Configuration Properties . . . . . . . . . . . . . . . . . 429
Game Object Configuration Properties . . . . . . . . . . . . . . . . 431

4
Game Object Factory 432
How to bypass the Game Object Factory . . . . . . . . . . . . . . . 433
Removing a Factory Function . . . . . . . . . . . . . . . . . . . . . 433
Adding Custom Game Objects to the Game Object Factory . . . . 434

Game Objects 436


Scene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
Display List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437
State and Name . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
Update List and Active . . . . . . . . . . . . . . . . . . . . . . . . 439
Parent Containers . . . . . . . . . . . . . . . . . . . . . . . . . . . 440
Additional Methods . . . . . . . . . . . . . . . . . . . . . . . . . . 441

Game Objects 442


Mask Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . 442

Game Objects 446


Origin Component . . . . . . . . . . . . . . . . . . . . . . . . . . . 446

Game Objects 448


Pipeline Component . . . . . . . . . . . . . . . . . . . . . . . . . . 448

Game Objects 450


Scroll Factor Component . . . . . . . . . . . . . . . . . . . . . . . . 450

Game Objects 452


Sprites and Images . . . . . . . . . . . . . . . . . . . . . . . . . . . 452

Game Objects 454


Transform Component . . . . . . . . . . . . . . . . . . . . . . . . . 454

Game Objects 461


Visible Component . . . . . . . . . . . . . . . . . . . . . . . . . . . 461

11. Cookbook 463


Same sound with variants . . . . . . . . . . . . . . . . . . . . . . . 463
Actions on animation events . . . . . . . . . . . . . . . . . . . . . . 463
Mouse right and left click . . . . . . . . . . . . . . . . . . . . . . . 463
Screen Transitions . . . . . . . . . . . . . . . . . . . . . . . . . . . 463

5
Lightning effect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 464
Rain, Snow effect . . . . . . . . . . . . . . . . . . . . . . . . . . . . 464
Lights in the dark . . . . . . . . . . . . . . . . . . . . . . . . . . . . 464
Underwater swimming effect . . . . . . . . . . . . . . . . . . . . . . 464
Infinite scrolling background . . . . . . . . . . . . . . . . . . . . . . 464
Dynamic map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465
Adding/Removing tiles from a tiled map . . . . . . . . . . . . . . . 465
Map building . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465
Composed game objects . . . . . . . . . . . . . . . . . . . . . . . . 465
Find paths and move foes automatically . . . . . . . . . . . . . . . 465
Enemies shooting at player . . . . . . . . . . . . . . . . . . . . . . . 465
Detect screen limit . . . . . . . . . . . . . . . . . . . . . . . . . . . 466
Jump simulation on an isometric view . . . . . . . . . . . . . . . . 466
Parabolic shot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466
Bullet hell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 466
Ships in formation . . . . . . . . . . . . . . . . . . . . . . . . . . . 466
Life bar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
Typing effect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
Adding video . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
Valid Words . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
Keep a scoreboard . . . . . . . . . . . . . . . . . . . . . . . . . . . 468
Windows build . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 468
Index of games . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 468

12. Assets 482


12.1 Fonts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
12.2 Graphic asssets and Pixel Art . . . . . . . . . . . . . . . . . . 483
12.3 Audio assets . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
Converting/Editing your audio assets . . . . . . . . . . . . . . . . . 485
12.4 Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 487

13. Build & Delivery 492


Static HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
NodeJS + Local phaser library . . . . . . . . . . . . . . . . . . . . 494
NodeJS + Local Phaser with modules . . . . . . . . . . . . . . . . 495
Gulp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497
Webpack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502

6
Parcel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 506
Vite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 508
Online: phasereditor2d . . . . . . . . . . . . . . . . . . . . . . . . . 510
Online: repl.it . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Online: codesandbox . . . . . . . . . . . . . . . . . . . . . . . . . . 510
Converting to Windows app . . . . . . . . . . . . . . . . . . . . . . 511
Automating itch.io upload . . . . . . . . . . . . . . . . . . . . . . . 511
Netlify publish . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 511

14. Juice 513


Ideas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 513
References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 515

15. Jams 517


Why you should participate in a jam . . . . . . . . . . . . . . . . . 517
Feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518
The game idea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518

Jam Types 520


Little short jams . . . . . . . . . . . . . . . . . . . . . . . . . . . . 520
Big Jams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 523
Technical jams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524
Rule of thumb . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 525

16. 4:44 Rule 526


How is it applied? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526
Just for jams? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 528

17 Level design 529


Some tips . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 529
References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 537

7
Authors
How do you do fellow kids? I’ve spent some time making games for jams
using Phaser. I had to learn by doing and I also learned a lot in those jams,
getting feedback and sharing amazing ideas.
Phaser does not require any heavy environment to build games, it’s just
JavaScript and you can try it on your browser. The source is right there and
you will never depend on arbitrary corporate decisions.
I decided to get some of the games I developed to put them together in a
book, as a showcase of the well-known game genres. During the struggle
to build games you rely heavily on examples and code snippets, and this is
precisely the main goal of this text.
Each of these games were created in a weekend and you should not expect
them to be optimized. But they explain how to build playable stuff. Along
with that, I added some advice that I hope you may find it useful. So, here
are the examples, the tools, and some ideas. The rest is up to you!
Pello Xabier Altadill Izura
https://pello.io
https://github.com/pxai

8
Foreword by Richard Davey, creator of Phaser
I’ve been working on Phaser for over 11 years now. It’s been an incredible
journey and I’ve met so many amazing people along the way. I’ve seen the
framework used in ways I never imagined, and I’ve seen games created with
it that have blown my mind.
When I first started working on Phaser, I had no idea it would become so
popular. I just wanted to create a framework that would make it easier for
people like Pello to create games for the web. I wanted to make it accessible
to everyone, regardless of their skill level.
Like all popular tools, it has evolved over time. It has grown in complexity
and features, but at its core, it’s still the same framework I started working
on all those years ago. In this book you’ll find a whole bunch of games that
Pello has created using Phaser. They cover a wide range of genres and styles,
and they show just how versatile the framework can be. You’ll also find lots
of content that I’ve written that dives into the core concepts of Phaser in
more details, and hopefully provides a useful starting point for those new to
the framework.
I hope you enjoy reading this book as much as I’ve enjoyed working on Phaser.
And I hope it inspires you to create your own games, and to share them with
the world.
Richard Davey
https://phaser.io

9
Introduction
You know the story. As a kid, I wanted to understand and learn to make
video games. I was young and stupid. Now I’m just stupid but I still want
to build games, just for fun. This book explains how to build games using
the Phaser framework. We will start from the simplest game and then we
will embark on a journey through different genres, where we’ll introduce new
concepts and Phaser tools for them.
Apart from showing the guts of these games, we’ll also dip our toes in other
crucial areas like assets management, building and delivery, and a bit of game
design. Nothing too fancy just some advice that you may find useful in your
journey as a creator; because when it comes to creating games, at least if
you want to do it right, it goes far beyond programming. There are many
other brainy books about game producing, level design, theory, etc. to go
deeper. For now, I’m presuming that you’re solo so I will try to put in your
backpack things that I would like somebody had told me when I started.
First of all, you must know that there are many people making games. That
wonderful idea that you think you have is already implemented in different
ways and genres. So, don’t get too excited because probably your mastermind
plan is not that good. But don’t give up because at least you’ll learn while
building and failing. And there are chances that your worst idea turns out
good.

Phaser
Phaser is an open-source framework for building HTML5 games. It uses
JavaScript or TypeScript. Phaser allows you to program games just using
a code editor and a web browser. In addition to that, you can easily create
a continuous building system as you would do in any frontend application
using Gulp, Webpack, Parcel or other tools as we will see later.
As a developer, I like Phaser because the development process is very similar
to any other web project. They provide the library but you can set up a
building system around it and have total fine-grained control of the whole
process. Also, because it’s built on a well-known language and a familiar
environment: the web. Creating a game and publishing it on the web is
pretty straightforward.

10
To build and run the games, you can just create an HTML file, add a reference
to the Phaser library and add JavaScript code. However, in the examples
of this book the code is organized in separate files to make it easier to ex-
plain and understand. You can see these examples and much more in this
repository: https://github.com/phaserjs/phaser-by-example

How the book is organized


Games are not just software but also artistic creations. As creations the
possibilities are endless and I can’t pretend to cover the infinite. Here, I
will try to offer a showcase of the most common classic game genres while
introducing different aspects of games: scenes, animations, tweens, maps,
physics engines, etc.
Then is up to you to get these tools and build your own ideas.
After showing some games, I introduce some aspects of game development
that are important to know or at least be aware of.
Version: 2.0
Editor: Can Delibaş

11
1. Basic game: runner

Figure 1: Runner game screen

Source code: https://github.com/phaserjs/phaser-by-example/tree/main/runner


Let’s get started with a very basic game: an infinite runner. Probably you’ve
played the dinosaur game in Chrome every time your Internet connection is
lost. This is just the same idea but with rectangles and coins.

Init project
This init file holds the basic configuration of the Phaser game: this is where
we define the scenes that take part in the game (like Splash, Game scene,
Game Over scene, etc.), and it also configures screen size and position,
physics type, etc.
import Phaser from "phaser";
import Game from "./scenes/game";
import GameOver from "./scenes/gameover";
This is the main configuration file for the game.
const config = {
width: 600,

12
height: 300,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
autoRound: false,
parent: "game-container",
physics: {
default: "arcade",
arcade: {
gravity: { y: 350 },
debug: true,
},
},
scene: [Game, GameOver],
};

const game = new Phaser.Game(config);


List: main.js
During development phase, it’s a good idea to set debug to true, so we will
see outlines around any game object with physical properties.

Player code
This is the class that represents the player. It extends a very basic Phaser
game object: a Rectangle. The player is set up in the constructor, where we
provide him physics with a body and gravity.
class Player extends Phaser.GameObjects.Rectangle {
constructor(scene, x, y, number) {
super(scene, x, y, 32, 32, 0x00ff00);
this.setOrigin(0.5);
this.scene.add.existing(this);
this.scene.physics.add.existing(this);
this.body.collideWorldBounds = true;
this.setScale(1);
this.jumping = false;

13
this.invincible = false;
this.health = 10;
this.body.mass = 10;
this.body.setDragY = 10;
}
}

export default Player;


List: player.js
This “player” sucks but don’t worry, as we move forward, we will see animated
and juicy characters.

Generating obstacles, coins and clouds


The game is a simple infinite runner where the player (the green rectangle)
needs to avoid obstacles and pick coins. Those elements are randomly gener-
ated.
export default class Generator {
constructor(scene) {
this.scene = scene;
this.scene.time.delayedCall(2000, () => this.init(), null, this);
this.pinos = 0;
}

init() {
this.generateCloud();
this.generateObstacle();
this.generateCoin();
}
This is the function that generates the clouds. It creates a new cloud and
then calls itself again after a random amount of time.
This is done using the Phaser time.delayedCall function.
generateCloud() {
new Cloud(this.scene);
this.scene.time.delayedCall(

14
Phaser.Math.Between(2000, 3000),
() => this.generateCloud(),
null,
this
);
}

generateObstacle() {
this.scene.obstacles.add(
new Obstacle(
this.scene,
800,
this.scene.height - Phaser.Math.Between(32, 128)
)
);
this.scene.time.delayedCall(
Phaser.Math.Between(1500, 2500),
() => this.generateObstacle(),
null,
this
);
}

generateCoin() {
this.scene.coins.add(
new Coin(
this.scene,
800,
this.scene.height - Phaser.Math.Between(32, 128)
)
);
this.scene.time.delayedCall(
Phaser.Math.Between(500, 1500),
() => this.generateCoin(1),
null,
this
);
}

15
}
This is a game object that represents a cloud. It’s a simple rectangle with a
random size and position. We use a tween to move it from right to left, and
then destroy it when it’s out of the screen.
class Cloud extends Phaser.GameObjects.Rectangle {
constructor(scene, x, y) {
const finalY = y || Phaser.Math.Between(0, 100);
super(scene, x, finalY, 98, 32, 0xffffff);
scene.add.existing(this);
const alpha = 1 / Phaser.Math.Between(1, 3);

this.setScale(alpha);
this.init();
}

init() {
this.scene.tweens.add({
targets: this,
x: { from: 800, to: -100 },
duration: 2000 / this.scale,
onComplete: () => {
this.destroy();
},
});
}
}
This is a game object that represents an obstacle. It works exactly like the
cloud, but it’s a red rectangle that is part of the obstacles group that we
created in the game scene. It can kill the player if it touches it.
class Obstacle extends Phaser.GameObjects.Rectangle {
constructor(scene, x, y) {
super(scene, x, y, 32, 32, 0xff0000);
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setAllowGravity(false);
const alpha = 1 / Phaser.Math.Between(1, 3);

16
this.init();
}

init() {
this.scene.tweens.add({
targets: this,
x: { from: 820, to: -100 },
duration: 2000,
onComplete: () => {
this.destroy();
},
});
}
}
This is a game object that represents a coin. It’s an animated sprite that is
part of the coins group that we created in the game scene. It moves like the
previous cloud and the obstacle objects.
It can increase the player’s score if it touches it.
class Coin extends Phaser.GameObjects.Sprite {
constructor(scene, x, y) {
super(scene, x, y, "coin");
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setAllowGravity(false);
const alpha = 1 / Phaser.Math.Between(1, 3);

this.init();
}

init() {
this.scene.tweens.add({
targets: this,
x: { from: 820, to: -100 },
duration: 2000,
onComplete: () => {

17
this.destroy();
},
});

const coinAnimation = this.scene.anims.create({


key: "coin",
frames: this.scene.anims.generateFrameNumbers("coin", {
start: 0,
end: 7,
}),
frameRate: 8,
});
this.play({ key: "coin", repeat: -1 });
}
}
List: generator.js
We could tweak this generator to increase the difficulty as the game advances.

The game!
This is the game scene itself! As any Phaser Scene object it uses three main
methods:
• preload: where we load game assets: images, sprites, fonts, sounds,
maps, etc.
• create: where we instantiate and start game elements like player, ene-
mies, or obstacle generators. Also, this is where we define groups for
obstacles, coins and clouds and most importantly: we define how these
groups behave when they touch the player.
• update: the game loop. This method is called repeatedly by Phaser
and this is where we can handle player input.
import Player from "../gameobjects/player";
import Generator from "../gameobjects/generator";

export default class Game extends Phaser.Scene {


constructor() {
super({ key: "game" });

18
this.player = null;
this.score = 0;
this.scoreText = null;
}

init(data) {
this.name = data.name;
this.number = data.number;
}
We use the preload method to load all the assets that we need for the game.
We also set the score to 0 in the registry, so we can access it from other
scenes.
preload() {
this.registry.set("score", "0");
this.load.audio("coin", "assets/sounds/coin.mp3");
this.load.audio("jump", "assets/sounds/jump.mp3");
this.load.audio("dead", "assets/sounds/dead.mp3");
this.load.audio("theme", "assets/sounds/theme.mp3");
this.load.spritesheet("coin", "./assets/images/coin.png", {
frameWidth: 32,
frameHeight: 32,
});
this.load.bitmapFont(
"arcade",
"assets/fonts/arcade.png",
"assets/fonts/arcade.xml"
);
this.score = 0;
}
Here we do several things.
• We use the create method to initialize the game.
• We set some variables to store width and height that we may need
later.,
• We set the background color, and create the player, the obstacles, and
the coins.
• We also create the keyboard input to listen to the space key.

19
• Also, we add a collider between the player and the obstacles and an
overlap between the player and the coins. The key part there is to set
a function that will be called when the player overlaps with a coin or
hits an obstacle.
create() {
this.width = this.sys.game.config.width;
this.height = this.sys.game.config.height;
this.center_width = this.width / 2;
this.center_height = this.height / 2;

this.cameras.main.setBackgroundColor(0x87ceeb);
this.obstacles = this.add.group();
this.coins = this.add.group();
this.generator = new Generator(this);
this.SPACE = this.input.keyboard.addKey(
Phaser.Input.Keyboard.KeyCodes.SPACE
);
this.player = new Player(this, this.center_width - 100, this.height - 200);
this.scoreText = this.add.bitmapText(
this.center_width,
10,
"arcade",
this.score,
20
);

this.physics.add.collider(
this.player,
this.obstacles,
this.hitObstacle,
() => {
return true;
},
this
);

this.physics.add.overlap(

20
this.player,
this.coins,
this.hitCoin,
() => {
return true;
},
this
);

this.loadAudios();
this.playMusic();
We use the pointerdown event to listen to the mouse click or touch event.
this.input.on("pointerdown", (pointer) => this.jump(), this);
We use updateScoreEvent to update the score every 100ms so the player
can see the score increasing as long as he survives.
this.updateScoreEvent = this.time.addEvent({
delay: 100,
callback: () => this.updateScore(),
callbackScope: this,
loop: true,
});
}
This method is called when the player hits an obstacle. We stop the up-
dateScoreEvent so the score doesn’t increase anymore.
And obviously, we finish the scene.
hitObstacle(player, obstacle) {
this.updateScoreEvent.destroy();
this.finishScene();
}
This method is called when the player hits a coin. We play a sound, update
the score, and destroy the coin.
hitCoin(player, coin) {
this.playAudio("coin");

21
this.updateScore(1000);
coin.destroy();
}
We use this loadAudios method to load all the audio files that we need for
the game.
Then we’ll play them using the playAudio method.
loadAudios() {
this.audios = {
jump: this.sound.add("jump"),
coin: this.sound.add("coin"),
dead: this.sound.add("dead"),
};
}

playAudio(key) {
this.audios[key].play();
}
This method is specific to the music. We use it to play the theme music in
a loop.
playMusic(theme = "theme") {
this.theme = this.sound.add(theme);
this.theme.stop();
this.theme.play({
mute: false,
volume: 1,
rate: 1,
detune: 0,
seek: 0,
loop: true,
delay: 0,
});
}
This is the game loop. The function is called every frame.
Here is where we can check if a key was pressed or the situation of the player

22
to act accordingly. We use the update method to check if the player pressed
the space key.
update() {
if (Phaser.Input.Keyboard.JustDown(this.SPACE)) {
this.jump();
} else if (this.player.body.blocked.down) {
this.jumpTween?.stop();
this.player.rotation = 0;
// ground
}
}
This is the method that we use to make the player jump. A jump is just a
velocity in the Y-axis. Gravity will do the rest.
We also play a jumping sound and we add a tween to rotate the player while
jumping.
jump() {
if (!this.player.body.blocked.down) return;
this.player.body.setVelocityY(-300);

this.playAudio("jump");
this.jumpTween = this.tweens.add({
targets: this.player,
duration: 1000,
angle: { from: 0, to: 360 },
repeat: -1,
});
}
What should we do when we finish the game scene?
• Stop the theme music
• Play the dead sound
• Set the score in the registry to show it in the gameover scene.
• Start the gameover scene.
finishScene() {
this.theme.stop();

23
this.playAudio("dead");
this.registry.set("score", "" + this.score);
this.scene.start("gameover");
}
This method is called every 100ms and it is used to update the score and
show it on the screen.
updateScore(points = 1) {
this.score += points;
this.scoreText.setText(this.score);
}
}
List: game.js
If the player dies, we store the point and we open the scene explained next:
GameOver.

Gameover

Figure 2: Runner game over screen

When the user fails, this is the scene that we show him. It’s rather simple:
we recover the points to show them and we set up an input listener so when

24
the user just clicks we send him back to the game scene.
export default class GameOver extends Phaser.Scene {
constructor() {
super({ key: "gameover" });
}

create() {
this.width = this.sys.game.config.width;
this.height = this.sys.game.config.height;
this.center_width = this.width / 2;
this.center_height = this.height / 2;

this.cameras.main.setBackgroundColor(0x87ceeb);

this.add
.bitmapText(
this.center_width,
50,
"arcade",
this.registry.get("score"),
25
)
.setOrigin(0.5);
this.add
.bitmapText(
this.center_width,
this.center_height,
"arcade",
"GAME OVER",
45
)
.setOrigin(0.5);
this.add
.bitmapText(
this.center_width,
250,
"arcade",

25
"Press SPACE or Click to restart!",
15
)
.setOrigin(0.5);
this.input.keyboard.on("keydown-SPACE", this.startGame, this);
this.input.on("pointerdown", (pointer) => this.startGame(), this);
}

showLine(text, y) {
let line = this.introLayer.add(
this.add
.bitmapText(this.center_width, y, "pixelFont", text, 25)
.setOrigin(0.5)
.setAlpha(0)
);
this.tweens.add({
targets: line,
duration: 2000,
alpha: 1,
});
}

startGame() {
this.scene.start("game");
}
}
List: gameover.js
Even in simple games like this, it’s important to have some kind of challenge,
that’s why you must show the points on this screen.

Other runners or similar games


Make Way
Source code: https://github.com/pxai/phasergames/tree/master/cars
Play it here: https://pello.itch.io/make-way

26
Goblin Bakery
Source code: https://github.com/pxai/phasergames/tree/master/goblin
Play it here: https://pello.itch.io/goblin-bakery

Melt Down
Source code: https://github.com/pxai/phasergames/tree/master/penguin
Play it here: https://pello.itch.io/meltdown

Electron
Source code: https://github.com/pxai/phasergames/tree/master/electron
Play it here: https://pello.itch.io/electron

27
2. Space Shooter: Starshake

Figure 3: Shooter game screen

Source code: https://github.com/phaserjs/phaser-by-example/tree/main/starshake


Play it here: https://pello.itch.io/starshake
Space shooters are the reason I got into this: when I was just a kid I saw a
Galaga machine and I was in awe, thinking how that thing was even possible.
But enough of nostalgia and let’s get down to code.
In a shooter, we take control of a space fighter that shoots its way through
waves of foe ships. One single hit by the enemy will take us down but with a
bit of skill and the help of some power-ups, we can make it to the final boss.
So in this type of game, we’ll need to create a player without any gravity,
capable of shooting. Every one of those shots is also a GameObject. Different

28
types of enemy waves will be generated and in some cases, they will be
grouped in formation or following a pattern. The stage is just an infinite
background that scrolls down the screen.

Init project
In this case, we’ll be adding some more scenes like transitions and most
importantly an awesome (well, not that much) Splash scene.
import Phaser from "phaser";
import Bootloader from "./scenes/bootloader";
import Outro from "./scenes/outro";
import Splash from "./scenes/splash";
import Transition from "./scenes/transition";
import Game from "./scenes/game";

const config = {
width: 1000,
height: 800,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
autoRound: false,
parent: "contenedor",
physics: {
default: "arcade",
arcade: {
gravity: { y: 0 },
debug: false,
},
},
scene: [Bootloader, Splash, Transition, Game, Outro],
};

const game = new Phaser.Game(config);


List: main.js

29
Loader
Again, this is the first Scene class that we run, just to load all the assets of
the game while we show a progress bar.
export default class Bootloader extends Phaser.Scene {
constructor() {
super({ key: "bootloader" });
}
Here we split the loading of the assets into different functions.
preload() {
this.createBars();
this.setLoadEvents();
this.loadFonts();
this.loadImages();
this.loadAudios();
this.loadSpritesheets();
this.setRegistry();
}
These are the events we need to control the loading bar and change to splash
scene when complete.
setLoadEvents() {
this.load.on(
"progress",
function (value) {
this.progressBar.clear();
this.progressBar.fillStyle(0x0088aa, 1);
this.progressBar.fillRect(
this.cameras.main.width / 4,
this.cameras.main.height / 2 - 16,
(this.cameras.main.width / 2) * value,
16
);
},
this
);

30
this.load.on(
"complete",
() => {
this.scene.start("splash");
},
this
);
}
Load the fonts we use in the game.
loadFonts() {
this.load.bitmapFont(
"wendy",
"assets/fonts/wendy.png",
"assets/fonts/wendy.xml"
);
}
Load the images we use in the game.
loadImages() {
this.load.image("logo", "assets/images/logo.png");
this.load.image("pello_logo", "assets/images/pello_logo.png");
this.load.image("background", "assets/images/background.png");
Array(4)
.fill(0)
.forEach((_, i) => {
this.load.image(`stage${i + 1}`, `assets/images/stage${i + 1}.png`);
});
}
Load the audio (sound effects and music) we use in the game.
loadAudios() {
this.load.audio("shot", "assets/sounds/shot.mp3");
this.load.audio("foeshot", "assets/sounds/foeshot.mp3");
this.load.audio("foedestroy", "assets/sounds/foedestroy.mp3");
this.load.audio("foexplosion", "assets/sounds/foexplosion.mp3");
this.load.audio("explosion", "assets/sounds/explosion.mp3");

31
this.load.audio("stageclear1", "assets/sounds/stageclear1.mp3");
this.load.audio("stageclear2", "assets/sounds/stageclear2.mp3");
this.load.audio("boss", "assets/sounds/boss.mp3");
this.load.audio("splash", "assets/sounds/splash.mp3");
Array(3)
.fill(0)
.forEach((_, i) => {
this.load.audio(`music${i + 1}`, `assets/sounds/music${i + 1}.mp3`);
});
}
Load the sprite sheets (animated images) we use in the game.
loadSpritesheets() {
this.load.spritesheet("player1", "assets/images/player1.png", {
frameWidth: 64,
frameHeight: 64,
});
this.load.spritesheet("foe0", "assets/images/foe0.png", {
frameWidth: 64,
frameHeight: 64,
});
this.load.spritesheet("foe1", "assets/images/foe1.png", {
frameWidth: 64,
frameHeight: 64,
});
this.load.spritesheet("foe2", "assets/images/foe2.png", {
frameWidth: 32,
frameHeight: 32,
});
this.load.spritesheet("guinxu", "assets/images/guinxu.png", {
frameWidth: 128,
frameHeight: 144,
});
this.load.spritesheet("plenny0", "assets/images/plenny0.png", {
frameWidth: 64,
frameHeight: 64,
});
}

32
Set the initial values of the registry. The game was designed to be played by
two players, but it can be played by one.
setRegistry() {
this.registry.set("score_player1", 0);
this.registry.set("power_player1", "water");
this.registry.set("lives_player1", 0);

this.registry.set("score_player2", 0);
this.registry.set("power_player2", "water");
this.registry.set("lives_player2", 0);
}
Create the bars we use to show the loading progress.
createBars() {
this.loadBar = this.add.graphics();
this.loadBar.fillStyle(0xd40000, 1);
this.loadBar.fillRect(
this.cameras.main.width / 4 - 2,
this.cameras.main.height / 2 - 18,
this.cameras.main.width / 2 + 4,
20
);
this.progressBar = this.add.graphics();
}
}
List: bootloader.js
It is not mandatory to split the code into different functions, but when you
have several assets of different types and special events, it will be better if
you organize it like this.

Light Particle
We’ll be showing particles for trails and other elements that will improve the
feedback and the game feel.
export class LightParticle extends Phaser.GameObjects.PointLight {
constructor(scene, x, y, color = 0xffffff, radius = 5, intensity = 0.5) {

33
super(scene, x, y, color, radius, intensity);
this.name = "celtic";
this.scene = scene;
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setAllowGravity(false);
this.body.setVelocityY(300);
this.init();
}
We add a tween to the particle to make it grow and fade out.
init() {
this.scene.tweens.add({
targets: this,
duration: Phaser.Math.Between(600, 1000),
scale: { from: 1, to: 3 },
alpha: { from: this.alpha, to: 0 },
onComplete: () => {
this.destroy();
},
});
}
}
List: particle.js

Generating foes
The enemies are generated automatically with some frequency. This class
takes care of the Foe generation at every stage.
import Foe from "./foe";

export default class FoeGenerator {


constructor(scene) {
this.scene = scene;
this.waveFoes = [];
this.generate();
this.activeWave = false;

34
this.waves = 0;
}
This is the main function to generate foes. Depending on the scene number,
it will generate different foes.
generate() {
if (this.scene.number === 4) {
this.scene.time.delayedCall(2000, () => this.releaseGuinxu(), null, this);
} else {
this.generateEvent1 = this.scene.time.addEvent({
delay: 7000,
callback: () => this.orderedWave(),
callbackScope: this,
loop: true,
});
this.generateEvent2 = this.scene.time.addEvent({
delay: 15000,
callback: () => this.wave(),
callbackScope: this,
loop: true,
});
if (this.scene.number > 1)
this.generateEvent3 = this.scene.time.addEvent({
delay: 3000,
callback: () => this.tank(),
callbackScope: this,
loop: true,
});
if (this.scene.number > 2)
this.generateEvent4 = this.scene.time.addEvent({
delay: 5000,
callback: () => this.slider(),
callbackScope: this,
loop: true,
});
}
}

35
This is the function that generates the boss.
releaseGuinxu() {
const guinxu = new Foe(
this.scene,
Phaser.Math.Between(200, 600),
200,
"guinxu",
0,
20
);
this.scene.playAudio("boss");
this.laughterEvent = this.scene.time.addEvent({
delay: 10000,
callback: () => {
this.scene.playAudio("boss");
},
callbackScope: this,
loop: true,
});
this.scene.tweens.add({
targets: guinxu,
alpha: { from: 0.3, to: 1 },
duration: 200,
repeat: 10,
});
this.scene.foeGroup.add(guinxu);
}
This is the function that stops the generation of foes.
stop() {
clearInterval(this.generationIntervalId);
this.scene.foeGroup.children.entries.forEach((foe) => {
if (foe === null || !foe.active) return;
foe.destroy();
});
}
This is called when the scene is finished and it takes care of destroying the

36
generation events.
finishScene() {
this.generateEvent1.destroy();
this.generateEvent2.destroy();
if (this.scene.number > 1) this.generateEvent3.destroy();
if (this.scene.number > 2) this.generateEvent4.destroy();
this.scene.endScene();
}
This is the function that creates the path for the foes to follow in formation.
createPath() {
this.waves++;
if (this.waves === 3) this.finishScene();
const start = Phaser.Math.Between(100, 600);
this.path = new Phaser.Curves.Path(start, 0);

this.path.lineTo(start, Phaser.Math.Between(20, 50));

let max = 8;
let h = 500 / max;

for (let i = 0; i < max; i++) {


if (i % 2 === 0) {
this.path.lineTo(start, 50 + h * (i + 1));
} else {
this.path.lineTo(start + 300, 50 + h * (i + 1));
}
}

this.path.lineTo(start, this.scene.height + 50);


this.graphics = this.scene.add.graphics();
this.graphics.lineStyle(0, 0xffffff, 0); // for debug
}
This is the function that generates a wave of foes in an ordered formation.
orderedWave(difficulty = 5) {
const x = Phaser.Math.Between(64, this.scene.width - 200);

37
const y = Phaser.Math.Between(-100, 0);
const minus = Phaser.Math.Between(-1, 1) > 0 ? 1 : -1;

Array(difficulty)
.fill()
.forEach((_, i) => this.addOrder(i, x, y, minus));
}
This function just creates a simple wave of foes.
wave(difficulty = 5) {
this.createPath();
const x = Phaser.Math.Between(64, this.scene.width - 200);
const y = Phaser.Math.Between(-100, 0);
const minus = Phaser.Math.Between(-1, 1) > 0 ? 1 : -1;

Array(difficulty)
.fill()
.forEach((_, i) => this.addToWave(i));
this.activeWave = true;
}
This function generates a single tank foe.
tank() {
this.scene.foeGroup.add(
new Foe(this.scene, Phaser.Math.Between(100, 600), -100, "foe2", 0, 620)
);
}
This generates a slider foe and adds a rotation tween to it.
slider() {
let velocity = -200;
let x = 0;
if (Phaser.Math.Between(-1, 1) > 0) {
velocity = 200;
x = -100;
} else {
x = this.scene.width + 100;
}

38
const foe = new Foe(
this.scene,
x,
Phaser.Math.Between(100, 600),
"foe1",
velocity,
0
);
this.scene.tweens.add({
targets: [foe, foe.shadow],
duration: 500,
rotation: "+=5",
repeat: -1,
});
this.scene.foeGroup.add(foe);
}
This function adds a foe to the scene, in a random position.
add() {
const foe = new Foe(
this.scene,
Phaser.Math.Between(32, this.scene.width - 32),
0
);
this.scene.foeGroup.add(foe);
}
This function generates and ordered group of foes.
addOrder(i, x, y, minus) {
const offset = minus * 70;

this.scene.foeGroup.add(
new Foe(this.scene, x + i * 70, i * y + offset, "foe0", 0, 300)
);
}
This function adds a foe to the wave.
addToWave(i) {

39
const foe = new Foe(
this.scene,
Phaser.Math.Between(32, this.scene.width - 32),
0,
"foe0"
);
this.scene.tweens.add({
targets: foe,
z: 1,
ease: "Linear",
duration: 12000,
repeat: -1,
delay: i * 100,
});
this;
this.scene.foeWaveGroup.add(foe);
}
This function updates all foes in the scene. This could be done independently
in each foe as we will see in other projects.
update() {
if (this.path) {
this.path.draw(this.graphics);

this.scene.foeWaveGroup.children.entries.forEach((foe) => {
if (foe === null || !foe.active) return;
let t = foe.z;
let vec = foe.getData("vector");
this.path.getPoint(t, vec);
foe.setPosition(vec.x, vec.y);
foe.shadow.setPosition(vec.x + 20, vec.y + 20);
foe.setDepth(foe.y);
});

if (this.activeWave && this.checkIfWaveDestroyed()) {


this.activeWave = false;
this.scene.spawnShake();
this.path.destroy();

40
}
}

this.scene.foeGroup.children.entries.forEach((foe) => {
if (foe === null || !foe.active || foe.y > this.scene.height + 100)
foe.destroy();
foe.update();
});
}
This function checks if the wave of foes has been destroyed so we can generate
a power-up.
checkIfWaveDestroyed() {
const foes = this.scene.foeWaveGroup.children.entries;

return foes.length === foes.filter((foe) => !foe.active).length;


}
}
List: foe_generator.js
In this generator, we can tweak the difficulty level and the randomness. As
we will see in the platformer example, in games using tiled maps, the enemies
can be set on the map configuration.

Foes

Figure 4: Foe sprites

There are different types of foes:


• Regular foe fighters: no armor, always in formation.
• Ufos: single armored ships.
• Tanks: ground shooting armored bases.

41
As we move forward to new stages we’ll meet new foes, but all of them are
grouped here.
import FoeShot from "./foe_shot";
import Explosion from "./explosion";

const TYPES = {
foe0: { points: 400, lives: 1 },
foe1: { points: 500, lives: 3 },
foe2: { points: 800, lives: 2 },
guinxu: { points: 10000, lives: 20 },
};

class Foe extends Phaser.GameObjects.Sprite {


constructor(scene, x, y, name = "foe0", velocityX = 0, velocityY = 0) {
super(scene, x, y, name);
this.name = name;
this.points = TYPES[name].points;
this.lives = TYPES[name].lives;
this.id = Math.random();
if (this.name !== "foe2") {
this.spawnShadow(x, y);
}
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setAllowGravity(false);
this.body.setCircle(19);
this.body.setOffset(12, 12);
this.body.setVelocityX(velocityX);
this.body.setVelocityY(velocityY);
this.setData("vector", new Phaser.Math.Vector2());
if (this.name === "guinxu") {
this.setGuinxuShot();
}
this.init();
}
This function sets a tween to the Guinxu foe, so it moves in a zig-zag pattern.

42
setGuinxuShot() {
this.patternIndex = 0;
this.pattern = Phaser.Utils.Array.NumberArrayStep(-300, 300, 50);
this.pattern = this.pattern.concat(
Phaser.Utils.Array.NumberArrayStep(300, -300, -50)
);
this.scene.tweens.add({
targets: this,
duration: 2000,
y: { from: this.y, to: this.y + Phaser.Math.Between(100, -100) },
x: { from: this.x, to: this.x + Phaser.Math.Between(100, -100) },
yoyo: true,
repeat: -1,
});
}
This function spawns a shadow for each foe. We’ll have to update it with
the foe itself.
spawnShadow(x, y) {
this.shadow = this.scene.add
.image(x + 20, y + 20, this.name)
.setScale(0.7)
.setTint(0x000000)
.setAlpha(0.4);
}

updateShadow() {
this.shadow.x = this.x + 20;
this.shadow.y = this.y + 20;
}
This function adds an animation to the foe.
init() {
this.scene.anims.create({
key: this.name,
frames: this.scene.anims.generateFrameNumbers(this.name),
frameRate: 10,
repeat: -1,

43
});
this.anims.play(this.name, true);
this.direction = -1;
}
This function is called from the foe generation. It updates the foe position,
checks if it’s out of bounds and also updates its shadow.
update() {
if (this.y > this.scene.height + 64) {
if (this.name !== "foe2") this.shadow.destroy();
this.destroy();
}

if (this.name === "guinxu" && Phaser.Math.Between(1, 6) > 5) {


this.guinxuShot();
} else if (Phaser.Math.Between(1, 101) > 100) {
if (!this.scene || !this.scene.player) return;
this.scene.playAudio("foeshot");
let shot = new FoeShot(this.scene, this.x, this.y, "foe", this.name);
this.scene.foeShots.add(shot);
this.scene.physics.moveTo(
shot,
this.scene.player.x,
this.scene.player.y,
300
);
this.scene.physics.moveTo(
shot.shadow,
this.scene.player.x,
this.scene.player.y,
300
);
}

if (this.name !== "foe2") {


this.updateShadow();
}
}

44
This takes care of the shots generated by the final boss.
guinxuShot() {
if (!this.scene || !this.scene.player) return;

this.scene.playAudio("foeshot");
let shot = new FoeShot(
this.scene,
this.x,
this.y,
"foe",
this.name,
this.pattern[this.patternIndex],
300
);
this.scene.foeShots.add(shot);
this.patternIndex =
this.patternIndex + 1 === this.pattern.length ? 0 : ++this.patternIndex;
}
This function is called when the foe is destroyed, adding an explosion effect
along with a tween and showing the points.
dead() {
let radius = 60;
let explosionRad = 20;
if (this.name === "guinxu") {
radius = 220;
explosionRad = 220;
this.scene.cameras.main.shake(500);
}

const explosion = this.scene.add


.circle(this.x, this.y, 5)
.setStrokeStyle(20, 0xffffff);
this.showPoints(this.points);
this.scene.tweens.add({
targets: explosion,
radius: { from: 10, to: radius },

45
alpha: { from: 1, to: 0.3 },
duration: 250,
onComplete: () => {
explosion.destroy();
},
});

new Explosion(this.scene, this.x, this.y, explosionRad);


if (
this.name !== "foe2" &&
this.scene &&
this.scene.scene.isActive() &&
this.shadow &&
this.shadow.active
)
this.shadow.destroy();

if (this.name === "guinxu") {


this.scene.number = 5;
this.scene.playAudio("explosion");
this.scene.endScene();
}
this.destroy();
}
As we do when destroying shots, this function shows the points when a foe
is destroyed with a simple tween effect.
showPoints(score, color = 0xff0000) {
let text = this.scene.add
.bitmapText(this.x + 20, this.y - 30, "wendy", "+" + score, 40, color)
.setOrigin(0.5);
this.scene.tweens.add({
targets: text,
duration: 800,
alpha: { from: 1, to: 0 },
y: { from: this.y - 20, to: this.y - 80 },
onComplete: () => {
text.destroy();

46
},
});
}
}

export default Foe;


List: foe.js
It could be ok to split this class into different subclasses for each type of
enemy.

Foes shot
Each foe shot is a game object controlled by this class.
const TYPES = {
chocolate: { color: 0xaf8057, radius: 16, intensity: 0.4 },
vanila: { color: 0xfff6d5, radius: 16, intensity: 0.4 },
fruit: { color: 0x00ff00, radius: 16, intensity: 0.4 },
water: { color: 0x0000cc, radius: 16, intensity: 0.4 },
foe: { color: 0xfff01f, radius: 16, intensity: 0.4 },
};

class FoeShot extends Phaser.GameObjects.PointLight {


constructor(
scene,
x,
y,
type = "water",
playerName,
velocityX = 0,
velocityY = -300
) {
const { color, radius, intensity } = TYPES[type];
super(scene, x, y, color, radius, intensity);
this.name = "foeshot";
this.scene = scene;
this.playerName = playerName;

47
this.spawnShadow(x, y, velocityX, velocityY);
scene.add.existing(this);
scene.physics.add.existing(this);
if (playerName === "guinxu") this.body.setVelocity(velocityX, velocityY);
this.body.setAllowGravity(false);
this.body.setCollideWorldBounds(true);
this.body.onWorldBounds = true;
this.body.setCircle(10);
this.body.setOffset(6, 9);

this.init();
}
This function spawns a shadow for each shot. We’ll have to update it with
the shot itself.
spawnShadow(x, y, velocityX, velocityY) {
this.shadow = this.scene.add
.circle(x + 20, y + 20, 10, 0x000000)
.setAlpha(0.4);
this.scene.add.existing(this.shadow);
this.scene.physics.add.existing(this.shadow);
if (this.playerName === "guinxu")
this.shadow.body.setVelocity(velocityX, velocityY);
}
This function adds a simple effect to the shot to make it flicker.
init() {
this.scene.tweens.add({
targets: this,
duration: 200,
intensity: { from: 0.3, to: 0.7 },
repeat: -1,
});
}
This function is called when the shot is destroyed, adding an explosion effect
along with a tween and showing the points.
shot() {

48
const explosion = this.scene.add
.circle(this.x, this.y, 5)
.setStrokeStyle(10, 0xffffff);
this.showPoints(50);
this.scene.tweens.add({
targets: explosion,
radius: { from: 5, to: 20 },
alpha: { from: 1, to: 0 },
duration: 250,
onComplete: () => {
explosion.destroy();
},
});
this.destroy();
}
This function shows the points when the shot is destroyed. The points are
shown in a bitmap text and they are tweened to make them move up and
fade out.
showPoints(score, color = 0xff0000) {
let text = this.scene.add
.bitmapText(this.x + 20, this.y - 30, "wendy", "+" + score, 40, color)
.setOrigin(0.5);
this.scene.tweens.add({
targets: text,
duration: 800,
alpha: { from: 1, to: 0 },
y: { from: this.y - 20, to: this.y - 80 },
onComplete: () => {
text.destroy();
},
});
}
}

export default FoeShot;


List: foe_shot.js

49
If you improve this game further, you could have a different type of shot for
each type of foe.

Explosion
Explosions are represented by a white ring that grows quickly as it vanishes.
There are different sizes but in any case, they are crucial for satisfactory
feedback.
class Explosion {
constructor(scene, x, y, radius = 5, min = 5, max = 7) {
this.scene = scene;
this.radius = radius;
this.x = x;
this.y = y;
this.lights = Array(Phaser.Math.Between(min, max))
.fill(0)
.map((_, i) => {
const offsetX =
this.x + Phaser.Math.Between(-this.radius / 2, this.radius / 2);
const offsetY =
this.y + Phaser.Math.Between(-this.radius / 2, -this.radius / 2);
const color = Phaser.Math.Between(0xff0000, 0xffffcc);
const radius = Phaser.Math.Between(this.radius / 2, this.radius);
const intensity = Phaser.Math.Between(0.3, 0.8);
return scene.lights.addPointLight(
offsetX,
offsetY,
color,
radius,
intensity
);
});
this.init();
}
This adds a simple effect to the explosion to shrink the lights.
init() {

50
this.scene.tweens.add({
targets: this.lights,
duration: Phaser.Math.Between(600, 1000),
scale: { from: 1, to: 0 },
});
}
}

export default Explosion;


List: explosion.js
You should also consider having specific explosions or more randomness to
the explosion effects.

Player

Figure 5: Player sprites

And here is the player. We define the animations for the following: left-right
turnings, the controls, we generate a fume trail and of course, we can let him
fire and apply shooting power-ups.
import Explosion from "./explosion";
import { LightParticle } from "./particle";
import ShootingPatterns from "./shooting_patterns";

class Player extends Phaser.GameObjects.Sprite {


constructor(scene, x, y, name = "player1", powerUp = "water") {
super(scene, x, y, name);
this.name = name;
this.spawnShadow(x, y);
this.powerUp = powerUp;
this.id = Math.random();

51
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setCollideWorldBounds(true);
this.body.setAllowGravity(false);
this.body.setCircle(26);
this.body.setOffset(6, 9);
this.power = 0;
this.blinking = false;
this.shootingPatterns = new ShootingPatterns(this.scene, this.name);
this.init();
this.setControls();
}
We add a shadow to the player, and we’ll have to update its position with
the player. Alternatively, we could have defined a Container with the player
and the shadow.
spawnShadow(x, y) {
this.shadow = this.scene.add
.image(x + 20, y + 20, "player1")
.setTint(0x000000)
.setAlpha(0.4);
}
We set the animations for the player. We’ll have 3 animations: one for the
idle state, one for moving right, and one for moving left.
init() {
this.scene.anims.create({
key: this.name,
frames: this.scene.anims.generateFrameNumbers(this.name, {
start: 0,
end: 0,
}),
frameRate: 10,
repeat: -1,
});
this.scene.anims.create({
key: this.name + "right",
frames: this.scene.anims.generateFrameNumbers(this.name, {

52
start: 1,
end: 1,
}),
frameRate: 10,
repeat: -1,
});
this.scene.anims.create({
key: this.name + "left",
frames: this.scene.anims.generateFrameNumbers(this.name, {
start: 2,
end: 2,
}),
frameRate: 10,
repeat: -1,
});
this.anims.play(this.name, true);

this.upDelta = 0;
}
We set the controls for the player. We’ll use the cursor keys and WASD keys
to move the player, and the space bar to shoot.
setControls() {
this.SPACE = this.scene.input.keyboard.addKey(
Phaser.Input.Keyboard.KeyCodes.SPACE
);
this.cursor = this.scene.input.keyboard.createCursorKeys();
this.W = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W);
this.A = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);
this.S = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S);
this.D = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);
}
This will be called when the player shoots. We’ll play a sound, and then call
the shoot method of the current shooting pattern.
shoot() {
this.scene.playAudio("shot");
this.shootingPatterns.shoot(this.x, this.y, this.powerUp);

53
}
This is the game loop for the player. We’ll check if the player is moving, and
if so, we’ll play the corresponding animation. We’ll also check if the player
is shooting, and if so, we’ll call the shoot method.
update(timestep, delta) {
if (this.death) return;
if (this.cursor.left.isDown) {
this.x -= 5;
this.anims.play(this.name + "left", true);
this.shadow.setScale(0.5, 1);
} else if (this.cursor.right.isDown) {
this.x += 5;
this.anims.play(this.name + "right", true);
this.shadow.setScale(0.5, 1);
} else {
this.anims.play(this.name, true);
this.shadow.setScale(1, 1);
}

if (this.cursor.up.isDown) {
this.y -= 5;
} else if (this.cursor.down.isDown) {
this.y += 5;
}

if (Phaser.Input.Keyboard.JustDown(this.SPACE)) {
this.shoot();
}
this.scene.trailLayer.add(
new LightParticle(this.scene, this.x, this.y, 0xffffff, 10)
);
this.updateShadow();
}
We update the shadow position to follow the player.
updateShadow() {
this.shadow.x = this.x + 20;

54
this.shadow.y = this.y + 20;
}
Every time the player destroys a foe or a shot we show the points. We’ll use
a bitmap text for that.
showPoints(score, color = 0xff0000) {
let text = this.scene.add
.bitmapText(this.x + 20, this.y - 30, "starshipped", score, 20, 0xfffd37)
.setOrigin(0.5);
this.scene.tweens.add({
targets: text,
duration: 2000,
alpha: { from: 1, to: 0 },
y: { from: text.y - 10, to: text.y - 100 },
});
}
This will be called when the player dies: we’ll show an explosion, shake the
camera, and destroy the player.
dead() {
const explosion = this.scene.add
.circle(this.x, this.y, 10)
.setStrokeStyle(40, 0xffffff);
this.scene.tweens.add({
targets: explosion,
radius: { from: 10, to: 512 },
alpha: { from: 1, to: 0.3 },
duration: 300,
onComplete: () => {
explosion.destroy();
},
});
this.scene.cameras.main.shake(500);
this.death = true;
this.shadow.destroy();
new Explosion(this.scene, this.x, this.y, 40);
super.destroy();
}

55
}

export default Player;


List: player.js

Shot
These are players’ shots, simple glowing game objects that also include a
shadow.
const TYPES = {
chocolate: { color: 0xaf8057, radius: 16, intensity: 0.4 },
vanila: { color: 0xfff6d5, radius: 16, intensity: 0.4 },
fruit: { color: 0xffffff, radius: 16, intensity: 0.4 },
water: { color: 0xffffff, radius: 16, intensity: 0.4 },
foe: { color: 0x00ff00, radius: 16, intensity: 0.4 },
};

class Shot extends Phaser.GameObjects.PointLight {


constructor(
scene,
x,
y,
type = "water",
playerName,
velocityX = 0,
velocityY = -500
) {
const { color, radius, intensity } = TYPES[type];
super(scene, x, y, color, radius, intensity);
this.name = "shot";
this.playerName = playerName;
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setAllowGravity(false);
this.body.setVelocityX(velocityX);
this.body.setVelocityY(velocityY);
this.body.setCircle(10);

56
this.body.setOffset(6, 9);
this.body.setCollideWorldBounds(true);
this.body.onWorldBounds = true;
this.spawnShadow(x, y, velocityX, velocityY);
this.init();
}
Each shot will have a shadow, which will be a circle with a lower alpha value.
spawnShadow(x, y, velocityX, velocityY) {
this.shadow = this.scene.add
.circle(x + 20, y + 20, 10, 0x000000)
.setAlpha(0.4);
this.scene.add.existing(this.shadow);
this.scene.physics.add.existing(this.shadow);
this.shadow.body.setVelocityX(velocityX);
this.shadow.body.setVelocityY(velocityY);
}
We add a tween to the shot to make it grow and fade out, repeatedly.
init() {
this.scene.tweens.add({
targets: this,
duration: 200,
intensity: { from: 0.3, to: 0.7 },
repeat: -1,
});
}
}

export default Shot;


List: shot.js

Shooting pattern
One of the key element in any shooter (and in many other genres) are power-
ups. Every time the player catches a power-up the firing pattern will change.
Patterns are just different ways of generating shots and this class groups all

57
of them.
import Shot from "./shot";

export default class ShootingPatterns {


constructor(scene, name) {
this.scene = scene;
this.name = name;
this.shootingMethods = {
water: this.single.bind(this),
fruit: this.tri.bind(this),
vanila: this.quintus.bind(this),
chocolate: this.massacre.bind(this),
};
}
These are the different functions we will use to shoot. Each one will shoot
a different number of shots, with different angles and speeds. The patterns
are applied depending on the current power-up.
shoot(x, y, powerUp) {
this.shootingMethods[powerUp](x, y, powerUp);
}

single(x, y, powerUp) {
this.scene.shots.add(new Shot(this.scene, x, y, powerUp, this.name));
}

tri(x, y, powerUp) {
this.scene.shots.add(new Shot(this.scene, x, y, powerUp, this.name, -60));
this.scene.shots.add(new Shot(this.scene, x, y, powerUp, this.name));
this.scene.shots.add(new Shot(this.scene, x, y, powerUp, this.name, 60));
}

quintus(x, y, powerUp) {
this.scene.shots.add(new Shot(this.scene, x, y, powerUp, this.name, -300));
this.scene.shots.add(new Shot(this.scene, x, y, powerUp, this.name, 300));
this.scene.shots.add(
new Shot(this.scene, x, y, powerUp, this.name, -300, 500)

58
);
this.scene.shots.add(
new Shot(this.scene, x, y, powerUp, this.name, 300, 500)
);
}

massacre(x, y, powerUp) {
this.scene.shots.add(
new Shot(this.scene, x, y, powerUp, this.name, 300, 0)
);
this.scene.shots.add(
new Shot(this.scene, x, y, powerUp, this.name, -300, 0)
);
this.scene.shots.add(
new Shot(this.scene, x, y, powerUp, this.name, 0, 500)
);
this.scene.shots.add(new Shot(this.scene, x, y, powerUp, this.name, 30));
this.scene.shots.add(new Shot(this.scene, x, y, powerUp, this.name, 60));
}
}
List: shooting_patterns.js
These are just a few patterns but we could extend this class as much as we
want.

Power Up

Figure 6: Power-up sprites

This is a very basic game object that will be generated when a complete
fighter formation is destroyed. As soon as the player catches it, the power-up
will be applied.

59
class PowerUp extends Phaser.GameObjects.Sprite {
constructor(scene, x, y, name = "plenny0", power = "fruit") {
super(scene, x, y, name);
this.name = name;
this.power = power;
this.scene = scene;
this.id = Math.random();
this.spawnShadow(x, y);
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setAllowGravity(false);
this.body.setCircle(19);
this.body.setOffset(12, 12);
this.body.setVelocityX(-100);
this.init();
}
The power-up also spawns a shadow.
spawnShadow(x, y) {
this.shadow = this.scene.add
.image(x + 20, y + 20, "plenny0")
.setTint(0x000000)
.setAlpha(0.4);
this.scene.physics.add.existing(this.shadow);
this.shadow.body.setVelocityX(-100);
}
This sets the animation and movement of the power-up.
init() {
this.scene.anims.create({
key: this.name,
frames: this.scene.anims.generateFrameNumbers(this.name),
frameRate: 10,
repeat: -1,
});

this.scene.tweens.add({
targets: [this],

60
duration: 5000,
x: { from: this.x, to: 0 },
y: { from: this.y - 10, to: this.y + 10 },
scale: { from: 0.8, to: 1 },
repeat: -1,
yoyo: true,
});

this.scene.tweens.add({
targets: this.shadow,
duration: 5000,
x: { from: this.shadow.x, to: 0 },
y: { from: this.shadow.y - 10, to: this.y + 10 },
scale: { from: 0.8, to: 1 },
repeat: -1,
yoyo: true,
});

this.anims.play(this.name, true);
this.body.setVelocityX(-100);
this.shadow.body.setVelocityX(-100);
this.direction = -1;
}
When this element is destroyed, it will also destroy the shadow.
destroy() {
this.shadow.destroy();
super.destroy();
}
}

export default PowerUp;


List: powerup.js

61
The game!
Here’s the game. A single generic class that takes control of all stages. It
follows the same steps we already saw before, but in this case, it needs to
define more groups (fighters, shots, power-ups,…) and their collisions. We’ll
also keep the score and show points each time an enemy is destroyed.
import FoeGenerator from "../gameobjects/foe_generator";
import Player from "../gameobjects/player";
import PowerUp from "../gameobjects/powerup";
import SceneEffect from "../gameobjects/scene_effect";

export default class Game extends Phaser.Scene {


constructor() {
super({ key: "game" });
this.player = null;
this.score = 0;
this.scoreText = null;
}
We need to initialize the scene with the data we passed from the previous
scene, especially the number of the stage to load the correct background.
Also, we need to get the current power-up from the registry, although we are
not applying it yet.
init(data) {
this.name = data.name;
this.number = data.number;
this.next = data.next;
this.currentPowerUp = +this.registry.get("currentPowerUp");
}
Here we create and start all the elements of the game. We create the back-
ground, the players, the foes, the shots, the power-ups, the scores, the audios
and the colliders.
create() {
this.duration = this.time * 1000;
this.width = this.sys.game.config.width;
this.height = this.sys.game.config.height;

62
this.center_width = this.width / 2;
this.center_height = this.height / 2;
new SceneEffect(this).simpleOpen(() => 0);
this.addBackground();
this.cameras.main.setBackgroundColor(0x333333);
this.lights.enable();
this.lights.setAmbientColor(0x666666);
this.addScores();
this.addFoes();
this.addPlayers();
this.addPowerUps();
this.addShots();
this.loadAudios();
this.addColliders();
}
This is how we create an infinite background. We create a tileSprite with the
size of the screen and we set the origin to 0,0. Then we set the scroll factor
to 0,1 so it will scroll only in the Y axis.
addBackground() {
this.background = this.add
.tileSprite(0, 0, this.width, this.height, "stage" + this.number)
.setOrigin(0)
.setScrollFactor(0, 1);
}
This is the method that will be called from the foe generator when a wave is
destroyed. We create a new power up and we add it to the power-up group.
spawnShake() {
const { x, y } = this.lastDestroyedWaveFoe;
this.shake = new PowerUp(this, x, y);
this.powerUps.add(this.shake);
}
This adds the score text to the scene. We create a group of scores, one for
each player. We add the score text to the group and we set the scroll factor
to 0 so it will not scroll with the camera.
addScores() {

63
this.scores = {
player1: {},
player2: {},
};

this.scores["player1"]["scoreText"] = this.add
.bitmapText(
150,
16,
"wendy",
String(this.registry.get("score_player1")).padStart(6, "0"),
50
)
.setOrigin(0.5)
.setScrollFactor(0);
this.scores["player2"]["scoreText"] = this.add
.bitmapText(this.width - 150, 16, "wendy", "0".padStart(6, "0"), 50)
.setOrigin(0.5)
.setScrollFactor(0);
}
This adds the players to the scene. We create a group of players but in this
particular implementation, we just add one player.
addPlayers() {
this.trailLayer = this.add.layer();
this.players = this.add.group();
this.player = new Player(this, this.center_width, this.center_height);
this.players.add(this.player);
}
Next, we have some functions to add other groups for the game elements.
addShots() {
this.shotsLayer = this.add.layer();
this.shots = this.add.group();
}

addFoes() {
this.foeGroup = this.add.group();

64
this.foeWaveGroup = this.add.group();
this.foeShots = this.add.group();
this.foes = new FoeGenerator(this);
}

addPowerUps() {
this.available = ["fruit", "vanila", "chocolate"];
this.powerUps = this.add.group();
}
Once we have created all groups of elements, we add the colliders between
them.
addColliders() {
this.physics.add.collider(
this.players,
this.foeGroup,
this.crashFoe,
() => {
return true;
},
this
);

this.physics.add.collider(
this.players,
this.foeWaveGroup,
this.crashFoe,
() => {
return true;
},
this
);

this.physics.add.overlap(
this.shots,
this.foeGroup,
this.destroyFoe,
() => {

65
return true;
},
this
);

this.physics.add.overlap(
this.shots,
this.foeWaveGroup,
this.destroyWaveFoe,
() => {
return true;
},
this
);

this.physics.add.collider(
this.players,
this.powerUps,
this.pickPowerUp,
() => {
return true;
},
this
);

this.physics.add.overlap(
this.players,
this.foeShots,
this.hitPlayer,
() => {
return true;
},
this
);

this.physics.add.collider(
this.shots,
this.foeShots,

66
this.destroyShot,
() => {
return true;
},
this
);
this.physics.world.on("worldbounds", this.onWorldBounds);
}
This is the callback for the world bounds and we will use it to destroy elements
that the game does not need anymore. We check if the element is a shot and
if it is, we destroy it. We also destroy the shadow of the shot. We do this
because the shadow is not a child of the shot, so it will not be destroyed
automatically.
onWorldBounds(body, t) {
const name = body.gameObject.name.toString();
if (["foeshot", "shot"].includes(name)) {
body.gameObject.shadow.destroy();
body.gameObject.destroy();
}
}
This is the callback for the collision between two shots. We destroy both
shots and we create an explosion where they meet.
destroyShot(shot, foeShot) {
const point = this.lights.addPointLight(shot.x, shot.y, 0xffffff, 10, 0.7);
this.tweens.add({
targets: point,
duration: 400,
scale: { from: 1, to: 0 },
});
this.playAudio("foexplosion");
shot.shadow.destroy();
shot.destroy();
foeShot.shadow.destroy();
foeShot.shot();
this.updateScore(shot.playerName, 50);
}

67
This is called when we destroy a foe that is part of a wave.
destroyWaveFoe(shot, foe) {
this.lastDestroyedWaveFoe = { x: foe.x, y: foe.y };
this.destroyFoe(shot, foe);
}
This is the callback we call when a shot hits a foe. We destroy the shot and
we decrease the lives of the foe. If the foe has no more lives, we destroy it
and we create an explosion. We also add the points to the score of the player
who shoots the foe.
destroyFoe(shot, foe) {
foe.lives--;
this.playAudio("foexplosion");
const point = this.lights.addPointLight(shot.x, shot.y, 0xffffff, 10, 0.7);
this.tweens.add({
targets: point,
duration: 400,
scale: { from: 1, to: 0 },
});
this.tweens.add({
targets: foe,
duration: 400,
tint: { from: 0xffffff, to: 0xff0000 },
});
this.updateScore(shot.playerName, 50);
this.tweens.add({ targets: foe, y: "-=10", yoyo: true, duration: 100 });

shot.destroy();
if (foe.lives === 0) {
this.playAudio("foedestroy");
const point = this.lights.addPointLight(
shot.x,
shot.y,
0xffffff,
10,
0.7
);

68
this.tweens.add({
targets: point,
duration: 400,
scale: { from: 1, to: 0 },
});
this.updateScore(shot.playerName, foe.points);
foe.dead();
}
}
This one is called when a foe shot hits the player. Unless the player is blinking
(because it just started), we destroy the player and we create an explosion.
We also destroy the shadow of the shot. Then we respawn the player
hitPlayer(player, shot) {
if (player.blinking) return;

this.players.remove(this.player);
player.dead();
this.playAudio("explosion");
shot.shadow.destroy();
shot.destroy();
this.time.delayedCall(1000, () => this.respawnPlayer(), null, this);
}
This one is called when a player crashes with a foe. Unless the player is
blinking (because it just started), we destroy the player, and the foe and also
at the end we respawn the player.
crashFoe(player, foe) {
if (player.blinking) return;
player.dead();
this.playAudio("explosion");
foe.dead();
this.time.delayedCall(1000, () => this.respawnPlayer(), null, this);
}
This is the callback when the player picks a powerup. We update the power-
up of the player and we destroy the power-up. We also create a tween to
make the player blink.

69
pickPowerUp(player, powerUp) {
this.playAudio("stageclear1");
this.updatePowerUp(player, powerUp);
this.tweens.add({
targets: player,
duration: 200,
alpha: { from: 0.5, to: 1 },
scale: { from: 1.4, to: 1 },
repeat: 3,
});
powerUp.destroy();
}
This adds a player to the game. We create a tween to make the player blink
and then we create a new player.
respawnPlayer() {
this.player = new Player(this, this.center_width, this.center_height);
this.player.blinking = true;
this.players.add(this.player);
this.tweens.add({
targets: this.player,
duration: 100,
alpha: { from: 0, to: 1 },
repeat: 10,
onComplete: () => {
this.player.blinking = false;
},
});
}
Here we load all the audio, and we add them to the this.audios object.
Later we can play them with the playAudio method.
loadAudios() {
this.audios = {
shot: this.sound.add("shot"),
foeshot: this.sound.add("foeshot"),
explosion: this.sound.add("explosion"),
foexplosion: this.sound.add("foexplosion"),

70
foedestroy: this.sound.add("foedestroy"),
stageclear1: this.sound.add("stageclear1"),
stageclear2: this.sound.add("stageclear2"),
boss: this.sound.add("boss"),
};
}

playAudio(key) {
this.audios[key].play();
}
The game loop is as simple as this. We update the player and the foes. We
also update the background to make it scroll.
update() {
if (this.player) this.player.update();
this.foes.update();
this.background.tilePositionY -= 10;
}
When the player finishes the stage, we destroy all the elements and we start
the transition to the next scene.
endScene() {
this.foeWaveGroup.children.entries.forEach((foe) => foe.shadow.destroy());
this.foeGroup.children.entries.forEach((foe) => foe.shadow.destroy());
this.shots.children.entries.forEach((shot) => shot.shadow.destroy());
this.foeShots.children.entries.forEach((shot) => shot.shadow.destroy());
this.time.delayedCall(
2000,
() => {
this.finishScene();
},
null,
this
);
}
This is the callback for the end of the scene. We stop all the audio, we stop
the scene and we start the transition to the next scene.

71
finishScene() {
this.game.sound.stopAll();
this.scene.stop("game");
const scene = this.number < 5 ? "transition" : "outro";
this.scene.start(scene, {
next: "game",
name: "STAGE",
number: this.number + 1,
});
}
The power-up looks the same but the effect is different. We keep increasing
its value so we can apply the effect to the player. In this game, the power-up
applies another shooting pattern.
updatePowerUp(player, powerUp) {
player.powerUp = this.available[this.currentPowerUp];
this.currentPowerUp =
this.currentPowerUp + 1 === this.available.length
? this.currentPowerUp
: this.currentPowerUp + 1;
this.registry.set("currentPowerUp", this.currentPowerUp);
}
This is the method we use to update the score of the player. We get the
score from the registry and we update it. We also create a tween to make
the score text blink.
updateScore(playerName, points = 0) {
const score = +this.registry.get("score_" + playerName) + points;
this.registry.set("score_" + playerName, score);
this.scores[playerName]["scoreText"].setText(
String(score).padStart(6, "0")
);
this.tweens.add({
targets: this.scores[playerName]["scoreText"],
duration: 200,
tint: { from: 0x0000ff, to: 0xffffff },
scale: { from: 1.2, to: 1 },
repeat: 2,

72
});
}
}
List: game.js
Every time the player is destroyed, a huge explosion will be displayed.

Scene Effect
This is just a simple curtain effect every time we change the scene.
export default class SceneEffect {
constructor(scene) {
this.scene = scene;
}
This adds a rectangle to the scene, and then we tween it to make it move
from the left to the right.
simpleClose(callback) {
const rectangleWidth = this.scene.width / 2;
const rectangle1 = this.scene.add
.rectangle(
0 - rectangleWidth,
0,
this.scene.width,
this.scene.height,
0x000000
)
.setOrigin(0.5, 0);

this.scene.tweens.add({
targets: rectangle1,
duration: 500,
x: { from: -rectangleWidth / 2, to: rectangleWidth },
onComplete: () => {
callback();
},
});

73
}
This adds a rectangle to the scene, and then we tween it to make it move
from the right to the left.
simpleOpen(callback) {
const rectangleWidth = this.scene.width / 2;
const rectangle1 = this.scene.add
.rectangle(
rectangleWidth,
0,
this.scene.width,
this.scene.height,
0x000000
)
.setOrigin(0.5, 0);

this.scene.tweens.add({
targets: rectangle1,
duration: 500,
x: { from: rectangleWidth, to: -rectangleWidth },
onComplete: () => {
callback();
},
});
}
This adds two rectangles to the scene, and then we tween them to make them
move from the center to the left and right.
close(callback) {
const rectangleWidth = this.scene.width / 2;
const rectangle1 = this.scene.add
.rectangle(
0 - rectangleWidth,
0,
this.scene.width / 2,
this.scene.height,
0x000000
)

74
.setOrigin(0.5, 0);
const rectangle2 = this.scene.add
.rectangle(
this.scene.width,
0,
this.scene.width / 2,
this.scene.height,
0x000000
)
.setOrigin(0, 0);
this.scene.tweens.add(
{
targets: rectangle1,
duration: 1000,
x: { from: -rectangleWidth / 2, to: rectangleWidth / 2 },
},
{
targets: rectangle2,
duration: 1000,
x: { from: this.scene.width, to: rectangleWidth },
onComplete: () => {
callback();
},
}
);
}
}
List: scene_effect.js
This is one of the simplest curtains, but there are several out there you can
apply.

Splash
A good Splash is a key element to sell the game. Not only because it’s the
first impression of the game but also because it will probably be used as a
screenshot or cover for our product. It also sets the tone of your game so do
not choose the elements of these screens lightly.

75
Figure 7: Splash screen

76
import SceneEffect from "../gameobjects/scene_effect";

export default class Splash extends Phaser.Scene {


constructor() {
super({ key: "splash" });
}

create() {
this.width = this.sys.game.config.width;
this.height = this.sys.game.config.height;
this.center_width = this.width / 2;
this.center_height = this.height / 2;
this.addBackground();
this.showLogo();
this.registry.set("currentPowerUp", 0);
this.time.delayedCall(1000, () => this.showInstructions(), null, this);

this.input.keyboard.on(
"keydown-SPACE",
() => this.transitionToChange(),
this
);

this.playMusic();
}
The background, as the game, is a tileSprite, so we can scroll it to make
it look like it’s moving.
addBackground() {
this.background = this.add
.tileSprite(0, 0, this.width, this.height, "background")
.setOrigin(0)
.setScrollFactor(0, 1);
}

update() {
this.background.tilePositionY -= 2;
this.background.tilePositionX += 2;

77
}
We add this effect to change to another screen:
transitionToChange() {
new SceneEffect(this).simpleClose(this.startGame.bind(this));
}

startGame() {
if (this.theme) this.theme.stop();
this.scene.start("transition", {
next: "game",
name: "STAGE",
number: 1,
time: 30,
});
}
We add the logo, and then we tween it to make it move up and down.
showLogo() {
this.gameLogoShadow = this.add
.image(this.center_width, 250, "logo")
.setScale(0.7)
.setOrigin(0.5);
this.gameLogoShadow.setOrigin(0.48);
this.gameLogoShadow.tint = 0x3e4e43;
this.gameLogoShadow.alpha = 0.6;
this.gameLogo = this.add
.image(this.center_width, 250, "logo")
.setScale(0.7)
.setOrigin(0.5);

this.tweens.add({
targets: [this.gameLogo, this.gameLogoShadow],
duration: 500,
y: {
from: -200,
to: 250,
},

78
});

this.tweens.add({
targets: [this.gameLogo, this.gameLogoShadow],
duration: 1500,
y: {
from: 250,
to: 200,
},
repeat: -1,
yoyo: true,
});
}
This is the music for the splash scene. We’ll play it in a loop.
playMusic(theme = "splash") {
this.theme = this.sound.add(theme);
this.theme.stop();
this.theme.play({
mute: false,
volume: 0.5,
rate: 1,
detune: 0,
seek: 0,
loop: true,
delay: 0,
});
}
Here we add the instructions to the scene.
showInstructions() {
this.add
.bitmapText(this.center_width, 450, "wendy", "Arrows to move", 60)
.setOrigin(0.5)
.setDropShadow(3, 4, 0x222222, 0.7);
this.add
.bitmapText(this.center_width, 500, "wendy", "SPACE to shoot", 60)
.setOrigin(0.5)

79
.setDropShadow(3, 4, 0x222222, 0.7);
this.add
.sprite(this.center_width - 95, 598, "pello_logo")
.setOrigin(0.5)
.setScale(0.3)
.setTint(0x000000)
.setAlpha(0.7);
this.add
.sprite(this.center_width - 100, 590, "pello_logo")
.setOrigin(0.5)
.setScale(0.3);

this.add
.bitmapText(this.center_width + 30, 590, "wendy", "PELLO", 50)
.setOrigin(0.5)
.setDropShadow(3, 4, 0x222222, 0.7);
this.space = this.add
.bitmapText(this.center_width, 680, "wendy", "Press SPACE to start", 60)
.setOrigin(0.5)
.setDropShadow(3, 4, 0x222222, 0.7);
this.tweens.add({
targets: this.space,
duration: 300,
alpha: { from: 0, to: 1 },
repeat: -1,
yoyo: true,
});
}
}
List: splash.js
Invest some time in the Splash. I’m sure that you can do it better than me.

Transition page
The transition page is just the previous page we see before the game screen.
It just shows the number of the stage that is coming next for a few seconds.

80
export default class Transition extends Phaser.Scene {
constructor() {
super({ key: "transition" });
}

init(data) {
this.name = data.name;
this.number = data.number;
this.next = data.next;
}
In the transition, we show a message with the current stage and some advice,
and then we load the next scene.
create() {
const messages = [
"Fire at will",
"Beware the tanks",
"Shoot down the UFOs",
"FINAL BOSS",
];

this.width = this.sys.game.config.width;
this.height = this.sys.game.config.height;
this.center_width = this.width / 2;
this.center_height = this.height / 2;
this.sound.add("stageclear2").play();
this.add
.bitmapText(
this.center_width,
this.center_height - 50,
"wendy",
messages[this.number - 1],
100
)
.setOrigin(0.5);
this.add
.bitmapText(
this.center_width,

81
this.center_height + 50,
"wendy",
"Ready player 1",
80
)
.setOrigin(0.5);

this.playMusic("music" + (this.number !== 4 ? this.number : 1));


this.time.delayedCall(2000, () => this.loadNext(), null, this);
}

loadNext() {
this.scene.start(this.next, {
name: this.name,
number: this.number,
time: this.time,
});
}
The music of the stage is loaded and played in this transition.
playMusic(theme = "music1") {
this.theme = this.sound.add(theme);
this.theme.play({
mute: false,
volume: 0.4,
rate: 1,
detune: 0,
seek: 0,
loop: true,
delay: 0,
});
}
}
List: transition.js
Despite the timeout, we let the player skip this scene.

82
Outro
This is just a Scene telling what happens when you finish the game and you
kill the final boss. This is completely optional but if your game has any type
of backstory or message it is worth adding it. After all, this is the scene that
only winners will be able to witness so it feels like a reward.
export default class Outro extends Phaser.Scene {
constructor() {
super({ key: "outro" });
}

create() {
this.width = this.sys.game.config.width;
this.height = this.sys.game.config.height;
this.center_width = this.width / 2;
this.center_height = this.height / 2;
this.introLayer = this.add.layer();
this.splashLayer = this.add.layer();
this.text = [
"Score: " + this.registry.get("score_player1"),
"The evil forces among with",
"their tyrannical leader GUINXU",
"were finally wiped out.",
"Thanks to commander Alva",
"And the powah of the Plenny Shakes",
" - press enter - ",
];
this.showHistory();
this.showPlayer();

this.input.keyboard.on("keydown-ENTER", this.startSplash, this);


}
These are the functions to show the dramatic story of the game, line by line.
showHistory() {
this.text.forEach((line, i) => {
this.time.delayedCall(

83
(i + 1) * 2000,
() => this.showLine(line, (i + 1) * 60),
null,
this
);
});
this.time.delayedCall(4000, () => this.showPlayer(), null, this);
}

showLine(text, y) {
let line = this.introLayer.add(
this.add
.bitmapText(this.center_width, y, "wendy", text, 50)
.setOrigin(0.5)
.setAlpha(0)
);
this.tweens.add({
targets: line,
duration: 2000,
alpha: 1,
});
}
This will just show the “player1” sprite.
showPlayer() {
this.player1 = this.add
.sprite(this.center_width, this.height - 200, "player1")
.setOrigin(0.5);
}
This will start the splash screen.
startSplash() {
this.scene.start("splash");
}
}
List: outro.js
This particular one had a joke about some famous Spanish indie developers.

84
Other shooter or similar games
U.F.I.S.H.
Source code: https://github.com/pxai/phasergames/tree/master/ufish
Play it here: https://pello.itch.io/ufish

85
3. Platformer: WallHammer

Figure 8: Platformer game screen

Source code: https://github.com/phaserjs/phaser-by-example/tree/main/wallhammer


Play it here: https://pello.itch.io/wallhammer
It’s time for another genre. Now we’ll create a platformer game. This genre
is very popular and there are many examples out there. We’ll create a very
simple one, but you can easily extend it to create a more complex game.
This game called WallHammer is a classic platformer where you have to
collect coins and avoid bats and zombies. You can block them, but if they
touch you, you’ll die. You can also collect lunchboxes to get power-ups like
speed, better hammer, etc.

86
Another key element of this platformer is that the player can build new
blocks and also destroy them. Also, most of the existing blocks of the scene
are breakable which adds some interesting mechanics that players can take
advantage of.

Tiled
To build the scenes, we are going to use tiled maps with the Tiled editor.
Tiled will allow us to design each scene independently from the code: colliding
platform blocks, background and objects will be easily placed making the
scene design super easy.
Each scene contains at least three basic layers:
• scene: the platform itself where all present elements will collide with
the player and foes.
• objects: the object layer groups locations where we set foes, start and
end points for the player, power-ups and any other element that the
game requires.
• background: some tiled for decoration purposes.
You can learn more about Tiled in the chapter dedicated to assets

Init project
This is where we initialize the game with definitions of scenes that should be
already familiar: Bootloader, Splash, Transition and a generic Game scene.
import Phaser from "phaser";
import Bootloader from "./scenes/bootloader";
import Outro from "./scenes/outro";
import Splash from "./scenes/splash";
import Transition from "./scenes/transition";
import Game from "./scenes/game";

const config = {
width: 1000,
height: 800,
scale: {
mode: Phaser.Scale.FIT,

87
autoCenter: Phaser.Scale.CENTER_BOTH,
},
autoRound: false,
parent: "contenedor",
physics: {
default: "arcade",
arcade: {
gravity: { y: 300 },
debug: true,
},
},
scene: [Bootloader, Splash, Transition, Game, Outro],
};

const game = new Phaser.Game(config);


List: main.js
Again, we’ll apply arcade physics.

Loader
In the loader, apart from images, sprites and sounds, we’ll also add a new
type of element: the tiled maps of the scenes along with the images used for
those maps.
export default class Bootloader extends Phaser.Scene {
constructor() {
super({ key: "bootloader" });
}

preload() {
this.createBars();
this.load.on(
"progress",
function (value) {
this.progressBar.clear();
this.progressBar.fillStyle(0xf09937, 1);
this.progressBar.fillRect(

88
this.cameras.main.width / 4,
this.cameras.main.height / 2 - 16,
(this.cameras.main.width / 2) * value,
16
);
},
this
);

this.load.on(
"complete",
() => {
this.scene.start("splash");
},
this
);

Array(5)
.fill(0)
.forEach((_, i) => {
this.load.audio(`music${i}`, `assets/sounds/music${i}.mp3`);
});

this.load.image("pello", "assets/images/pello.png");
this.load.image("landscape", "assets/images/landscape.png");

this.load.audio("build", "assets/sounds/build.mp3");
this.load.audio("coin", "assets/sounds/coin.mp3");
this.load.audio("death", "assets/sounds/death.mp3");
this.load.audio("jump", "assets/sounds/jump.mp3");
this.load.audio("kill", "assets/sounds/kill.mp3");
this.load.audio("land", "assets/sounds/land.mp3");
this.load.audio("lunchbox", "assets/sounds/lunchbox.mp3");
this.load.audio("prize", "assets/sounds/prize.mp3");
this.load.audio("stone_fail", "assets/sounds/stone_fail.mp3");
this.load.audio("stone", "assets/sounds/stone.mp3");
this.load.audio("foedeath", "assets/sounds/foedeath.mp3");
this.load.audio("stage", "assets/sounds/stage.mp3");

89
this.load.audio("splash", "assets/sounds/splash.mp3");

Array(2)
.fill(0)
.forEach((_, i) => {
this.load.image(`brick${i}`, `assets/images/brick${i}.png`);
});

Array(5)
.fill(0)
.forEach((_, i) => {
this.load.image(
`platform${i + 2}`,
`assets/images/platform${i + 2}.png`
);
});

this.load.bitmapFont(
"pixelFont",
"assets/fonts/mario.png",
"assets/fonts/mario.xml"
);
this.load.spritesheet("walt", "assets/images/walt.png", {
frameWidth: 64,
frameHeight: 64,
});

Array(5)
.fill(0)
.forEach((_, i) => {
this.load.tilemapTiledJSON(`scene${i}`, `assets/maps/scene${i}.json`);
});
this.load.image("softbricks", "assets/maps/softbricks.png");
this.load.image("bricks", "assets/maps/bricks.png");
this.load.image("background", "assets/maps/background.png");

this.load.image("chain", "assets/images/chain.png");

90
this.load.spritesheet("bat", "assets/images/bat.png", {
frameWidth: 32,
frameHeight: 32,
});
this.load.spritesheet("zombie", "assets/images/zombie.png", {
frameWidth: 64,
frameHeight: 64,
});
this.load.spritesheet("coin", "assets/images/coin.png", {
frameWidth: 64,
frameHeight: 64,
});
this.load.spritesheet("lunchbox", "assets/images/lunchbox.png", {
frameWidth: 64,
frameHeight: 64,
});
this.load.spritesheet("hammer", "assets/images/hammer.png", {
frameWidth: 64,
frameHeight: 64,
});
this.load.spritesheet("speed", "assets/images/speed.png", {
frameWidth: 64,
frameHeight: 64,
});
this.load.spritesheet("boots", "assets/images/boots.png", {
frameWidth: 64,
frameHeight: 64,
});
this.load.spritesheet("star", "assets/images/star.png", {
frameWidth: 64,
frameHeight: 64,
});

this.load.bitmapFont(
"hammerfont",
"assets/fonts/hammer.png",
"assets/fonts/hammer.xml"
);

91
this.registry.set("score", 0);
this.registry.set("coins", 0);
}

createBars() {
this.loadBar = this.add.graphics();
this.loadBar.fillStyle(0xca6702, 1);
this.loadBar.fillRect(
this.cameras.main.width / 4 - 2,
this.cameras.main.height / 2 - 18,
this.cameras.main.width / 2 + 4,
20
);
this.progressBar = this.add.graphics();
}
}
List: bootloader.js
To avoid repeating commands, we name the scene assets with numbers so we
can load them using a for loop.

Bat

Figure 9: Bat sprites

The bat is the most common and generic enemy here. These are flying
animated sprites that cross the scene until they find and obstacle or the
player itself. With one touch the player is dead. If they hit any part of the
scene platform they will automatically turn and fly in the opposite direction.
export default class Bat extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y, type = "right") {
super(scene, x, y, "bat");
this.name = "bat";
this.scene.physics.add.existing(this);

92
this.scene.physics.world.enable(this);
this.body.setAllowGravity(false);
this.scene.add.existing(this);
this.direction = type === "right" ? 1 : -1;

this.init();
}
Inits the animations for the bat and starts the movement. We also add a
listener for the animationcomplete event.
init() {
this.scene.anims.create({
key: this.name,
frames: this.scene.anims.generateFrameNumbers(this.name, {
start: 0,
end: 1,
}),
frameRate: 5,
repeat: -1,
});

this.scene.anims.create({
key: this.name + "death",
frames: this.scene.anims.generateFrameNumbers(this.name, {
start: 2,
end: 5,
}),
frameRate: 5,
});

this.anims.play(this.name, true);
this.body.setVelocityX(this.direction * 150);
this.flipX = this.direction > 0;
this.on("animationcomplete", this.animationComplete, this);
}

update() {}

93
Turns the bat around and changes the direction
turn() {
this.direction = -this.direction;
this.flipX = this.direction > 0;
this.body.setVelocityX(this.direction * 150);
}
This kills the bat “nicely” by playing the death animation.
death() {
this.dead = true;
this.body.enable = false;
this.body.rotation = 0;
this.anims.play(this.name + "death");
}
This is called when any animation is completed. If the death animation is
completed, then it destroys the bat.
animationComplete(animation, frame) {
if (animation.key === this.name + "death") {
this.destroy();
}
}
}
List: bat.js
As you see bats are not very smart so we need to place them at specific
heights to be dangerous or just annoying.

Zombie

Figure 10: Zombie sprites

94
Zombies are the dark side of the player: they are also construction workers
but they joined the ranks of the ever-growing army of the undead. They just
walk on the ground and as it happens with the bats, they turn when they
hit an obstacle.
export default class Zombie extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y, type = "right") {
super(scene, x, y, "zombie");
this.name = "zombie";
this.scene = scene;
this.scene.physics.add.existing(this);
this.scene.physics.world.enable(this);
this.body.setAllowGravity(true);
this.scene.add.existing(this);
this.direction = type === "right" ? -1 : 1;

this.init();
}
As we did with the Bat. Inits the animations for the zombies and starts the
movement. We also add a listener for the animationcomplete event.
init() {
this.scene.anims.create({
key: this.name,
frames: this.scene.anims.generateFrameNumbers(this.name, {
start: 0,
end: 2,
}),
frameRate: 5,
repeat: -1,
});

this.scene.anims.create({
key: this.name + "death",
frames: this.scene.anims.generateFrameNumbers(this.name, {
start: 3,
end: 5,
}),

95
frameRate: 5,
});

this.anims.play(this.name, true);
this.body.setVelocityX(this.direction * 100);
this.flipX = this.direction < 0;
this.on("animationcomplete", this.animationComplete, this);
}
Turns the zombie around and changes the direction
turn() {
this.direction = -this.direction;
this.flipX = this.direction < 0;
this.body.setVelocityX(this.direction * 100);
}
This kills the zombie “nicely” by playing the death animation.
death() {
this.dead = true;
this.body.enable = false;
this.body.rotation = 0;
this.anims.play(this.name + "death");
}
Again, when the death animation is completed, then it destroys the zombie.
animationComplete(animation, frame) {
if (animation.key === this.name + "death") {
this.destroy();
}
}
}
List: zombie.js
Both zombies and bats can be blocked placing a block in their face and they
will turn back on collision. Also, the player can kill them with a hammer
blow but they must get dangerously close for that.

96
Turn
Turns are invisible game objects that we can place on the stage so we can
force foes to turn in specific spots.
class Turn extends Phaser.GameObjects.Rectangle {
constructor(scene, x, y, width = 32, height = 32, type = "") {
super(scene, x, y, width, height, 0xffffff);
this.type = type;
this.setAlpha(0);
this.x = x;
this.y = y;
scene.add.existing(this);
scene.physics.add.existing(this);

this.body.immovable = true;
this.body.moves = false;
}

disable() {
this.visible = false;
this.destroy();
}

destroy() {
super.destroy();
}
}

export default Turn;


List: turn.js
With the use of this turn, we can enclose the path of foes without adding
blocks that affect the player.

Lunchbox
Yummy lunchboxes contain random power-ups for the player. They must be
placed in the scene map inside the object layer.

97
Figure 11: Lunchbox sprites

class LunchBox extends Phaser.GameObjects.Sprite {


constructor(scene, x, y, name = "lunchbox") {
super(scene, x, y, name);
this.scene = scene;
this.name = name;
this.setScale(1);
this.setOrigin(0.5);

scene.add.existing(this);
scene.physics.add.existing(this);

this.body.immovable = true;
this.body.moves = false;
this.disabled = false;
this.init();
}
Inits the animations and it adds a little tween effect to make the lunchbox
move up and down.
init() {
this.scene.anims.create({
key: this.name,
frames: this.scene.anims.generateFrameNumbers(this.name, {
start: 0,
end: 0,
}),
frameRate: 1,
});

this.scene.anims.create({

98
key: this.name + "opened",
frames: this.scene.anims.generateFrameNumbers(this.name, {
start: 1,
end: 1,
}),
frameRate: 1,
});

this.anims.play(this.name, true);
this.scene.tweens.add({
targets: this,
duration: 500,
y: this.y - 20,
repeat: -1,
yoyo: true,
});
}
This is called when the player picks the lunchbox. It plays the opened ani-
mation and calls the showPrize method.
pick() {
this.anims.play(this.name + "opened", true);
this.showPrize();
this.disabled = true;
this.scene.time.delayedCall(
1000,
() => {
this.destroy();
this.prizeSprite.destroy();
},
null,
this
);
}
This method picks a random prize and it shows it to the player when picking
the lunchbox. It plays a tween animation and calls the applyPrize method
from the player.

99
showPrize() {
const prize = ["boots", "hammer", "coin", "star", "speed"];
const selectedPrize = Phaser.Math.RND.pick(prize);
this.scene.player.applyPrize(selectedPrize);
this.prizeSprite = this.scene.add
.sprite(this.x, this.y, selectedPrize)
.setOrigin(0.5)
.setScale(0.8);
this.scene.tweens.add({
targets: this.prizeSprite,
duration: 500,
y: { from: this.y, to: this.y - 64 },
onComplete: () => {
this.scene.playAudio("prize");
},
});
}
}

export default LunchBox;


List: lunchbox.js
To pick these lunchboxes the player just needs to walk through them. They
will automatically open and show the power-ups.

Particle
Again we need to show some particles in different moments of the game to
provide proper feedback or consequences of actions For example each time
the player walks or lands after a jump. Also when breaking blocks or adding
new ones. Some particle classes are just squares that shrink and vanish,
others are more complex. This class is used to create the smoke particles.
It’s a simple rectangle that scales down and fades out.

This class is used to create the smoke particles. It’s a simple rectangle that
scales down and fades out.

100
export class Smoke extends Phaser.GameObjects.Rectangle {
constructor(scene, x, y, width, height, color = 0xffffff, gravity = false) {
width = width || Phaser.Math.Between(10, 25);
height = height || Phaser.Math.Between(10, 25);
super(scene, x, y, width, height, color);
scene.add.existing(this);
this.color = color;
this.init();
}

init() {
this.scene.tweens.add({
targets: this,
duration: 800,
scale: { from: 1, to: 0 },
onComplete: () => {
this.destroy();
},
});
}
}

// 0xa13000 red brick


// 0xb03e00 orange brick
// 0xb06f00 golden brick
// 0x4d4d4d grey brick
This is similar to the previous one but it represents smoke of rock that we
will generate when the player breaks something.
export class RockSmoke extends Phaser.GameObjects.Rectangle {
constructor(scene, x, y, width, height, color = 0xffeaab, gravity = false) {
width = width || Phaser.Math.Between(30, 55);
height = height || Phaser.Math.Between(30, 55);
super(scene, x, y, width, height, color);
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setAllowGravity(false);
this.body.setVelocityY(-100);

101
this.init();
}

init() {
this.scene.tweens.add({
targets: this,
duration: 800,
scale: { from: 1, to: 0 },
onComplete: () => {
this.destroy();
},
});
}
}
This is similar to the smoke, but it represents the smoke that comes out when
the player jumps and it has gravity.
export class JumpSmoke extends Phaser.GameObjects.Rectangle {
constructor(scene, x, y, width, height, color = 0xffeaab, gravity = false) {
width = width || Phaser.Math.Between(10, 25);
height = height || Phaser.Math.Between(10, 25);
super(scene, x, y, width, height, color);
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setAllowGravity(false);
this.body.setVelocityX(Phaser.Math.Between(-20, 20));
this.init();
}

init() {
this.scene.tweens.add({
targets: this,
duration: 800,
scale: { from: 1, to: 0 },
onComplete: () => {
this.destroy();
},
});

102
}
}
This represents pieces of rock that we will generate when the player breaks
something.
export class Debris extends Phaser.GameObjects.Rectangle {
constructor(scene, x, y, color = 0xb03e00, width, height, gravity = false) {
width = width || Phaser.Math.Between(15, 30);
height = height || Phaser.Math.Between(15, 30);
super(scene, x, y + 5, width, height, color);
scene.add.existing(this);
scene.physics.add.existing(this);
this.body.setAllowGravity(true);
this.body.setVelocityX(Phaser.Math.Between(-50, 50));
this.body.setVelocityY(width * height);
}
}
List: particle.js

Player code

Figure 12: Player sprites

Here is our hero class. It’s a Phaser Sprite game object with animations and
controls attached to it:
• right/left buttons
• up: to jump
• down: to build a brick.
• space: to hit with the hammer.
In this class, we also have methods to add particle effects when walking or
jumping, and we also apply power-ups by changing some parameters.
import Blow from "./blow";
import Brick from "./brick";

103
import { JumpSmoke } from "./particle";

class Player extends Phaser.GameObjects.Sprite {


constructor(scene, x, y, health = 10) {
super(scene, x, y, "walt");
this.setOrigin(0.5);

this.scene.add.existing(this);
this.scene.physics.add.existing(this);
this.cursor = this.scene.input.keyboard.createCursorKeys();
this.spaceBar = this.scene.input.keyboard.addKey(
Phaser.Input.Keyboard.KeyCodes.SPACE
);
this.down = this.scene.input.keyboard.addKey(
Phaser.Input.Keyboard.KeyCodes.DOWN
);
this.right = true;
this.body.setGravityY(100);
this.body.setSize(48, 60);
this.init();
this.jumping = false;
this.building = false;
this.falling = false;
this.mjolnir = false;
this.walkVelocity = 200;
this.jumpVelocity = -400;
this.invincible = false;

this.health = health;

this.dead = false;

this.W = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W);
this.A = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);
this.S = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S);
this.D = this.scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);
}

104
Inits the animations for the player: init, idle, walk, jump, death, etc… and it
adds a listener for the animationcomplete event.
init() {
this.scene.anims.create({
key: "startidle",
frames: this.scene.anims.generateFrameNumbers("walt", {
start: 0,
end: 1,
}),
frameRate: 3,
repeat: -1,
});

this.scene.anims.create({
key: "playeridle",
frames: this.scene.anims.generateFrameNumbers("walt", {
start: 2,
end: 3,
}),
frameRate: 3,
repeat: -1,
});

this.scene.anims.create({
key: "playerwalk",
frames: this.scene.anims.generateFrameNumbers("walt", {
start: 4,
end: 6,
}),
frameRate: 10,
repeat: -1,
});

this.scene.anims.create({
key: "playerjump",
frames: this.scene.anims.generateFrameNumbers("walt", {
start: 4,

105
end: 4,
}),
frameRate: 1,
});

this.scene.anims.create({
key: "playerhammer",
frames: this.scene.anims.generateFrameNumbers("walt", {
start: 7,
end: 8,
}),
frameRate: 10,
});

this.scene.anims.create({
key: "playerbuild",
frames: this.scene.anims.generateFrameNumbers("walt", {
start: 9,
end: 10,
}),
frameRate: 10,
repeat: 2,
});

this.scene.anims.create({
key: "playerdead",
frames: this.scene.anims.generateFrameNumbers("walt", {
start: 11,
end: 16,
}),
frameRate: 5,
});

this.anims.play("startidle", true);

this.on("animationcomplete", this.animationComplete, this);


}

106

You might also like