Afternoon 5: Expanding the Game
Let’s flesh out the game by adding an additional enemy, a power-up, a boss battle, and sounds.
Harder Enemy
The green enemy fighters in our current game pose no real threat to our players. To make our game more difficult, our next enemy type will be able to shoot at our player while also being faster and tougher.
Enemy Setup
First load the sprite sheet in the pre-loader:
preload: function () {
this.load.image('sea', 'assets/sea.png');
this.load.image('bullet', 'assets/bullet.png');
this.load.spritesheet('greenEnemy', 'assets/enemy.png', 32, 32);
this.load.spritesheet('whiteEnemy', 'assets/shooting-enemy.png', 32, 32);
this.load.spritesheet('explosion', 'assets/explosion.png', 32, 32);
this.load.spritesheet('player', 'assets/player.png', 64, 64);
},
Then create the group for the sprites (which we will call “shooters” from now on):
setupEnemies: function () {
...
this.enemyDelay = BasicGame.SPAWN_ENEMY_DELAY;
this.shooterPool = this.add.group();
this.shooterPool.enableBody = true;
this.shooterPool.physicsBodyType = Phaser.Physics.ARCADE;
this.shooterPool.createMultiple(20, 'whiteEnemy');
this.shooterPool.setAll('anchor.x', 0.5);
this.shooterPool.setAll('anchor.y', 0.5);
this.shooterPool.setAll('outOfBoundsKill', true);
this.shooterPool.setAll('checkWorldBounds', true);
this.shooterPool.setAll(
'reward', BasicGame.SHOOTER_REWARD, false, false, 0, true
);
// Set the animation for each sprite
this.shooterPool.forEach(function (enemy) {
enemy.animations.add('fly', [ 0, 1, 2 ], 20, true);
enemy.animations.add('hit', [ 3, 1, 3, 2 ], 20, false);
enemy.events.onAnimationComplete.add( function (e) {
e.play('fly');
}, this);
});
// start spawning 5 seconds into the game
this.nextShooterAt = this.time.now + Phaser.Timer.SECOND * 5;
this.shooterDelay = BasicGame.SPAWN_SHOOTER_DELAY;
},
Diagonal Movement
Instead of moving only downwards like the regular enemy, we’ll make the shooters move diagonally across the screen. Add the following code into spawnEnemies():
spawnEnemies: function () {
...
enemy.play('fly');
}
if (this.nextShooterAt < this.time.now && this.shooterPool.countDead() > 0) {
this.nextShooterAt = this.time.now + this.shooterDelay;
var shooter = this.shooterPool.getFirstExists(false);
// spawn at a random location at the top
shooter.reset(
this.rnd.integerInRange(20, this.game.width - 20), 0,
BasicGame.SHOOTER_HEALTH
);
// choose a random target location at the bottom
var target = this.rnd.integerInRange(20, this.game.width - 20);
// move to target and rotate the sprite accordingly
shooter.rotation = this.physics.arcade.moveToXY(
shooter, target, this.game.height,
this.rnd.integerInRange(
BasicGame.SHOOTER_MIN_VELOCITY, BasicGame.SHOOTER_MAX_VELOCITY
)
) - Math.PI / 2;
shooter.play('fly');
// each shooter has their own shot timer
shooter.nextShotAt = 0;
}
},

The figure above shows the initial spawn and target areas for the shooters; the arrows show possible flight paths. Here we’re using moveToXY(), a function similar to moveToPointer() which moves the object to a given point in the world.
Both moveToPointer() and moveToXY() returns the angle towards the target in radians, and we can assign this value to object.rotation to rotate our sprite towards the target. But applying the value directly will result in incorrectly oriented shooters:

This is because Phaser assumes that your sprites are oriented to the right. We rotated our sprite counterclockwise Math.PI / 2 radians (90 degrees) to compensate for the fact that our sprite is oriented downwards.

Angles/Rotation in Phaser
Angles in Phaser are same as in Trigonometry, though it might look wrong at first glance for those used to Cartesian coordinates rather than screen coordinates.

The rotation seems flipped (increasing angles are clockwise rotations rather than counterclockwise) because the y values for the two coordinate systems are flipped.
By the way, you can use object.angle instead of object.rotation if you prefer rotating in degrees rather than radians.
Shooting
Setting up the bullets are pretty much the same as the regular bullets. First the preload():
preload: function () {
this.load.image('sea', 'assets/sea.png');
this.load.image('bullet', 'assets/bullet.png');
this.load.image('enemyBullet', 'assets/enemy-bullet.png');
this.load.spritesheet('greenEnemy', 'assets/enemy.png', 32, 32);
this.load.spritesheet('whiteEnemy', 'assets/shooting-enemy.png', 32, 32);
this.load.spritesheet('explosion', 'assets/explosion.png', 32, 32);
this.load.spritesheet('player', 'assets/player.png', 64, 64);
},
Then the sprite group at setupBullets():
setupBullets: function () {
this.enemyBulletPool = this.add.group();
this.enemyBulletPool.enableBody = true;
this.enemyBulletPool.physicsBodyType = Phaser.Physics.ARCADE;
this.enemyBulletPool.createMultiple(100, 'enemyBullet');
this.enemyBulletPool.setAll('anchor.x', 0.5);
this.enemyBulletPool.setAll('anchor.y', 0.5);
this.enemyBulletPool.setAll('outOfBoundsKill', true);
this.enemyBulletPool.setAll('checkWorldBounds', true);
this.enemyBulletPool.setAll('reward', 0, false, false, 0, true);
// Add an empty sprite group into our game
this.bulletPool = this.add.group();
We’ve already set the shot timer for the individual shooters in the spawning section. All that’s left is to create a new function that fires the enemy bullets.
update: function () {
this.checkCollisions();
this.spawnEnemies();
this.enemyFire();
this.processPlayerInput();
this.processDelayedEffects();
},
And the actual function, iterating over the live shooters in the world:
244 enemyFire: function() {
245 this.shooterPool.forEachAlive(function (enemy) {
246 if (this.time.now > enemy.nextShotAt && this.enemyBulletPool.countDead() > 0) {
247 var bullet = this.enemyBulletPool.getFirstExists(false);
248 bullet.reset(enemy.x, enemy.y);
249 this.physics.arcade.moveToObject(
250 bullet, this.player, BasicGame.ENEMY_BULLET_VELOCITY
251 );
252 enemy.nextShotAt = this.time.now + BasicGame.SHOOTER_SHOT_DELAY;
253 }
254 }, this);
255 },
Collision Detection
To wrap things up, let’s handle the collisions for the shooters as well as their bullets:
checkCollisions: function () {
this.physics.arcade.overlap(
this.bulletPool, this.enemyPool, this.enemyHit, null, this
);
this.physics.arcade.overlap(
this.bulletPool, this.shooterPool, this.enemyHit, null, this
);
this.physics.arcade.overlap(
this.player, this.enemyPool, this.playerHit, null, this
);
this.physics.arcade.overlap(
this.player, this.shooterPool, this.playerHit, null, this
);
this.physics.arcade.overlap(
this.player, this.enemyBulletPool, this.playerHit, null, this
);
},
We’ll also destroy the shooters and bullets in addToScore() upon winning:
addToScore: function (score) {
this.score += score;
this.scoreText.text = this.score;
if (this.score >= 2000) {
this.enemyPool.destroy();
this.shooterPool.destroy();
this.enemyBulletPool.destroy();
this.displayEnd(true);
}
},

Power-up
Our regular bullet stream is now a lot weaker with the introduction of the shooters. To counter this, let’s add a power-up that our players can pickup to get a spread shot.

Pre-loading the asset:
preload: function () {
this.load.image('sea', 'assets/sea.png');
this.load.image('bullet', 'assets/bullet.png');
this.load.image('enemyBullet', 'assets/enemy-bullet.png');
this.load.image('powerup1', 'assets/powerup1.png');
this.load.spritesheet('whiteEnemy', 'assets/shooting-enemy.png', 32, 32);
Then creating the sprite group:
setupPlayerIcons: function () {
this.powerUpPool = this.add.group();
this.powerUpPool.enableBody = true;
this.powerUpPool.physicsBodyType = Phaser.Physics.ARCADE;
this.powerUpPool.createMultiple(5, 'powerup1');
this.powerUpPool.setAll('anchor.x', 0.5);
this.powerUpPool.setAll('anchor.y', 0.5);
this.powerUpPool.setAll('outOfBoundsKill', true);
this.powerUpPool.setAll('checkWorldBounds', true);
this.powerUpPool.setAll(
'reward', BasicGame.POWERUP_REWARD, false, false, 0, true
);
this.lives = this.add.group();
...
We also add the possibility of spawning a power-up when an enemy dies, 30% chance for regular enemies and 50% for shooters:
setupEnemies: function () {
...
this.enemyPool.setAll('reward', BasicGame.ENEMY_REWARD, false, false, 0, true);
this.enemyPool.setAll(
'dropRate', BasicGame.ENEMY_DROP_RATE, false, false, 0, true
);
...
this.shooterPool.setAll(
'reward', BasicGame.SHOOTER_REWARD, false, false, 0, true
);
this.shooterPool.setAll(
'dropRate', BasicGame.SHOOTER_DROP_RATE, false, false, 0, true
);
Add the call in damageEnemy() to a function that spawns power-ups:
damageEnemy: function (enemy, damage) {
enemy.damage(damage);
if (enemy.alive) {
enemy.play('hit');
} else {
this.explode(enemy);
this.spawnPowerUp(enemy);
this.addToScore(enemy.reward);
}
},
Here’s the new function for spawning power-ups:
414 spawnPowerUp: function (enemy) {
415 if (this.powerUpPool.countDead() === 0 || this.weaponLevel === 5) {
416 return;
417 }
418
419 if (this.rnd.frac() < enemy.dropRate) {
420 var powerUp = this.powerUpPool.getFirstExists(false);
421 powerUp.reset(enemy.x, enemy.y);
422 powerUp.body.velocity.y = BasicGame.POWERUP_VELOCITY;
423 }
424 },
Weapon levels
You might have noticed the this.weaponLevel == 5 in the last code snippet. Our weapon strength will have up to 5 levels, each incremented by picking up a power-up.
Setting the initial value to zero:
setupPlayer: function () {
...
this.player.body.setSize(20, 20, 0, -5);
this.weaponLevel = 0;
},
Adding a collision handler:
checkCollisions: function () {
this.physics.arcade.overlap(
this.bulletPool, this.enemyPool, this.enemyHit, null, this
);
...
this.physics.arcade.overlap(
this.player, this.powerUpPool, this.playerPowerUp, null, this
);
},
And a new function for incrementing the weapon level:
391 playerPowerUp: function (player, powerUp) {
392 this.addToScore(powerUp.reward);
393 powerUp.kill();
394 if (this.weaponLevel < 5) {
395 this.weaponLevel++;
396 }
397 },
A common theme in shoot ‘em ups is that your weapon power resets when you die. Let’s add that into our code:
playerHit: function (player, enemy) {
...
if (life !== null) {
life.kill();
this.weaponLevel = 0;
this.ghostUntil = this.time.now + BasicGame.PLAYER_GHOST_TIME;
And finally, the code for implementing the spread shot:
fire: function() {
if (!this.player.alive || this.nextShotAt > this.time.now) {
return;
}
if (this.bulletPool.countDead() === 0) {
return;
}
this.nextShotAt = this.time.now + this.shotDelay;
// Find the first dead bullet in the pool
var bullet = this.bulletPool.getFirstExists(false);
// Reset (revive) the sprite and place it in a new location
bullet.reset(this.player.x, this.player.y - 20);
bullet.body.velocity.y = -BasicGame.BULLET_VELOCITY;
var bullet;
if (this.weaponLevel === 0) {
if (this.bulletPool.countDead() === 0) {
return;
}
bullet = this.bulletPool.getFirstExists(false);
bullet.reset(this.player.x, this.player.y - 20);
bullet.body.velocity.y = -BasicGame.BULLET_VELOCITY;
} else {
if (this.bulletPool.countDead() < this.weaponLevel * 2) {
return;
}
for (var i = 0; i < this.weaponLevel; i++) {
bullet = this.bulletPool.getFirstExists(false);
// spawn left bullet slightly left off center
bullet.reset(this.player.x - (10 + i * 6), this.player.y - 20);
// the left bullets spread from -95 degrees to -135 degrees
this.physics.arcade.velocityFromAngle(
-95 - i * 10, BasicGame.BULLET_VELOCITY, bullet.body.velocity
);
bullet = this.bulletPool.getFirstExists(false);
// spawn right bullet slightly right off center
bullet.reset(this.player.x + (10 + i * 6), this.player.y - 20);
// the right bullets spread from -85 degrees to -45
this.physics.arcade.velocityFromAngle(
-85 + i * 10, BasicGame.BULLET_VELOCITY, bullet.body.velocity
);
}
}
},
One last thing before you test your new spread shot: let’s increase the win condition to 20,000 points so that the game will not end before you can see your new weapon in all its greatness:
if (this.score >= 2000) {
if (this.score >= 20000) {

Note that it’s you can run out of available bullet sprites as shown with the bullet gaps above. You can avoid this by increasing the amount of bullet sprites created in the setupBullets() function, but it’s not really that necessary gameplay-wise.
Boss Battle
Shooters are nice, but our game wouldn’t be a proper shoot ‘em up if it didn’t have a boss battle.

First let’s setup the sprite sheet pre-loading:
preload: function () {
...
this.load.spritesheet('whiteEnemy', 'assets/shooting-enemy.png', 32, 32);
this.load.spritesheet('boss', 'assets/boss.png', 93, 75);
this.load.spritesheet('explosion', 'assets/explosion.png', 32, 32);
this.load.spritesheet('player', 'assets/player.png', 64, 64);
},
Then the `setupEnemies()` code:
setupEnemies: function () {
...
this.shooterDelay = BasicGame.SPAWN_SHOOTER_DELAY;
this.bossPool = this.add.group();
this.bossPool.enableBody = true;
this.bossPool.physicsBodyType = Phaser.Physics.ARCADE;
this.bossPool.createMultiple(1, 'boss');
this.bossPool.setAll('anchor.x', 0.5);
this.bossPool.setAll('anchor.y', 0.5);
this.bossPool.setAll('outOfBoundsKill', true);
this.bossPool.setAll('checkWorldBounds', true);
this.bossPool.setAll('reward', BasicGame.BOSS_REWARD, false, false, 0, true);
this.bossPool.setAll(
'dropRate', BasicGame.BOSS_DROP_RATE, false, false, 0, true
);
// Set the animation for each sprite
this.bossPool.forEach(function (enemy) {
enemy.animations.add('fly', [ 0, 1, 2 ], 20, true);
enemy.animations.add('hit', [ 3, 1, 3, 2 ], 20, false);
enemy.events.onAnimationComplete.add( function (e) {
e.play('fly');
}, this);
});
this.boss = this.bossPool.getTop();
this.bossApproaching = false;
},
We made a group containing our single boss. This is for two reasons: to put the boss in the proper sprite order - above the enemies, but below the bullets and text; and to step around the sprite vs sprite collision coding quirk we mentioned way back. We also stored the actual boss in a property for convenience.
We then replace what happens when we reach 20,000 points from ending the game to spawning the boss:
addToScore: function (score) {
this.score += score;
this.scoreText.text = this.score;
if (this.score >= 20000) {
this.enemyPool.destroy();
this.shooterPool.destroy();
this.enemyBulletPool.destroy();
this.displayEnd(true);
}
// this approach prevents the boss from spawning again upon winning
if (this.score >= 20000 && this.bossPool.countDead() == 1) {
this.spawnBoss();
}
},
Then the new spawnBoss() function:
464 spawnBoss: function () {
465 this.bossApproaching = true;
466 this.boss.reset(this.game.width / 2, 0, BasicGame.BOSS_HEALTH);
467 this.physics.enable(this.boss, Phaser.Physics.ARCADE);
468 this.boss.body.velocity.y = BasicGame.BOSS_Y_VELOCITY;
469 this.boss.play('fly');
470 },
The bossApproaching flag is there to make the boss invulnerable until it reaches its target position. Let’s add the code to processDelayedEffects() to check this:
processDelayedEffects: function () {
...
this.showReturn = false;
}
if (this.bossApproaching && this.boss.y > 80) {
this.bossApproaching = false;
this.boss.nextShotAt = 0;
this.boss.body.velocity.y = 0;
this.boss.body.velocity.x = BasicGame.BOSS_X_VELOCITY;
// allow bouncing off world bounds
this.boss.body.bounce.x = 1;
this.boss.body.collideWorldBounds = true;
}
},
Once it reaches the target height, it becomes a 500 health enemy and starts bouncing from right to left using the built-in physics engine.
Next is to setup the collision detection for the boss, taking into account the invulnerable phase:
checkCollisions: function () {
...
this.player, this.powerUpPool, this.playerPowerUp, null, this
);
if (this.bossApproaching === false) {
this.physics.arcade.overlap(
this.bulletPool, this.bossPool, this.enemyHit, null, this
);
this.physics.arcade.overlap(
this.player, this.bossPool, this.playerHit, null, this
);
}
And modify the damageEnemy() to get our game winning condition back:
damageEnemy: function (enemy, damage) {
enemy.damage(damage);
if (enemy.alive) {
enemy.play('hit');
} else {
this.explode(enemy);
this.spawnPowerUp(enemy);
this.addToScore(enemy.reward);
// We check the sprite key (e.g. 'greenEnemy') to see if the sprite is a boss
// For full games, it would be better to set flags on the sprites themselves
if (enemy.key === 'boss') {
this.enemyPool.destroy();
this.shooterPool.destroy();
this.bossPool.destroy();
this.enemyBulletPool.destroy();
this.displayEnd(true);
}
}
},
We’ve saved the boss shooting code for last:
enemyFire: function() {
...
}, this);
if (this.bossApproaching === false && this.boss.alive &&
this.boss.nextShotAt < this.time.now &&
this.enemyBulletPool.countDead() >= 10) {
this.boss.nextShotAt = this.time.now + BasicGame.BOSS_SHOT_DELAY;
for (var i = 0; i < 5; i++) {
// process 2 bullets at a time
var leftBullet = this.enemyBulletPool.getFirstExists(false);
leftBullet.reset(this.boss.x - 10 - i * 10, this.boss.y + 20);
var rightBullet = this.enemyBulletPool.getFirstExists(false);
rightBullet.reset(this.boss.x + 10 + i * 10, this.boss.y + 20);
if (this.boss.health > BasicGame.BOSS_HEALTH / 2) {
// aim directly at the player
this.physics.arcade.moveToObject(
leftBullet, this.player, BasicGame.ENEMY_BULLET_VELOCITY
);
this.physics.arcade.moveToObject(
rightBullet, this.player, BasicGame.ENEMY_BULLET_VELOCITY
);
} else {
// aim slightly off center of the player
this.physics.arcade.moveToXY(
leftBullet, this.player.x - i * 100, this.player.y,
BasicGame.ENEMY_BULLET_VELOCITY
);
this.physics.arcade.moveToXY(
rightBullet, this.player.x + i * 100, this.player.y,
BasicGame.ENEMY_BULLET_VELOCITY
);
}
}
}
},
There are two additional phases to this boss fight after the “approaching” phase. First is where the boss just fires 10 bullets concentrated to the player.

Then once the boss’s health goes down to 250, the boss now fires 10 bullets at the area around the player. While this is the same amount of bullets as the previous phase, the spread makes it much harder to dodge.

Sound Effects
We’ve saved the sound effects for the end of the workshop because integrating it with the main tutorial may make it more complicated that it should be.
Anyway, adding sound effects in Phaser is as easy as adding sprites. First, pre-load the sounds:
preload: function () {
...
this.load.spritesheet('player', 'assets/player.png', 64, 64);
this.load.audio('explosion', ['assets/explosion.ogg', 'assets/explosion.wav']);
this.load.audio('playerExplosion',
['assets/player-explosion.ogg', 'assets/player-explosion.wav']);
this.load.audio('enemyFire',
['assets/enemy-fire.ogg', 'assets/enemy-fire.wav']);
this.load.audio('playerFire',
['assets/player-fire.ogg', 'assets/player-fire.wav']);
this.load.audio('powerUp', ['assets/powerup.ogg', 'assets/powerup.wav']);
},
You can use multiple formats for each loaded sound; Phaser will choose the best format based on the browser. Using Ogg Vorbis (.ogg) and AAC in MP4 (.m4a) should give you the best coverage among browsers. WAV should be avoided due to its file size, and MP3 should be avoided for public projects due to possible licensing issues.
Once loaded, we then initialize the audio, adding a new function setupAudio():
create: function () {
...
this.setupAudio();
this.cursors = this.input.keyboard.createCursorKeys();
},
...
setupAudio: function () {
this.explosionSFX = this.add.audio('explosion');
this.playerExplosionSFX = this.add.audio('playerExplosion');
this.enemyFireSFX = this.add.audio('enemyFire');
this.playerFireSFX = this.add.audio('playerFire');
this.powerUpSFX = this.add.audio('powerUp');
},
Then play the audio when they are needed. Enemy explosion:
damageEnemy: function (enemy, damage) {
enemy.damage(damage);
if (enemy.alive) {
enemy.play('hit');
} else {
this.explode(enemy);
this.explosionSFX.play();
this.spawnPowerUp(enemy);
Player explosion:
playerHit: function (player, enemy) {
// check first if this.ghostUntil is not not undefined or null
if (this.ghostUntil && this.ghostUntil > this.time.now) {
return;
}
this.playerExplosionSFX.play();
// crashing into an enemy only deals 5 damage
Enemy firing:
enemyFire: function() {
...
enemy.nextShotAt = this.time.now + BasicGame.SHOOTER_SHOT_DELAY;
this.enemyFireSFX.play();
}
}, this);
if (this.bossApproaching === false && this.boss.alive &&
this.boss.nextShotAt < this.time.now &&
this.enemyBulletPool.countDead() >= 10) {
this.boss.nextShotAt = this.time.now + BasicGame.BOSS_SHOT_DELAY;
this.enemyFireSFX.play();
for (var i = 0; i < 5; i++) {
Player firing:
fire: function() {
if (!this.player.alive || this.nextShotAt > this.time.now) {
return;
}
this.nextShotAt = this.time.now + this.shotDelay;
this.playerFireSFX.play();
if (this.weaponLevel == 0) {
if (this.bulletPool.countDead() == 0) {
Power-up pickup:
playerPowerUp: function (player, powerUp) {
this.addToScore(powerUp.reward);
powerUp.kill();
this.powerUpSFX.play();
if (this.weaponLevel < 5) {
this.weaponLevel++;
}
},
Go ahead and play your game to check if the sounds are properly playing.
You might notice that the sound effects are pretty loud especially when you’re playing in a quiet room. To wrap up this chapter, let’s adjust the game’s volume. It accepts a value between 0 and 1 so let’s pick 0.3:
setupAudio: function () {
this.sound.volume = 0.3;
this.explosionSFX = this.add.audio('explosion');
this.playerExplosionSFX = this.add.audio('playerExplosion');
this.enemyFireSFX = this.add.audio('enemyFire');
this.playerFireSFX = this.add.audio('playerFire');
this.powerUpSFX = this.add.audio('powerUp');
},
And now we’re done with the full game. We wrap up the tutorial in the next chapter.