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

phaser.js game demo

This is part 3 of my javascript game development tutorial series with phaser.js. Last time we added a plasma trail to our ship and set up our shooting behaviour.

In this part we will add enemies to shoot at! We’ll give our enemies “personality” by defining their movement style, and we’ll wire up the necessary collision detections.

If you haven’t gone through the earlier parts, you might want to do 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 12: Add enemies!

Now that our ship has a gun, we need something to shoot at.

Let’s make some enemies. Add an enemy ship asset and create a group of them. We launch them with a repeated random timer, and give them a random downward diagonal speed.

By setting an x-axis drag only, the ships appear to adjust their course as they get closer to our own ship. Although there is no true “homing” calculation, it almost looks as if they are aiming for us kamikaze-style.

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


...
 var game = new Phaser.Game(800,600, Phaser.AUTO, 'phaser-demo', {preload: preload, create: create, update: update, render: render});
 
 var player;
+var greenEnemies;
 var starfield;
 var cursors;
 var bank;
...
     game.load.image('starfield', '/assets/starfield.png');
     game.load.image('ship', '/assets/player.png');
     game.load.image('bullet', '/assets/bullet.png');
+    game.load.image('enemy-green', '/assets/enemy-green.png');
 }
 
 function create() {
...
     player.body.maxVelocity.setTo(MAXSPEED, MAXSPEED);
     player.body.drag.setTo(DRAG, DRAG);
 
+    //  The baddies!
+    greenEnemies = game.add.group();
+    greenEnemies.enableBody = true;
+    greenEnemies.physicsBodyType = Phaser.Physics.ARCADE;
+    greenEnemies.createMultiple(5, 'enemy-green');
+    greenEnemies.setAll('anchor.x', 0.5);
+    greenEnemies.setAll('anchor.y', 0.5);
+    greenEnemies.setAll('scale.x', 0.5);
+    greenEnemies.setAll('scale.y', 0.5);
+    greenEnemies.setAll('angle', 180);
+    greenEnemies.setAll('outOfBoundsKill', true);
+    greenEnemies.setAll('checkWorldBounds', true);
+
+    launchGreenEnemy();
+
     //  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);
+    if (enemy) {
+        enemy.reset(game.rnd.integerInRange(0, game.width), -20);
+        enemy.body.velocity.x = game.rnd.integerInRange(-300, 300);
+        enemy.body.velocity.y = ENEMY_SPEED;
+        enemy.body.drag.x = 100;
+    }
+
+    //  Send another enemy soon
+    game.time.events.add(game.rnd.integerInRange(MIN_ENEMY_SPACING, MAX_ENEMY_SPACING), launchGreenEnemy);
+}

Step 13: Add rotation to enemy ships for more natural movement

To complete the movement of the enemy ships, they should face the direction they are moving.

Each spite can have its own update function, and in it we just need a little more trig to get them facing the right way. Arc tan ftw!

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


...
         enemy.body.velocity.x = game.rnd.integerInRange(-300, 300);
         enemy.body.velocity.y = ENEMY_SPEED;
         enemy.body.drag.x = 100;
+
+        //  Update function for each enemy ship to update rotation etc
+        enemy.update = function(){
+          enemy.angle = 180 - game.math.radToDeg(Math.atan2(enemy.body.velocity.x, enemy.body.velocity.y));
+        }
     }
 
     //  Send another enemy soon
     

Step 14: Add particle emitters for enemy trails

Enemies should have trails too, very similar to our own, with a few tweaks.

Note that we handle disposing of enemies off screen on our own (to make sure they completely clear the screen before being “recycled”), and we use the onKilled event to clean up their associated trails.

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


...
     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.spritesheet('explosion', '/assets/explode.png', 128, 128);
 }
 
 function create() {
...
     greenEnemies.setAll('scale.x', 0.5);
     greenEnemies.setAll('scale.y', 0.5);
     greenEnemies.setAll('angle', 180);
-    greenEnemies.setAll('outOfBoundsKill', true);
-    greenEnemies.setAll('checkWorldBounds', true);
+    greenEnemies.forEach(function(enemy){
+        addEnemyEmitterTrail(enemy);
+        enemy.events.onKilled.add(function(){
+            enemy.trail.kill();
+        });
+    });
 
     launchGreenEnemy();
 
...
         enemy.body.velocity.y = ENEMY_SPEED;
         enemy.body.drag.x = 100;
 
+        enemy.trail.start(false, 800, 1);
+
         //  Update function for each enemy ship to update rotation etc
         enemy.update = function(){
           enemy.angle = 180 - game.math.radToDeg(Math.atan2(enemy.body.velocity.x, enemy.body.velocity.y));
+
+          enemy.trail.x = enemy.x;
+          enemy.trail.y = enemy.y -10;
+
+          //  Kill enemies once they go off screen
+          if (enemy.y > game.height + 200) {
+            enemy.kill();
+          }
         }
     }
 
     //  Send another enemy soon
     game.time.events.add(game.rnd.integerInRange(MIN_ENEMY_SPACING, MAX_ENEMY_SPACING), launchGreenEnemy);
 }
+
+
+function addEnemyEmitterTrail(enemy) {
+    var enemyTrail = game.add.emitter(enemy.x, player.y - 10, 100);
+    enemyTrail.width = 10;
+    enemyTrail.makeParticles('explosion', [1,2,3,4,5]);
+    enemyTrail.setXSpeed(20, -20);
+    enemyTrail.setRotation(50,-50);
+    enemyTrail.setAlpha(0.4, 0, 800);
+    enemyTrail.setScale(0.01, 0.1, 0.01, 0.1, 1000, Phaser.Easing.Quintic.Out);
+    enemy.trail = enemyTrail;
+}

Step 15: Add collision detection between ships

Up to now, the enemy ships pass straight through us. Phaser will take care of optimized, quadtree-backed collision detection for us, we just have to tell it in the update method what objects to check collisions on, and give it a callback.

We use the overlap instead of collide method because we don’t need phaser to adjust the positions of our objects on a collision to keep them from overlapping.

We also add an explosion object pool to draw from when our ships collide. Each explosion is a spitesheet animation, which we can play on collisions.

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


...
 var cursors;
 var bank;
 var shipTrail;
+var explosions;
 var bullets;
 var fireButton;
 var bulletTimer = 0;
...
     shipTrail.setAlpha(1, 0.01, 800);
     shipTrail.setScale(0.05, 0.4, 0.05, 0.4, 2000, Phaser.Easing.Quintic.Out);
     shipTrail.start(false, 5000, 10);
+
+    //  An explosion pool
+    explosions = game.add.group();
+    explosions.enableBody = true;
+    explosions.physicsBodyType = Phaser.Physics.ARCADE;
+    explosions.createMultiple(30, 'explosion');
+    explosions.setAll('anchor.x', 0.5);
+    explosions.setAll('anchor.y', 0.5);
+    explosions.forEach( function(explosion) {
+        explosion.animations.add('explosion');
+    });
 }
 
 function update() {
...
 
     //  Keep the shipTrail lined up with the ship
     shipTrail.x = player.x;
+
+    //  Check collisions
+    game.physics.arcade.overlap(player, greenEnemies, shipCollide, null, this);
 }
 
 function render() {
...
     enemyTrail.setScale(0.01, 0.1, 0.01, 0.1, 1000, Phaser.Easing.Quintic.Out);
     enemy.trail = enemyTrail;
 }
+
+
+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();
+}

Step 16: Use debugging to adjust hit bounds of enemy ship

You might have noticed that the enemies collide with our ship before they actually touch us. This probably means that the bounding box around the enemy is too large. We can check that with one of phaser’s debugging options.

Debugging confirms that suspicion, and helps us find the right scaling compensation to make the bounding boxes fit the enemy ship graphic best.

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


...
     greenEnemies.setAll('angle', 180);
     greenEnemies.forEach(function(enemy){
         addEnemyEmitterTrail(enemy);
+        enemy.body.setSize(enemy.width * 3 / 4, enemy.height * 3 / 4);
         enemy.events.onKilled.add(function(){
             enemy.trail.kill();
         });
...
 }
 
 function render() {
-
+    // for (var i = 0; i < greenEnemies.length; i++)
+    // {
+    //     game.debug.body(greenEnemies.children[i]);
+    // }
+    // game.debug.body(player);
 }
 
 function fireBullet() {
 

Step 17: Add collisions for bullets hitting enemy ships

At the moment, our bullets are useless against the enemy ships. Adding a collision check for our bullet group against the enemies group will fix that.

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


...
 
     //  Check collisions
     game.physics.arcade.overlap(player, greenEnemies, shipCollide, null, this);
+    game.physics.arcade.overlap(greenEnemies, bullets, hitEnemy, null, this);
 }
 
 function render() {
...
     explosion.play('explosion', 30, false, true);
     enemy.kill();
 }
+
+
+function hitEnemy(enemy, bullet) {
+    var explosion = explosions.getFirstExists(false);
+    explosion.reset(bullet.body.x + bullet.body.halfWidth, bullet.body.y + bullet.body.halfHeight);
+    explosion.body.velocity.y = enemy.body.velocity.y;
+    explosion.alpha = 0.7;
+    explosion.play('explosion', 30, false, true);
+    enemy.kill();
+    bullet.kill()
+}

This is really starting to feel like a game now! Check out part 4, where we add a very important element of game design: pacing.

As always, put your questions or comments below, and please share this tutorial if it has been helpful. See you in part 4.