fbpx

Build a Space Shooter with Phaser 3 – 5

In the last part of this course, “Build a Space Shooter with Phaser 3”, we have added the bulk of our code for our space shooter. We have covered adding enemies, player lasers, enemy lasers, frustum culling, and collisions in the last part. There are a few things we will finish up in this part to conclude this course. We will be adding a scrolling background, filling in the main menu, and create the game over screen.

We will start by adding the scrolling background. The scrolling background will have multiple, scrolling at different speeds. First, let’s go to our Entities.js file. At bottom of the file, we can add a new class, ScrollingBackground. It does not need to extend anything.

class ScrollingBackground {
  constructor(scene, key, velocityY) {
    
  }
}

Our constructor will be taking in the scene we instantiate a scrolling background, and the image key of our star background. We will first set the scene of the instance to our parameter we’ve taken in. We will also store our keys into an instance of a scrolling background.

this.scene = scene;
this.key = key;
this.velocityY = velocityY;

We will be implementing a function called createLayers. Before we do however, we need to create a group inside our constructor.

this.layers = this.scene.add.group();

Inside our new createLayers function, let’s add the following code to create sprites out of the image key we’re given.

    for (var i = 0; i < 2; i++) {
      // creating two backgrounds will allow a continuous scroll
      var layer = this.scene.add.sprite(0, 0, this.key);
      layer.y = (layer.displayHeight * i);
      var flipX = Phaser.Math.Between(0, 10) >= 5 ? -1 : 1;
      var flipY = Phaser.Math.Between(0, 10) >= 5 ? -1 : 1;
      layer.setScale(flipX * 2, flipY * 2);
      layer.setDepth(-5 - (i - 1));
      this.scene.physics.world.enableBody(layer, 0);
      layer.body.velocity.y = this.velocityY;

      this.layers.add(layer);
    }

The above code iterates through each key we take in. For each key, we create two sprites with the key at each iteration of the first for loop. We then add the sprite to our layers group. We then apply a downwards velocity in which each layer is slower the farther back they are based on the value of i.

We can then call createLayers at the bottom of our constructor.

this.createLayers();

Now we can head over to SceneMain.js and initialize the scrolling background. Insert the following code before we instantiate the player and add it after we define this.sfx.

this.backgrounds = [];
for (var i = 0; i < 5; i++) { // create five scrolling backgrounds
  var bg = new ScrollingBackground(this, "sprBg0", i * 10);
  this.backgrounds.push(bg);
}

Try running the game, you should see a the stars behind the player. All four backgrounds should be drawn now. We can now head back to Entities.js and add an update function with the following code inside:

    if (this.layers.getChildren()[0].y > 0) {
      for (var i = 0; i < this.layers.getChildren().length; i++) {
        var layer = this.layers.getChildren()[i];
        layer.y = (-layer.displayHeight) + (layer.displayHeight * i);
      }
    }

The above code allows the background layers to wrap back around to the bottom. If we didn’t have this code, the backgrounds will just move off-screen and there will remain the black background. We can go back to SceneMain.js and call the update function of our scrolling background instance. In the update function of SceneMain, add the following code:

    for (var i = 0; i < this.backgrounds.length; i++) {
      this.backgrounds[i].update();
    }

That concludes adding our background to the game! If we run the game we should see multiple background layers scrolling down at different speeds.

We can finish up by adding our main menu and game over screen.

Navigate to SceneMainMenu and remove the line that starts SceneMain. Before we continue however, we should a sound effect object for SceneMainMenu. Add the following to the very top of the create function:

this.sfx = {
  btnOver: this.sound.add("sndBtnOver"),
  btnDown: this.sound.add("sndBtnDown")
};

We can then add the play button to the create function by adding a sprite.

this.btnPlay = this.add.sprite(
  this.game.config.width * 0.5,
  this.game.config.height * 0.5,
  "sprBtnPlay"
);

In order to start SceneMain, we will need to first set our sprite as interactive. Add the following directly below where we defined this.btnPlay:

this.btnPlay.setInteractive();

Since we set our sprite as being interactive, we can now add pointer events such as over, out, down, and up. We can execute code when each of these events are triggered by the mouse or tap. The first event we will add is the pointerover event. We will be changing the texture of the button to our sprBtnPlayHover.png image when the pointer moves over the button. Add the following after we set our button as interactive:

this.btnPlay.on("pointerover", function() {
  this.btnPlay.setTexture("sprBtnPlayHover"); // set the button texture to sprBtnPlayHover
  this.sfx.btnOver.play(); // play the button over sound
}, this);

If we run the game and move the mouse over the button we should see:

Now we can add the pointerout event. In this event we will reset the texture back to the normal play button image. Add the following under where we define the pointerover event:

this.btnPlay.on("pointerout", function() {
  this.setTexture("sprBtnPlay");
});

If we run the game again and move the mouse over the button, then off, we should see the button texture reset to the default image.

Next, we can add the pointerdown event. This is where we will change the texture of the play button to sprBtnPlayDown.png.

this.btnPlay.on("pointerdown", function() {
  this.btnPlay.setTexture("sprBtnPlayDown");
  this.sfx.btnDown.play();
}, this);

If we run the game, we should see the button texture change to sprBtnPlayDown.png when we move the mouse over the button and click. We can then add the pointerup event to reset the button texture after we click.

this.btnPlay.on("pointerup", function() {
  this.setTexture("sprBtnPlay");
}, this);

We can a line one more line inside our pointerup event to start SceneMain. The final pointerup event should look like:

this.btnPlay.on("pointerup", function() {
  this.btnPlay.setTexture("sprBtnPlay");
  this.scene.start("SceneMain");
}, this);

When we run the game and click the play button, it should now start SceneMain!

It works!

There are now just a couple things we can do to finish up our main menu. The first is adding a title. To add our title, we can create text. Add the following under the pointerup event

this.title = this.add.text(this.game.config.width * 0.5, 128, "SPACE SHOOTER", {
  fontFamily: 'monospace',
  fontSize: 48,
  fontStyle: 'bold',
  color: '#ffffff',
  align: 'center'
});

To center the title, we can set the origin of the text to half the width, and half the height. We can do this by writing the following under our title definition:

this.title.setOrigin(0.5);

The last thing we will do for our main menu is add our scrolling background in. We can copy the code from the create function of SceneMain to SceneMainMenu, but the code is available below as well.

this.backgrounds = [];
for (var i = 0; i < 5; i++) {
  var keys = ["sprBg0", "sprBg1"];
  var key = keys[Phaser.Math.Between(0, keys.length - 1)];
  var bg = new ScrollingBackground(this, key, i * 10);
  this.backgrounds.push(bg);
}

We can also create the update function as well. In the update function we will update our background layers. Add the following to the update function to update our scrolling backgrounds.

for (var i = 0; i < this.backgrounds.length; i++) {
  this.backgrounds[i].update();
}

Try running the game now. You may notice some green squares in the top-left corner of the screen.

The reason why we are not seeing our backgrounds, is because they haven’t been loaded yet. They have been loaded in SceneMain, but if we look at our scene array in game.js, SceneMain is after SceneMainMenu so we are not able to access loaded content from SceneMain. To fix this, we will have to move the lines loading sprBg0.png and sprBg1.png to the preload function of SceneMainMenu. The preload function of SceneMainMenu should look similar to:

this.load.image("sprBg0", "content/sprBg0.png");
this.load.image("sprBg1", "content/sprBg1.png");
this.load.image("sprBtnPlay", "content/sprBtnPlay.png");
this.load.image("sprBtnPlayHover", "content/sprBtnPlayHover.png");
this.load.image("sprBtnPlayDown", "content/sprBtnPlayDown.png");
this.load.image("sprBtnRestart", "content/sprBtnRestart.png");
this.load.image("sprBtnRestartHover", "content/sprBtnRestartHover.png");
this.load.image("sprBtnRestartDown", "content/sprBtnRestartDown.png");

this.load.audio("sndBtnOver", "content/sndBtnOver.wav");
this.load.audio("sndBtnDown", "content/sndBtnDown.wav");

When we now run the game, we should see background looking how it should.

The final part of this course will be fleshing out SceneGameOver and adding the scene start code in the appropriate colliders.

We can copy over the title code from SceneMainMenu to SceneGameOver. In the create function of SceneGameOver, we can add the following code to create our title. This title code is essentially identical to the code we used previously. The only change we make is changing the string to draw from "SPACE SHOOTER" to GAME OVER".

this.title = this.add.text(this.game.config.width * 0.5, 128, "GAME OVER", {
  fontFamily: 'monospace',
  fontSize: 48,
  fontStyle: 'bold',
  color: '#ffffff',
  align: 'center'
});
this.title.setOrigin(0.5);

Next we can add our sound effect object, as we did with both SceneMainMenu and SceneMain.

this.sfx = {
  btnOver: this.sound.add("sndBtnOver"),
  btnDown: this.sound.add("sndBtnDown")
};

Once we have added the game over title and sound effect object, we can add in the restart button. The code is pretty much identical to that which we used with the play button. Add the following to the create function of SceneGameOver:

    this.btnRestart = this.add.sprite(
      this.game.config.width * 0.5,
      this.game.config.height * 0.5,
      "sprBtnRestart"
    );

    this.btnRestart.setInteractive();

    this.btnRestart.on("pointerover", function() {
      this.btnRestart.setTexture("sprBtnRestartHover"); // set the button texture to sprBtnPlayHover
      this.sfx.btnOver.play(); // play the button over sound
    }, this);

    this.btnRestart.on("pointerout", function() {
      this.setTexture("sprBtnRestart");
    });

    this.btnRestart.on("pointerdown", function() {
      this.btnRestart.setTexture("sprBtnRestartDown");
      this.sfx.btnDown.play();
    }, this);

    this.btnRestart.on("pointerup", function() {
      this.btnRestart.setTexture("sprBtnRestart");
      this.scene.start("SceneMain");
    }, this);

After our button code, we can create our background layers. As we have before, we can add the following code to the create function:

this.backgrounds = [];
for (var i = 0; i < 5; i++) {
  var keys = ["sprBg0", "sprBg1"];
  var key = keys[Phaser.Math.Between(0, keys.length - 1)];
  var bg = new ScrollingBackground(this, key, i * 10);
  this.backgrounds.push(bg);
}

Then add our update code to update the backgrounds in the update function:

for (var i = 0; i < this.backgrounds.length; i++) {
  this.backgrounds[i].update();
}

We finished adding the game over title, restart button, and scrolling background layers. That’s great, except we can’t see our changes yet because we haven’t started SceneGameOver anywhere yet. To change this, we can go to our Entities.js file and create an onDestroy function for our player. The plan is, we will want to create an event that will start SceneGameOver after a slight delay. Inside our new onDestroy function, we can add the following code:

this.scene.time.addEvent({ // go to game over scene
  delay: 1000,
  callback: function() {
    this.scene.scene.start("SceneGameOver");
  },
  callbackScope: this,
  loop: false
});

In order to finish with our onDestroy function, we will need to go back to SceneMain.js and look in the create function. Specifically we will have to call the player’s onDestroy function in every collider that involves the player. Just after we call player.explode(false);, we can insert: player.onDestroy();

There we have it! This concludes the end of our five part course, “Build a Space Shooter with Phaser 3”!

There are quite a features you could add to this project as well. A few I can think of are:

  • add lives
  • add a score
  • add increasing difficulty
  • add upgrade
  • add bosses

If you decide to expand this project, I would really love to hear about it! Add a comment below or email me at jared.york@jaredyork.com. 🙂


We covered the majority of the components you need to make your own games with Phaser 3. I would also like to thank the Phaser 3 team for making such an awesome HTML5 game framework and for all the work they have done. You can learn more about Phaser at their official website, here. As always, please feel free to ask me questions, and I will be more than glad to help. I’m thinking about creating more free and paid courses in the future. Let me know what you would like a course about and I may consider it. 🙂

The final code for this course is available on GitHub.

If you would like to receive updates on future courses I release, please feel free to fill out the form.

21 Comments

  • Cam Abreu says:

    Excellent. Followed the tutorial and typed everything by hand as if it was one of those old programming magazines that John Carmack used to read and code games from when he was a kid. I have a background in public school education (middle school teacher), and I want to say that your tutorial was excellent! You started off simple, each section getting a little longer and harder. You didn’t give the reader EVERYTHING, and made them think about some things. I very much learned from the experience and am comfortable working on my own with phaser after this ONE tutorial. Brilliant!

    • Jared York says:

      Hi Cam! I appreciate the kind words! It can be hard for me to judge if my writing makes any sense for others, so your confirmation is helpful. Yes, it’s quite difficult to balance how much information to provide to the reader. Hopefully it helps that I just published a new course called, “JavaScript Beginner Blocks”, which brings everyone new to JavaScript up to speed. I figure if readers aren’t familiar with JavaScript, I can just refer them to my course. I have some new Phaser courses in the works, as well as courses on other languages as well. Stay tuned for more. 🙂

  • Lucas says:

    Hey,
    is it possible to get this on a smartphone?

    With Cordova for example?

    • Jared York says:

      Hi Lucas, thanks for taking a look at my course! Yes, it is possible to get it on a smartphone. I may make a tutorial for doing so. To fully answer your question, I found that using Cordova with the Ionic Webview for Cordova plugin: https://github.com/ionic-team/cordova-plugin-ionic-webview

      I would use that plugin with Cordova, and you should have no issues getting any Phaser game on a mobile device. Obviously there’s some tweaks needed to be able to interact with the game on mobile, perhaps that’s what you’re also referring to as well? 🙂

      • michelangelo says:

        Do you think that the Ionic version will also benefit on Android? I read that IOS uses WKWebView but on android it should not be much different, even seeing the code seems to me extends the system webview:
        https://github.com/ionic-team/cordova-plugin-ionic-webview/blob/master/src/android/com/ionicframework/cordova/webview/IonicWebViewEngine.java#L29
        It is not a controversy, I would really like what you have on android with ionic webview.
        I also tried the code on mobile and to add buttons to create a virtual \”pad\”, but I can not make two buttons work together, for example shot and direction, would you have suggestions?
        Anyway thank you very much for the tutorial.

        • Jared York says:

          The only reason why I use the ionic version is because I couldn’t get any assets to load with a normal Cordova build on iOS and Android. For some reason the content origin policy kept blocking the asset loading, but the ionic web view seemed to fix the problem. Unfortunately, I haven’t dug much into multitouch yet. I thought here was an array that held all the touches or pointers you could iterate through but I’m not positive. I’m glad to hear the tutorial helped you out though! I should be able to write a multitouch tutorial once I learn more about how to implement it.

  • Podcast says:

    Hey Cam.

    Is it possible to get the game on a smartphone with cordova?

  • Michael Vorm says:

    Hi Jared.

    I want to thank you for creating such a easy to follow and fun tutorial! Great intro to Phaser!

  • Nícolas Iensen says:

    Great tutorial, Jared.

    I gave it a try using Parcel, a bundler for web applications, you can check it out at https://github.com/nicolasiensen/space-shooter

    Cheers mate!

    • Jared York says:

      Thanks Nícolas! Your adaptation of my tutorial looks very interesting. I’ve never tried Parcel before, but it looks like a great utility for organizing and packaging the code. I will definitely check it out more in-depth. I’m glad you enjoyed the tutorial! Thanks for checking it out.

  • ReydVires says:

    I love this tutorial than ever! I\’m waiting for another.

  • Flavio says:

    Great tutorial, for me it was good that you did not give everything right away and I made some mistakes and had to figure out by myself.

    I’m trying to add a paralax background now with layers of galaxy and planets… let’s see how it goes.

    It would be nice to have a tut explaining how to create an game app using Cordova adding ads and finally publishing it in Google Play.

    • Jared York says:

      I’m glad you found my tutorial helpful! I usually try to leave some parts a bit vague as an execise for the reader. 🙂 It shouldn’t be too hard to add those parallax layers. I will definitely consider a Cordova tutorial covering ads and how to publish to Google Play.

  • Jovan Cahiles says:

    Hey Jared, thanks for the tutorial. I\’d need some insight on to something. Why is it that we need to do a double \”scene\” inside the onDestroy function in Player?

    • Jared York says:

      Hi Jovan,

      My apologies for not seeing your comment until now. The reason we need to double “scene” in the onDestroy function in because if you were to start a scene within a scene class, you would write:

      this.scene.start(“SceneWhatever”);

      Since we can’t access scene within our Player class by default (until we inherit the constructor of Entity, which sets scene to it’s own property within our Player class), this is why we have to write the first this.scene to access the instance of the scene class. Then we have to add the second “scene” to access that scene “subsystem” that you would normally access inside a scene class. Hopefully this helps clear this up. Please let me know if you need more help. Sorry again for not finding your comment until now. Have a great day!

  • Marc Holman says:

    Hi,

    Everything was very clear to me up to the scrolling background. I’m pretty confused here. I hate to be a pain but can you take me through the scrolling line by line – this is the most important part for me and I am thoroughly lost here.

    Thanks alot!
    Marc

  • marc holman says:

    For example, if there are two layers, what are the four backgrounds you refer to doing? The movement code especially is confusing and I would suggest in future tutorials taking a line by line approach for less self-explanatory code instead of writing the whole code block and then just generally saying what it does without going into the details.

  • marc holman says:

    Also we are duplicating a lot of code with the scrolling background in each scene. Is there a reason you didn’t decide to just create a scrolling background class that subsumes all of this in one object? I have seen code that extends scene and uses super({key: ‘key’, active: true}) to run one scene inside another and this would be a good way to roll all of this code into one object. I’ll try to work it out and shoot you a link if I’m successful.

    • Jared York says:

      Hi Marc,

      Regarding the enemies group update, you’re totally right. It would make more sense to add an empty update method to the entity class, or check if the update function is null or undefined (but may run into an issue by what you mentioned in your last question of your first reply. I honestly have no idea why any of the browsers throw an error with the missing update for the GunShip, and CarrierShip. I will have to investigate this further when I have more time. I’ll update the tutorial once I figure out the issue.

      Regarding the ScrollingBackground class. This class represents a single layer of a scrolling background.

      Step 1: Define the class. We are taking in a scene (to create the layer sprites), taking in an image key (for the layer sprites), as well as the Y velocity.

      Step 2: Create a group to store our two sprites (the top and bottom, so the scrolling is seamless without cut-offs.)

      Step 3: Add the createLayers method. This is where we create the two sprites for the seamless scrolling of this layer (a ScrollingBackground instance.)

      Step 4: Add a for loop that runs twice. This ensures that we will get two sprites added to our layers group in the end. In our for loop, we create a sprite with the image key we’ve received. First, we are setting the position of the sprite to 0, 0. This is because, we don’t know the height of the layer before hand so we then set the proper Y position of the layer after we’ve instantiated the sprite.

      Next, we just sprinkle in a little randomness to the appearance of the current layer sprite. There’s a roughly 50% chance that the image will be flipped horizontally and vertically.

      To apply our flip lines, we call layer.setScale, where we add our flipX and flipY variables. We multiply the two variables by two when we set the scale. Since the background png image in the content folder is only 50% of the game screen, we have to scale it up two times.

      Then, we set the depth of the background. We initially set it to -5. Sprites and images with negative numbered depths are drawing at a lower depth than sprites and images with higher depths. Looking back, there really isn’t a point to setting the depth to -5 – (i – 1). Coming up with the background code was extremely tedious to pull off (I was trying to remember the exact method I wrote in previous prototypes.) Really, the depth can be set to -5, which allows the star layers to be drawn beneath the entities. After we set the depth, we then enable the physics body, and provide it the sprite. Then we set the Y velocity of the sprite. In order for the velocity to make the sprite do something, this is why it’s important to enable the physics body of the sprite. Thanks for asking about this, it’s making me go back and reconsider why I wrote some of the code the way I did. Layers is probably not the right way to refer the two sprites. The ScrollingBackground class would probably make way more sense being renamed to ScrollingBackgroundLayer. However, if that’s renamed, what is an understandable name for the group when the top and bottom sprites of the layer is stored? I’ve yet to try to think of a name that makes sense. Thank you for the constructive suggestion regarding going line by line through the tutorial, this is super valuable.

      I haven’t experimented too much with running multiple scenes at once. I’ve tried using it for creating a GUI for the last Ludum Dare game jam, but that’s the only experience I’ve had with it. If you figure out a way to make a scrolling background work with another active scene, I’d love to take a look at it. Thanks again for your series of comments, hopefully my explanation helps a little bit (let me know if you need further clarification), and it’s also making me rethink quite a bit of my code. I will definitely take your questions to heart and revise some of the tutorial to reflect them. If you would like to see more of my tutorials in the future, I’m going to be starting an email newsletter which can be subscribed to here. I’d like to try taking a more line by line approach in the future, and perhaps you would be interested in seeing future tutorials in that kind of format. 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *