PhaserByExample v2 1
PhaserByExample v2 1
Authors 8
Introduction 10
Phaser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
How the book is organized . . . . . . . . . . . . . . . . . . . . . . . 11
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
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
3
Lightning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
Utils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
Splash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
GameOver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
Outro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
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
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
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
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
11
1. Basic game: runner
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],
};
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;
}
}
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();
},
});
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";
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
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.
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
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],
};
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";
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);
let max = 8;
let h = 500 / max;
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);
});
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;
Foes
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 },
};
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();
}
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);
}
45
alpha: { from: 1, to: 0.3 },
duration: 250,
onComplete: () => {
explosion.destroy();
},
});
46
},
});
}
}
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 },
};
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();
},
});
}
}
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 },
});
}
}
Player
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";
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
}
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 },
};
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,
});
}
}
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";
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
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();
}
}
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";
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";
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);
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();
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
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],
};
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
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
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();
}
}
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
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");
},
});
}
}
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();
},
});
}
}
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
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";
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);
106