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

phaser.js tutorial screenshot

This is part 2 of my javascript game development tutorial series with phaser.js. Part 1 covered setting up phaser and creating the player’s ship and the world.

In this part we will build on what we started in part 1, adding some special effects to the player’s ship, and the ability to shoot. We’ll tweak the firing logic to get the right look and feel. While we’re at it, we add the ability to control the ship by the mouse as well as the arrow keys.

The tutorial is split into five general themes:

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 7: Add particle emitter to ship for a plasma trail effect

In the spirit of making our ship feel more alive, and adding more production value to the game, let’s add a nice plasma trail effect to our ship.

A particle emitter works well for this, and I repurposed my bullet asset to look like some kind of plasma material.

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


...
 var starfield;
 var cursors;
 var bank;
+var shipTrail;
 
 var ACCLERATION = 600;
 var DRAG = 400;
...
 function preload() {
     game.load.image('starfield', '/assets/starfield.png');
     game.load.image('ship', '/assets/player.png');
+    game.load.image('bullet', '/assets/bullet.png');
 }
 
 function create() {
...
 
     //  And some controls to play the game with
     cursors = game.input.keyboard.createCursorKeys();
+
+    //  Add an emitter for the ship's trail
+    shipTrail = game.add.emitter(player.x, player.y + 10, 400);
+    shipTrail.width = 10;
+    shipTrail.makeParticles('bullet');
+    shipTrail.setXSpeed(30, -30);
+    shipTrail.setYSpeed(200, 180);
+    shipTrail.setRotation(50,-50);
+    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);
 }
 
 function update() {
...
     //  Squish and rotate ship for illusion of "banking"
     bank = player.body.velocity.x / MAXSPEED;
     player.scale.x = 1 - Math.abs(bank) / 2;
-    player.angle = bank * 10;
+    player.angle = bank * 30;
+
+    //  Keep the shipTrail lined up with the ship
+    shipTrail.x = player.x;
 }
 
 function render() {
 

Step 8: Add mouse input to control to ship

Let’s give an alternate input method. The ship moves faster the farther away it is from the mouse.

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


...
         player.body.acceleration.x = 0;
     }
 
+    //  Move ship towards mouse pointer
+    if (game.input.x < game.width - 20 &&
+        game.input.x > 20 &&
+        game.input.y > 20 &&
+        game.input.y < game.height - 20) {
+        var minDist = 200;
+        var dist = game.input.x - player.x;
+        player.body.velocity.x = MAXSPEED * game.math.clamp(dist / minDist, -1, 1);
+    }
+
     //  Squish and rotate ship for illusion of "banking"
     bank = player.body.velocity.x / MAXSPEED;
     player.scale.x = 1 - Math.abs(bank) / 2;
     

Step 9: Add basic shooting

What’s a space shooter without the ability to shoot? Let’s add that. We already have a bullet asset (more of an energy burst really) loaded for our plasma trail. We create an object pool of these bullets and add an input to fire them.

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


...
 var cursors;
 var bank;
 var shipTrail;
+var bullets;
+var fireButton;
 
 var ACCLERATION = 600;
 var DRAG = 400;
...
     //  The scrolling starfield background
     starfield = game.add.tileSprite(0, 0, 800, 600, 'starfield');
 
+    //  Our bullet group
+    bullets = game.add.group();
+    bullets.enableBody = true;
+    bullets.physicsBodyType = Phaser.Physics.ARCADE;
+    bullets.createMultiple(30, 'bullet');
+    bullets.setAll('anchor.x', 0.5);
+    bullets.setAll('anchor.y', 1);
+    bullets.setAll('outOfBoundsKill', true);
+    bullets.setAll('checkWorldBounds', true);
+
     //  The hero!
     player = game.add.sprite(400, 500, 'ship');
     player.anchor.setTo(0.5, 0.5);
...
 
     //  And some controls to play the game with
     cursors = game.input.keyboard.createCursorKeys();
+    fireButton = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
 
     //  Add an emitter for the ship's trail
     shipTrail = game.add.emitter(player.x, player.y + 10, 400);
...
         player.body.acceleration.x = 0;
     }
 
+    //  Fire bullet
+    if (fireButton.isDown || game.input.activePointer.isDown) {
+        fireBullet();
+    }
+
     //  Move ship towards mouse pointer
     if (game.input.x < game.width - 20 &&
         game.input.x > 20 &&
...
 
 }
 
+function fireBullet() {
+    //  Grab the first bullet we can from the pool
+    var bullet = bullets.getFirstExists(false);
+
+    if (bullet)
+    {
+        //  And fire it
+        bullet.reset(player.x, player.y + 8);
+        bullet.body.velocity.y = -400;
+    }
+}

Step 10: Improved bullet firing

Now we can shoot, but it doesn’t feel quite right. The bullets are locked to the y-axis, even though our ship is tilting and banking.

To make things better, we need to align the angle of the bullets to the angle of the ship when it fires, and we also need to make the bullets move diagonally – at constant speeds – based on their angles. We also need to make a slight adjustment so the bullet always comes out of the tip of the ship, even when the ship is tilted off its axis.

Trigonometry is our friend here. If you remember your trig from school, this should be a piece of cake. If not, you’ll probably want to brush up, though phaser offers some convenience functions to help.

The basic approach is to use the sine function to map the angle of the ship/bullet to an x and y component, so we can nudge the bullet to line up with the ship, and give it the right amount of horizontal and vertical velocity.

With our math in place, our bullets fire correctly.

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


...
 }
 
 function fireBullet() {
+    var BULLET_SPEED = 400;
     //  Grab the first bullet we can from the pool
     var bullet = bullets.getFirstExists(false);
 
     if (bullet)
     {
         //  And fire it
-        bullet.reset(player.x, player.y + 8);
-        bullet.body.velocity.y = -400;
+        //  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;
     }
 }
 

Step 11: Limit firing rate

We can set a firing rate by moving our firing code behind some logic that tracks the last time we fired.

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


...
 var shipTrail;
 var bullets;
 var fireButton;
+var bulletTimer = 0;
 
 var ACCLERATION = 600;
 var DRAG = 400;
...
 }
 
 function fireBullet() {
-    var BULLET_SPEED = 400;
-    //  Grab the first bullet we can from the pool
-    var bullet = bullets.getFirstExists(false);
-
-    if (bullet)
+    //  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;
+        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;
+        }
     }
 }
 

Thanks for following along! In the next part, we’ll add some enemies to shoot at!

Add your questions or comments below, and please share this tutorial if you find it helpful. See you in part 3.