Phaser.js tutorial: Building a polished space shooter game (Part 4)

phaser demo

Welcome to part 4 of my javascript game development tutorial series with phaser.js. In part 3 we added enemies to our game.

At this point, the basic mechanics of the game have been figured out, and the attention shifts to adjusting the timing and mechanics as the player progresses in the game to keep it fun and challenging. That is what pacing is all about.

We will be adding score, health, a game-over condition, new enemies and a weapon upgrade. That’s a lot! Although there is more code in this part, much of it is similar to techniques we have already used.

If you haven’t gone through the earlier parts yet, do that that first:

You can play the finished game here. All of the code and assets are available on github. In the code below, new code is in green, and removed code is in red.

Step 18: Add player health and display it

Challenge comes from risk and consequences. At the moment, enemies can’t hurt us. Let’s give our ship a limited amount of shields that enemies can damage. Phaser natively understands health and damage, but that may change in future versions.

Because we can die now, we need to do some clean up if that happens (get rid of our plasma trail). We also need to consider what our input keys will do when our ship isn’t around.

We can make a simple HUD just by rendering text to the screen. Remember to update it whenever its underlaying data changes.

Play the game at this step
View the code at this step


...
 var bullets;
 var fireButton;
 var bulletTimer = 0;
+var shields;
 
 var ACCLERATION = 600;
 var DRAG = 400;
...
 
     //  The hero!
     player = game.add.sprite(400, 500, 'ship');
+    player.health = 100;
     player.anchor.setTo(0.5, 0.5);
     game.physics.enable(player, Phaser.Physics.ARCADE);
     player.body.maxVelocity.setTo(MAXSPEED, MAXSPEED);
     player.body.drag.setTo(DRAG, DRAG);
+    player.events.onKilled.add(function(){
+        shipTrail.kill();
+    });
 
     //  The baddies!
     greenEnemies = game.add.group();
...
     greenEnemies.forEach(function(enemy){
         addEnemyEmitterTrail(enemy);
         enemy.body.setSize(enemy.width * 3 / 4, enemy.height * 3 / 4);
+        enemy.damageAmount = 20;
         enemy.events.onKilled.add(function(){
             enemy.trail.kill();
         });
...
     explosions.forEach( function(explosion) {
         explosion.animations.add('explosion');
     });
+
+    //  Shields stat
+    shields = game.add.text(game.world.width - 150, 10, 'Shields: ' + player.health +'%', { font: '20px Arial', fill: '#fff' });
+    shields.render = function () {
+        shields.text = 'Shields: ' + Math.max(player.health, 0) +'%';
+    };
+
+
 }
 
 function update() {
...
     }
 
     //  Fire bullet
-    if (fireButton.isDown || game.input.activePointer.isDown) {
+    if (player.alive && (fireButton.isDown || game.input.activePointer.isDown)) {
         fireBullet();
     }
 
...
     explosion.alpha = 0.7;
     explosion.play('explosion', 30, false, true);
     enemy.kill();
+
+    player.damage(enemy.damageAmount);
+    shields.render();
 }
 

Step 19: Add game over text and restart function

A game really starts to feel like a game when you have a win and a lose condition. This game won’t have a win condition, as it’s a procedural survive-as-long-as-you-can type of game. Getting a high score is the only “win”, but we’ll worry about that later.

We definitely have a lose condition though, which is when the ship’s shields drop to or below zero. We’ll check for that condition in our update loop. At that point, we should say something relevant, like “game over”, and make sure our code paths don’t do anything unexpected, as the game loop will keep going unless we tell it to do something different (like load a new state or set a pause flag).

Because we want players to keep playing, we should also give a way to restart, so we temporarily remap our input keys to trigger code that will reset everything and kick it off again. It is important to clean up any active timers or input handlers before resetting, so you don’t get unexpected results or cause any memory leaks. We’ve done a little bit of refactoring for this reason.

Play the game at this step
View the code at this step


...
 var fireButton;
 var bulletTimer = 0;
 var shields;
+var greenEnemyLaunchTimer;
+var gameOver;
 
 var ACCLERATION = 600;
 var DRAG = 400;
...
     player.events.onKilled.add(function(){
         shipTrail.kill();
     });
+    player.events.onRevived.add(function(){
+        shipTrail.start(false, 5000, 10);
+    });
 
     //  The baddies!
     greenEnemies = game.add.group();
...
         });
     });
 
-    launchGreenEnemy();
+    game.time.events.add(1000, launchGreenEnemy);
 
     //  And some controls to play the game with
     cursors = game.input.keyboard.createCursorKeys();
...
     };
 
 
+    //  Game over text
+    gameOver = game.add.text(game.world.centerX, game.world.centerY, 'GAME OVER!', { font: '84px Arial', fill: '#fff' });
+    gameOver.anchor.setTo(0.5, 0.5);
+    gameOver.visible = false;
 }
 
 function update() {
...
     //  Check collisions
     game.physics.arcade.overlap(player, greenEnemies, shipCollide, null, this);
     game.physics.arcade.overlap(greenEnemies, bullets, hitEnemy, null, this);
+
+    //  Game over?
+    if (! player.alive && gameOver.visible === false) {
+        gameOver.visible = true;
+        var fadeInGameOver = game.add.tween(gameOver);
+        fadeInGameOver.to({alpha: 1}, 1000, Phaser.Easing.Quintic.Out);
+        fadeInGameOver.onComplete.add(setResetHandlers);
+        fadeInGameOver.start();
+        function setResetHandlers() {
+            //  The "click to restart" handler
+            tapRestart = game.input.onTap.addOnce(_restart,this);
+            spaceRestart = fireButton.onDown.addOnce(_restart,this);
+            function _restart() {
+              tapRestart.detach();
+              spaceRestart.detach();
+              restart();
+            }
+        }
+    }
 }
 
 function render() {
...
     }
 
     //  Send another enemy soon
-    game.time.events.add(game.rnd.integerInRange(MIN_ENEMY_SPACING, MAX_ENEMY_SPACING), launchGreenEnemy);
+    greenEnemyLaunchTimer = game.time.events.add(game.rnd.integerInRange(MIN_ENEMY_SPACING, MAX_ENEMY_SPACING), launchGreenEnemy);
 }
 
 
...
     enemy.kill();
     bullet.kill()
 }
+
+
+function restart () {
+    //  Reset the enemies
+    greenEnemies.callAll('kill');
+    game.time.events.remove(greenEnemyLaunchTimer);
+    game.time.events.add(1000, launchGreenEnemy);
+
+    //  Revive the player
+    player.revive();
+    player.health = 100;
+    shields.render();
+    score = 0;
+    scoreText.render();
+
+    //  Hide the text
+    gameOver.visible = false;
+
+}

Step 20: Add a score and display it

Let’s get a score in the game to really make it complete. This is very similar to how we added our shields.

With score in place, our game is fully playable. But is it fun? Is it challenging? Does it have enough content? Will players keep playing?

Answering these questions is the true challenge of game design and development. So we’re not finished yet…

Play the game at this step
View the code at this step


...
 var fireButton;
 var bulletTimer = 0;
 var shields;
+var score = 0;
+var scoreText;
 var greenEnemyLaunchTimer;
 var gameOver;
 
...
         shields.text = 'Shields: ' + Math.max(player.health, 0) +'%';
     };
 
+    //  Score
+    scoreText = game.add.text(10, 10, '', { font: '20px Arial', fill: '#fff' });
+    scoreText.render = function () {
+        scoreText.text = 'Score: ' + score;
+    };
+    scoreText.render();
 
     //  Game over text
     gameOver = game.add.text(game.world.centerX, game.world.centerY, 'GAME OVER!', { font: '84px Arial', fill: '#fff' });
...
     //  Game over?
     if (! player.alive && gameOver.visible === false) {
         gameOver.visible = true;
+        gameOver.alpha = 0;
         var fadeInGameOver = game.add.tween(gameOver);
         fadeInGameOver.to({alpha: 1}, 1000, Phaser.Easing.Quintic.Out);
         fadeInGameOver.onComplete.add(setResetHandlers);
...
     explosion.play('explosion', 30, false, true);
     enemy.kill();
     bullet.kill()
+
+    // Increase score
+    score += enemy.damageAmount * 10;
+    scoreText.render()
 }
 

Step 21: Use bitmap text for a custom font

Before working more on the game play, lets make a quick improvement in production value by using bitmap fonts for our on-screen texts. This font was created with http://kvazars.com/littera/.

Play the game at this step
View the code at this step


...
     game.load.image('bullet', '/assets/bullet.png');
     game.load.image('enemy-green', '/assets/enemy-green.png');
     game.load.spritesheet('explosion', '/assets/explode.png', 128, 128);
+    game.load.bitmapFont('spacefont', '/assets/spacefont/spacefont.png', '/assets/spacefont/spacefont.xml');  
 }
 
 function create() {
...
     });
 
     //  Shields stat
-    shields = game.add.text(game.world.width - 150, 10, 'Shields: ' + player.health +'%', { font: '20px Arial', fill: '#fff' });
+    shields = game.add.bitmapText(game.world.width - 250, 10, 'spacefont', '' + player.health +'%', 50);
     shields.render = function () {
         shields.text = 'Shields: ' + Math.max(player.health, 0) +'%';
     };
+    shields.render();
 
     //  Score
-    scoreText = game.add.text(10, 10, '', { font: '20px Arial', fill: '#fff' });
+    scoreText = game.add.bitmapText(10, 10, 'spacefont', '', 50);
     scoreText.render = function () {
         scoreText.text = 'Score: ' + score;
     };
     scoreText.render();
 
     //  Game over text
-    gameOver = game.add.text(game.world.centerX, game.world.centerY, 'GAME OVER!', { font: '84px Arial', fill: '#fff' });
-    gameOver.anchor.setTo(0.5, 0.5);
+    gameOver = game.add.bitmapText(game.world.centerX, game.world.centerY, 'spacefont', 'GAME OVER!', 110);
+    gameOver.x = gameOver.x - gameOver.textWidth / 2;
+    gameOver.y = gameOver.y - gameOver.textHeight / 3;
     gameOver.visible = false;
 }
 

Step 22: New enemies!

The green enemies are not too challenging, and get boring after a while. Let’s add a new type of enemy for variety and increased difficulty.

We make a new group of enemies very similarly to how we added the first group. However, we have some more complex logic in how we launch them, to create “waves” of enemies instead of just launching one at a time. Also, we give them a sine wave movement with a nifty mapping of their y-position to an x-position adjustment. It takes a little bit of playing with the movement equations to get the right feel.

Play the game at this step
View the code at this step


...
 
 var player;
 var greenEnemies;
+var blueEnemies;
 var starfield;
 var cursors;
 var bank;
...
 var score = 0;
 var scoreText;
 var greenEnemyLaunchTimer;
+var blueEnemyLaunchTimer;
 var gameOver;
 
 var ACCLERATION = 600;
...
     game.load.image('ship', '/assets/player.png');
     game.load.image('bullet', '/assets/bullet.png');
     game.load.image('enemy-green', '/assets/enemy-green.png');
+    game.load.image('enemy-blue', '/assets/enemy-blue.png');
     game.load.spritesheet('explosion', '/assets/explode.png', 128, 128);
     game.load.bitmapFont('spacefont', '/assets/spacefont/spacefont.png', '/assets/spacefont/spacefont.xml');  
 }
...
 
     game.time.events.add(1000, launchGreenEnemy);
 
+    blueEnemies = game.add.group();
+    blueEnemies.enableBody = true;
+    blueEnemies.physicsBodyType = Phaser.Physics.ARCADE;
+    blueEnemies.createMultiple(30, 'enemy-blue');
+    blueEnemies.setAll('anchor.x', 0.5);
+    blueEnemies.setAll('anchor.y', 0.5);
+    blueEnemies.setAll('scale.x', 0.5);
+    blueEnemies.setAll('scale.y', 0.5);
+    blueEnemies.setAll('angle', 180);
+    blueEnemies.forEach(function(enemy){
+        enemy.damageAmount = 40;
+    });
+
+    game.time.events.add(1000, launchBlueEnemy);
+
     //  And some controls to play the game with
     cursors = game.input.keyboard.createCursorKeys();
     fireButton = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
...
     game.physics.arcade.overlap(player, greenEnemies, shipCollide, null, this);
     game.physics.arcade.overlap(greenEnemies, bullets, hitEnemy, null, this);
 
+    game.physics.arcade.overlap(player, blueEnemies, shipCollide, null, this);
+    game.physics.arcade.overlap(bullets, blueEnemies, hitEnemy, null, this);
+
     //  Game over?
     if (! player.alive && gameOver.visible === false) {
         gameOver.visible = true;

...
     greenEnemyLaunchTimer = game.time.events.add(game.rnd.integerInRange(MIN_ENEMY_SPACING, MAX_ENEMY_SPACING), launchGreenEnemy);
 }
 
+function launchBlueEnemy() {
+    var startingX = game.rnd.integerInRange(100, game.width - 100);
+    var verticalSpeed = 180;
+    var spread = 60;
+    var frequency = 70;
+    var verticalSpacing = 70;
+    var numEnemiesInWave = 5;
+    var timeBetweenWaves = 7000;
+
+    //  Launch wave
+    for (var i =0; i < numEnemiesInWave; i++) {
+        var enemy = blueEnemies.getFirstExists(false);
+        if (enemy) {
+            enemy.startingX = startingX;
+            enemy.reset(game.width / 2, -verticalSpacing * i);
+            enemy.body.velocity.y = verticalSpeed;
+
+            //  Update function for each enemy
+            enemy.update = function(){
+              //  Wave movement
+              this.body.x = this.startingX + Math.sin((this.y) / frequency) * spread;
+
+              //  Squish and rotate ship for illusion of "banking"
+              bank = Math.cos((this.y + 60) / frequency)
+              this.scale.x = 0.5 - Math.abs(bank) / 8;
+              this.angle = 180 - bank * 2;
+
+              //  Kill enemies once they go off screen
+              if (this.y > game.height + 200) {
+                this.kill();
+              }
+            };
+        }
+    }
+
+    //  Send another wave soon
+    blueEnemyLaunchTimer = game.time.events.add(timeBetweenWaves, launchBlueEnemy);
+}
 
 function addEnemyEmitterTrail(enemy) {
     var enemyTrail = game.add.emitter(enemy.x, player.y - 10, 100);
...
     game.time.events.remove(greenEnemyLaunchTimer);
     game.time.events.add(1000, launchGreenEnemy);
 
+    blueEnemies.callAll('kill');
+    game.time.events.remove(blueEnemyLaunchTimer);
     //  Revive the player
     player.revive();
     player.health = 100;
     

Step 23: Give the blue baddies bullets!

We’re not the only one in the universe who can shoot. Having enemies that can shoot back at us really ups the difficulty and energy level of the game.

To make the blue enemies shoot, we use some of the same techniques that we used for our own bullets. However, we have to add some ai to tell the enemies when and how to shoot.

We’ll make each enemy fire one shot as soon as it gets one eighth of the way down the screen. We’ll use a phaser helper function to aim the bullet at our ship at the time of shooting. This creates a nice cascade of bullets to dodge.

We also need allow the bullets to hit our ship and damage us.

Play the game at this step
View the code at this step


...
 var player;
 var greenEnemies;
 var blueEnemies;
+var enemyBullets;
 var starfield;
 var cursors;
 var bank;
...
     game.load.image('bullet', '/assets/bullet.png');
     game.load.image('enemy-green', '/assets/enemy-green.png');
     game.load.image('enemy-blue', '/assets/enemy-blue.png');
+    game.load.image('blueEnemyBullet', '/assets/enemy-blue-bullet.png');
     game.load.spritesheet('explosion', '/assets/explode.png', 128, 128);
     game.load.bitmapFont('spacefont', '/assets/spacefont/spacefont.png', '/assets/spacefont/spacefont.xml');  
 }
...
 
     game.time.events.add(1000, launchGreenEnemy);
 
+    //  Blue enemy's bullets
+    blueEnemyBullets = game.add.group();
+    blueEnemyBullets.enableBody = true;
+    blueEnemyBullets.physicsBodyType = Phaser.Physics.ARCADE;
+    blueEnemyBullets.createMultiple(30, 'blueEnemyBullet');
+    blueEnemyBullets.callAll('crop', null, {x: 90, y: 0, width: 90, height: 70});
+    blueEnemyBullets.setAll('alpha', 0.9);
+    blueEnemyBullets.setAll('anchor.x', 0.5);
+    blueEnemyBullets.setAll('anchor.y', 0.5);
+    blueEnemyBullets.setAll('outOfBoundsKill', true);
+    blueEnemyBullets.setAll('checkWorldBounds', true);
+    blueEnemyBullets.forEach(function(enemy){
+        enemy.body.setSize(20, 20);
+    });
+
+    //  More baddies!
     blueEnemies = game.add.group();
     blueEnemies.enableBody = true;
     blueEnemies.physicsBodyType = Phaser.Physics.ARCADE;
...
     game.physics.arcade.overlap(player, blueEnemies, shipCollide, null, this);
     game.physics.arcade.overlap(bullets, blueEnemies, hitEnemy, null, this);
 
+    game.physics.arcade.overlap(blueEnemyBullets, player, enemyHitsPlayer, null, this);
+
     //  Game over?
     if (! player.alive && gameOver.visible === false) {
         gameOver.visible = true;
...
             enemy.reset(game.width / 2, -verticalSpacing * i);
             enemy.body.velocity.y = verticalSpeed;
 
+            //  Set up firing
+            var bulletSpeed = 400;
+            var firingDelay = 2000;
+            enemy.bullets = 1;
+            enemy.lastShot = 0;
+
             //  Update function for each enemy
             enemy.update = function(){
               //  Wave movement
...
               this.scale.x = 0.5 - Math.abs(bank) / 8;
               this.angle = 180 - bank * 2;
 
+              //  Fire
+              enemyBullet = blueEnemyBullets.getFirstExists(false);
+              if (enemyBullet &&
+                  this.alive &&
+                  this.bullets &&
+                  this.y > game.width / 8 &&
+                  game.time.now > firingDelay + this.lastShot) {
+                    this.lastShot = game.time.now;
+                    this.bullets--;
+                    enemyBullet.reset(this.x, this.y + this.height / 2);
+                    enemyBullet.damageAmount = this.damageAmount;
+                    var angle = game.physics.arcade.moveToObject(enemyBullet, player, bulletSpeed);
+                    enemyBullet.angle = game.math.radToDeg(angle);
+                }
+
               //  Kill enemies once they go off screen
               if (this.y > game.height + 200) {
                 this.kill();
...
     scoreText.render()
 }
 
+function enemyHitsPlayer (player, bullet) {
+    var explosion = explosions.getFirstExists(false);
+    explosion.reset(player.body.x + player.body.halfWidth, player.body.y + player.body.halfHeight);
+    explosion.alpha = 0.7;
+    explosion.play('explosion', 30, false, true);
+    bullet.kill();
+
+    player.damage(bullet.damageAmount);
+    shields.render()
+}
+
 
 function restart () {
     //  Reset the enemies
     greenEnemies.callAll('kill');
     game.time.events.remove(greenEnemyLaunchTimer);
     game.time.events.add(1000, launchGreenEnemy);
+    blueEnemyBullets.callAll('kill');
 
     blueEnemies.callAll('kill');
     game.time.events.remove(blueEnemyLaunchTimer);
     

Step 24: Progressive difficulty

To build up the difficulty, we’ll make the green enemies come at us more frequently each time we kill one. Also, we hold back the blue enemies until we reach a score threshold. Now the game has a distinct sense of difficulty progression.

Remember to keep track of all the things you’ll need to reset in the restart function.

Play the game at this step
View the code at this step


...
 var score = 0;
 var scoreText;
 var greenEnemyLaunchTimer;
+var greenEnemySpacing = 1000;
 var blueEnemyLaunchTimer;
+var blueEnemyLaunched = false;
 var gameOver;
 
 var ACCLERATION = 600;
...
         enemy.damageAmount = 40;
     });
 
-    game.time.events.add(1000, launchBlueEnemy);
-
     //  And some controls to play the game with
     cursors = game.input.keyboard.createCursorKeys();
     fireButton = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
...
 
 
 function launchGreenEnemy() {
-    var MIN_ENEMY_SPACING = 300;
-    var MAX_ENEMY_SPACING = 3000;
     var ENEMY_SPEED = 300;
 
     var enemy = greenEnemies.getFirstExists(false);
...
     }
 
     //  Send another enemy soon
-    greenEnemyLaunchTimer = game.time.events.add(game.rnd.integerInRange(MIN_ENEMY_SPACING, MAX_ENEMY_SPACING), launchGreenEnemy);
+    greenEnemyLaunchTimer = game.time.events.add(game.rnd.integerInRange(greenEnemySpacing, greenEnemySpacing + 1000), launchGreenEnemy);
 }
 
 function launchBlueEnemy() {
...
     var frequency = 70;
     var verticalSpacing = 70;
     var numEnemiesInWave = 5;
-    var timeBetweenWaves = 7000;
+    var timeBetweenWaves = 2500;
 
     //  Launch wave
     for (var i =0; i < numEnemiesInWave; i++) {
...
     }
 
     //  Send another wave soon
-    blueEnemyLaunchTimer = game.time.events.add(timeBetweenWaves, launchBlueEnemy);
+    blueEnemyLaunchTimer = game.time.events.add(game.rnd.integerInRange(timeBetweenWaves, timeBetweenWaves + 4000), launchBlueEnemy);
 }
 
 function addEnemyEmitterTrail(enemy) {
...
 
     // Increase score
     score += enemy.damageAmount * 10;
-    scoreText.render()
+    scoreText.render();
+
+    //  Pacing
+    //  Enemies come quicker as score increases
+    greenEnemySpacing *= 0.9;
+    //  Blue enemies come in after a score of 1000
+    if (!blueEnemyLaunched && score > 1000) {
+      blueEnemyLaunched = true;
+      launchBlueEnemy();
+      //  Slow green enemies down now that there are other enemies
+      greenEnemySpacing *= 2;
+    }
 }
 
 function enemyHitsPlayer (player, bullet) {
...
     greenEnemies.callAll('kill');
     game.time.events.remove(greenEnemyLaunchTimer);
     game.time.events.add(1000, launchGreenEnemy);
+    blueEnemies.callAll('kill');
     blueEnemyBullets.callAll('kill');
+    game.time.events.remove(blueEnemyLaunchTimer);
 
     blueEnemies.callAll('kill');
     game.time.events.remove(blueEnemyLaunchTimer);
...
     //  Hide the text
     gameOver.visible = false;
 
+    //  Reset pacing
+    greenEnemySpacing = 1000;
+    blueEnemyLaunched = false;
 }
 

Step 25: Weapon upgrades :)

If the enemies get tougher, our guns should get better. Not only does this even out the difficulty, getting new guns is fun.

We’ll use some similar pacing thresholds to update our gun type after a certain score, and define how our new gun shoots.

Play the game at this step
View the code at this step


...
     game.physics.enable(player, Phaser.Physics.ARCADE);
     player.body.maxVelocity.setTo(MAXSPEED, MAXSPEED);
     player.body.drag.setTo(DRAG, DRAG);
+    player.weaponLevel = 1
     player.events.onKilled.add(function(){
         shipTrail.kill();
     });
...
 }
 
 function fireBullet() {
-    //  To avoid them being allowed to fire too fast we set a time limit
-    if (game.time.now > bulletTimer)
-    {
-        var BULLET_SPEED = 400;
-        var BULLET_SPACING = 250;
-        //  Grab the first bullet we can from the pool
-        var bullet = bullets.getFirstExists(false);
-
-        if (bullet)
+    switch (player.weaponLevel) {
+        case 1:
+        //  To avoid them being allowed to fire too fast we set a time limit
+        if (game.time.now > bulletTimer)
         {
-            //  And fire it
-            //  Make bullet come out of tip of ship with right angle
-            var bulletOffset = 20 * Math.sin(game.math.degToRad(player.angle));
-            bullet.reset(player.x + bulletOffset, player.y);
-            bullet.angle = player.angle;
-            game.physics.arcade.velocityFromAngle(bullet.angle - 90, BULLET_SPEED, bullet.body.velocity);
-            bullet.body.velocity.x += player.body.velocity.x;
-
-            bulletTimer = game.time.now + BULLET_SPACING;
+            var BULLET_SPEED = 400;
+            var BULLET_SPACING = 250;
+            //  Grab the first bullet we can from the pool
+            var bullet = bullets.getFirstExists(false);
+
+            if (bullet)
+            {
+                //  And fire it
+                //  Make bullet come out of tip of ship with right angle
+                var bulletOffset = 20 * Math.sin(game.math.degToRad(player.angle));
+                bullet.reset(player.x + bulletOffset, player.y);
+                bullet.angle = player.angle;
+                game.physics.arcade.velocityFromAngle(bullet.angle - 90, BULLET_SPEED, bullet.body.velocity);
+                bullet.body.velocity.x += player.body.velocity.x;
+
+                bulletTimer = game.time.now + BULLET_SPACING;
+            }
+        }
+        break;
+
+        case 2:
+        if (game.time.now > bulletTimer) {
+            var BULLET_SPEED = 400;
+            var BULLET_SPACING = 550;
+
+
+            for (var i = 0; i < 3; i++) {
+                var bullet = bullets.getFirstExists(false);
+                if (bullet) {
+                    //  Make bullet come out of tip of ship with right angle
+                    var bulletOffset = 20 * Math.sin(game.math.degToRad(player.angle));
+                    bullet.reset(player.x + bulletOffset, player.y);
+                    //  "Spread" angle of 1st and 3rd bullets
+                    var spreadAngle;
+                    if (i === 0) spreadAngle = -20;
+                    if (i === 1) spreadAngle = 0;
+                    if (i === 2) spreadAngle = 20;
+                    bullet.angle = player.angle + spreadAngle;
+                    game.physics.arcade.velocityFromAngle(spreadAngle - 90, BULLET_SPEED, bullet.body.velocity);
+                    bullet.body.velocity.x += player.body.velocity.x;
+                }
+                bulletTimer = game.time.now + BULLET_SPACING;
+            }
         }
     }
 }
...
       //  Slow green enemies down now that there are other enemies
       greenEnemySpacing *= 2;
     }
+    //  Weapon upgrade
+    if (score > 4000 && player.weaponLevel < 2) {
+      player.weaponLevel = 2;
+    }
 }
 
 function enemyHitsPlayer (player, bullet) {
...
     blueEnemies.callAll('kill');
     game.time.events.remove(blueEnemyLaunchTimer);
     //  Revive the player
+    player.weaponLevel = 1;
     player.revive();
     player.health = 100;
     shields.render();
     

Step 26: Add larger explosion for when player dies

Let’s add some more special effects and create a bigger impact when the player dies by making a bigger explosion when our ship gets killed.

Play the game at this step
View the code at this step


...
 var bank;
 var shipTrail;
 var explosions;
+var playerDeath;
 var bullets;
 var fireButton;
 var bulletTimer = 0;
...
         explosion.animations.add('explosion');
     });
 
+    //  Big explosion
+    playerDeath = game.add.emitter(player.x, player.y);
+    playerDeath.width = 50;
+    playerDeath.height = 50;
+    playerDeath.makeParticles('explosion', [0,1,2,3,4,5,6,7], 10);
+    playerDeath.setAlpha(0.9, 0, 800);
+    playerDeath.setScale(0.1, 0.6, 0.1, 0.6, 1000, Phaser.Easing.Quintic.Out);
+
     //  Shields stat
     shields = game.add.bitmapText(game.world.width - 250, 10, 'spacefont', '' + player.health +'%', 50);
     shields.render = function () {
...
 
 
 function shipCollide(player, enemy) {
-    var explosion = explosions.getFirstExists(false);
-    explosion.reset(enemy.body.x + enemy.body.halfWidth, enemy.body.y + enemy.body.halfHeight);
-    explosion.body.velocity.y = enemy.body.velocity.y;
-    explosion.alpha = 0.7;
-    explosion.play('explosion', 30, false, true);
     enemy.kill();
 
     player.damage(enemy.damageAmount);
     shields.render();
+
+    if (player.alive) {
+        var explosion = explosions.getFirstExists(false);
+        explosion.reset(player.body.x + player.body.halfWidth, player.body.y + player.body.halfHeight);
+        explosion.alpha = 0.7;
+        explosion.play('explosion', 30, false, true);
+    } else {
+        playerDeath.x = player.x;
+        playerDeath.y = player.y;
+        playerDeath.start(false, 1000, 10, 10);
+    }
 }
 
 
...
 }
 
 function enemyHitsPlayer (player, bullet) {
-    var explosion = explosions.getFirstExists(false);
-    explosion.reset(player.body.x + player.body.halfWidth, player.body.y + player.body.halfHeight);
-    explosion.alpha = 0.7;
-    explosion.play('explosion', 30, false, true);
     bullet.kill();
 
     player.damage(bullet.damageAmount);
     shields.render()
+
+    if (player.alive) {
+        var explosion = explosions.getFirstExists(false);
+        explosion.reset(player.body.x + player.body.halfWidth, player.body.y + player.body.halfHeight);
+        explosion.alpha = 0.7;
+        explosion.play('explosion', 30, false, true);
+    } else {
+        playerDeath.x = player.x;
+        playerDeath.y = player.y;
+        playerDeath.start(false, 1000, 10, 10);
+    }
 }
 

You made it! Try to tweak the code at this point to give the game a different feeling. Or try adding another enemy type or a new weapon upgrade. Or maybe some astroids to dodge. Or some bonus items to collect. You can spend a lot of time in this phase of game development, tweaking and testing the game.

The next and final part of this tutorial series is the biggest and most complex: adding a boss to the game! Be sure to check it out.

Post any questions below, or link to your forked versions of the game. See you at the boss.