fbpx

Build a Space Shooter with Phaser 3 – 3

In the last part, we finished setting up the basis for our scene files: SceneMainMenu.js, SceneMain.js, and SceneGameOver.js. At this point you can try running the game by navigating to localhost/(game folder name)/index.html. There might be a port that’s needed when viewing files on some web servers, for example, localhost:8000. If everything is right, you should see a black rectangle on the page. The black rectangle is the area where your game will be drawn.

It’s time to dive back into the code. Take a second and open up SceneMain.js. When we left off, our SceneMain.js should look like:

class SceneMain extends Phaser.Scene {
  constructor() {
    super({ key: "SceneMain" });
  }
  create() {
    
  }
}

At this point we need to add a new function called preload. This function should be inserted between constructor and create. All three functions inside SceneMain should look like:

  constructor() {
    super({ key: "SceneMain" });
  }
  preload() {

  }
  create() {
    
  }

Inside our new preload function, we need to add the code to load our game assets. To load image files, the line you would type inside the preload function would be:

this.load.image(imageKey, path);

If we start with our first image file we want to load, sprBg0.png, we should add:

this.load.image("sprBg0", "content/sprBg0.png");

The first parameter of this.load.image refers to the key (a string) that we will reference the image when we add images and sprites within the game. The second parameter is the path to the image we wish to load.

Lets finish adding the rest of our loading code for our images:

this.load.image("sprBg0", "content/sprBg0.png");
this.load.image("sprBg1", "content/sprBg1.png");
this.load.spritesheet("sprExplosion", "content/sprExplosion.png", {
  frameWidth: 32,
  frameHeight: 32
});
this.load.spritesheet("sprEnemy0", "content/sprEnemy0.png", {
  frameWidth: 16,
  frameHeight: 16
});
this.load.image("sprEnemy1", "content/sprEnemy1.png");
this.load.spritesheet("sprEnemy2", "content/sprEnemy2.png", {
  frameWidth: 16,
  frameHeight: 16
});
this.load.image("sprLaserEnemy0", "content/sprLaserEnemy0.png");
this.load.image("sprLaserPlayer", "content/sprLaserPlayer.png");
this.load.spritesheet("sprPlayer", "content/sprPlayer.png", {
  frameWidth: 16,
  frameHeight: 16
});

The lines that include spritesheet means we are loading an animation instead of a static image. A sprite sheet is an image with multiple frames, side-by-side. The third argument of the load.spritesheet function is an object defining the frame width and height in pixels. Now we will need to add the loading code for our sounds, which follow the same format as loading images:

this.load.audio("sndExplode0", "content/sndExplode0.wav");
this.load.audio("sndExplode1", "content/sndExplode1.wav");
this.load.audio("sndLaser", "content/sndLaser.wav");

Once we have loaded our content, we need to add a little bit more code to create our animations. At the top of the create function of our screen we will create our animations:

    this.anims.create({
      key: "sprEnemy0",
      frames: this.anims.generateFrameNumbers("sprEnemy0"),
      frameRate: 20,
      repeat: -1
    });

    this.anims.create({
      key: "sprEnemy2",
      frames: this.anims.generateFrameNumbers("sprEnemy2"),
      frameRate: 20,
      repeat: -1
    });

    this.anims.create({
      key: "sprExplosion",
      frames: this.anims.generateFrameNumbers("sprExplosion"),
      frameRate: 20,
      repeat: 0
    });

    this.anims.create({
      key: "sprPlayer",
      frames: this.anims.generateFrameNumbers("sprPlayer"),
      frameRate: 20,
      repeat: -1
    });

We also need to add our sounds to some sort of variable or object so we can reference it later. I like to organize my sound references by storing them as values of a sound effect object. If there are more than one sound of a type (say, three explosion sounds), I add an array as the value of a property (going with the last example, I would go with “explosion” as the property key.) Let’s add our sound effect object and hopefully it will make more sense:

this.sfx = {
  explosions: [
    this.sound.add("sndExplode0"),
    this.sound.add("sndExplode1")
  ],
  laser: this.sound.add("sndLaser")
};

We will later be able to play sound effects from our object with, for example:

this.sfx.laser.play();

We will also have to load some images and sounds for the main menu and game over screen. Open up SceneMainMenu.js and create a preload function inside SceneMainMenu. Inside the new preload function add the following to add our buttons and sounds:

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");

Once we have added the loading code for our images and sounds. We have also created our animations and added our sounds to an object for organization. Now we can navigate back to the game in your browser and a black rectangle should still being showing. If you haven’t already, open the development tools in the browser you’re using. If you are using Chrome or Firefox, you can simply press F12 to open it. Look under the Console tab to ensure there are no errors (they are displayed in red.) If you see no errors, we can proceed to proceed with adding the player!

Before we add the player spaceship to our game, we should add a new file in our JavaScript folder called Entities.js. This entities script will contain all of the classes for the various entities in our game. We will classify the player, enemies, lasers, etc. as entities. Be sure to also add a reference to Entities.js to our index.html a line before SceneMainMenu.js. After adding the reference to our Entities.js file, open it and declare a new class named Entity.

class Entity {
  constructor() {
    
  }
}

After declaring our Entity class, we need to add some parameters that our Entity class should take in. We will add these parameters between the parenthesis in our constructor:

constructor(scene, x, y, key, type) {

}

Each of the parameters we’ve added to the constructor will be important, because we will be extending Phaser.GameObjects.Sprite for all entities we create. Much like how we extended Phaser.Scene when we first started this game, we will want to build on top of Phaser’s Sprite class. Because of this, we will have to change class Entity { to:

class Entity extends Phaser.GameObjects.Sprite {

As always with extending a class, we will need to add super to our constructor. Since the player, enemies, and various projectiles we add will have the same basics properties, it helps to keep us from adding redundant, duplicate code. In this way we will be inheriting the properties and functions of the base Phaser.GameObjects.Sprite class for all of our entities. Here’s (a crude diagram) of the inheritance hierarchy we will be implementing:

         Phaser.GameObjects.Sprite
                   |
                 Entity
                  _|_
               _/  |  \_
            _/     |     \_
          /        |        \
      Player     Enemy     Laser
                   |
                 _/ \_
               /       \
         ChasingShip  GunShip

All of our entities will have the same basic properties (our scene, x position, y position, image key, and a type string we will use to fetch certain entities if we need to.) Lets add super into our constructor which should look like:

super(scene, x, y, key);

Essentially, what we are doing is taking the parameters in when an Entity is instantiated and providing scene, x, y, and key to the Phaser.GameObjects.Sprite base class.

After adding the super keyword to the constructor, on the next line, add the following lines:

this.scene = scene;
this.scene.add.existing(this);
this.scene.physics.world.enableBody(this, 0);
this.setData("type", type);
this.setData("isDead", false);

This piece of code assigns the scene of the Entity, as well as adding an instantiated Entity to the rendering queue of the scene. We also enable instantiated Entity‘s as physics objects in the physics world of the scene. Finally, we define the speed of the player. With that we should be finished adding to our Entity class. Now we can move on to creating our Player class.

By now, you should know the drill for adding a class. Add one right after the Entity class and name it Player and ensure it extends Entity. Add a constructor to the Player class with parameters: scene, x, y, and key. Then add our super keyword in the constructor providing it the following parameters:

super(scene, x, y, key, "Player");

We also will want a way to determine the speed that the player should move. By adding a speed key/value pair for the speed of the player, we can refer to that later for our movement functions. Under the super keyword, add the following:

this.setData("speed", 200);

We also will want to add a little piece of code to play the player animation:

this.play("sprPlayer");

To add the movement functions, add the following four functions after the constructor.

moveUp() {

}

moveDown() {

}

moveLeft() {

}

moveRight() {

}

Now add the following lines in each of the functions:

moveUp() {
  this.body.velocity.y = -this.getData("speed");
}

moveDown() {
  this.body.velocity.y = this.getData("speed");
}

moveLeft() {
  this.body.velocity.x = -this.getData("speed");
}

moveRight() {
  this.body.velocity.x = this.getData("speed");
}

These are the movement functions that will be called in the update function. Next, add the update function directly below the moveRight function. Inside the update function add:

this.body.setVelocity(0, 0);

this.x = Phaser.Math.Clamp(this.x, 0, this.scene.game.config.width);
this.y = Phaser.Math.Clamp(this.y, 0, this.scene.game.config.height);

We are now finished with the Player class! In the last bit of code, every game update the player’s velocity will be set to zero. If none of the movement keys are pressed, the player will stay still. The next two lines of the player update code ensures that the player cannot move off-screen. At this point, we can create an instance of the player in the create function of SceneMain. Add the following to the create function of SceneMain:

this.player = new Player(
  this,
  this.game.config.width * 0.5,
  this.game.config.height * 0.5,
  "sprPlayer"
); 

This is where we create the instance of the Player is created. We can refer to the player anywhere in SceneMain. The player is then positioned in the center of the canvas. If you were to try running the game, you still won’t see the player move yet. This is because we first have to add the update function to SceneMain and add the movement checks. Since this.player is now added, we can now add the update function. Add the update function right under the create function of SceneMain and add the following inside:

this.player.update();

if (this.keyW.isDown) {
  this.player.moveUp();
}
else if (this.keyS.isDown) {
  this.player.moveDown();
}

if (this.keyA.isDown) {
  this.player.moveLeft();
}
else if (this.keyD.isDown) {
  this.player.moveRight();
}

Like I mentioned before, this.player.update(); will run the update code that will keep the player still, as well as ensure it can’t move off-screen. With the next bit that contains keyW, keyS, keyA, keyD, you may wonder why none of those variables have been initialized yet. We will initialize the key variables in a second. These if statements check if the corresponding key is down, if so, move the player in the appropriate direction. In the create function of SceneMain, add the following to initialize our key variables after we initialize the player:

this.keyW = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W);
this.keyS = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S);
this.keyA = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);
this.keyD = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);
this.keySpace = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

If we run our code now, the player should be able to move via the W, S, A, D keys. In the next part, we will add the ability for the player to shoot lasers (which will use the space key.)


And that concludes the third part of “Build a Space Shooter in Phaser 3”. As always, I welcome your suggestions, comments, and feedback! Do not hesitate to ask if you need help, I would be more than willing to answer any questions. If you would like to receive updates on new courses I release via email, feel free to fill out the form.


28 thoughts on “Build a Space Shooter with Phaser 3 – 3

  1. I followed everything word for so far and nothing is working. I can even say , I did a copy paste of everything so far. Maybe another time,or maybe I’ll just try something different before diving in long projects. ๐Ÿ™‚

    1. Hi Youss,

      Thanks for taking interest in my course! I’m sorry you’re having issues with the code, is there any chance you could zip up your project and send it to me? I would be glad to take a look at it for you and let you know how to fix it. I also have some courses coming down the pipeline on how to recreate some smaller arcade games. Perhaps you would prefer those more? I’d be interested in your thoughts! ๐Ÿ™‚

      1. I would definitely like to do those as well.

        Also, I don\’t know exactly what went wrong, but it seems to be working now. I didn\’t touch anything. Just had to restart my laptop for an update, closed the editor I was working on ,and when I came back it was there. In a page I had open to check.

        Quick question, maybe a too-noob one:

        \”Inside our new preload function, we need to add the code to load our game assets. To load image files, the line you would type inside the preload function would be:

        this.load.image(imageKey, path);\” We are not supposed to type in the parameters right? But rather the arguments and proceed with the rest…?

        1. Yes, that was just an example of the arguments needed. You would actually fill it in with the real arguments as I did near the top of this part:

          this.load.image(“sprBg0”, “content/sprBg0.png”);
          this.load.image(“sprBg1”, “content/sprBg1.png”);
          this.load.spritesheet(“sprExplosion”, “content/sprExplosion.png”, {
          frameWidth: 32,
          frameHeight: 32
          });
          this.load.spritesheet(“sprEnemy0”, “content/sprEnemy0.png”, {
          frameWidth: 16,
          frameHeight: 16
          });
          this.load.image(“sprEnemy1”, “content/sprEnemy1.png”);
          this.load.spritesheet(“sprEnemy2”, “content/sprEnemy2.png”, {
          frameWidth: 16,
          frameHeight: 16
          });
          this.load.image(“sprLaserEnemy0”, “content/sprLaserEnemy0.png”);
          this.load.image(“sprLaserPlayer”, “content/sprLaserPlayer.png”);
          this.load.spritesheet(“sprPlayer”, “content/sprPlayer.png”, {
          frameWidth: 16,
          frameHeight: 16
          });

          this.load.audio(“sndExplode0”, “content/sndExplode0.wav”);
          this.load.audio(“sndExplode1”, “content/sndExplode1.wav”);
          this.load.audio(“sndLaser”, “content/sndLaser.wav”);

          There’s no such thing as too-noob question, there’s plenty I don’t know myself. ๐Ÿ™‚
          I hope this helps.

          1. I get these errors:

            The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. https://goo.gl/7K7WLu
            phaser_raw_script.js:92953 Phaser v3.16.2 (WebGL | Web Audio) https://phaser.io

            (This one appeared at least a dozen times)

            phaser_raw_script.js:64827 Access to XMLHttpRequest at \\\’file:///C:/xampp/htdocs/Space_Shooter_Game/sprPlayer.png\\\’ from origin \\\’null\\\’ has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, secure, https.

            phaser_raw_script.js:77148 Audio cache entry missing: sndExplode0
            phaser_raw_script.js:77148 Audio cache entry missing: sndExplode1
            phaser_raw_script.js:77148 Audio cache entry missing: sndLaser

            phaser_raw_script.js:23832 Uncaught TypeError: Cannot set property \\\’seek\\\’ of undefined
            at WebAudioSound.resetConfig (phaser_raw_script.js:23832)
            at WebAudioSound.play (phaser_raw_script.js:23732)
            at WebAudioSound.play (phaser_raw_script.js:77295)
            at SceneMain.create (SceneMain.js:74)
            at SceneManager.create (phaser_raw_script.js:81071)
            at SceneManager.loadComplete (phaser_raw_script.js:80984)
            at LoaderPlugin.emit (phaser_raw_script.js:2337)
            at LoaderPlugin.loadComplete (phaser_raw_script.js:120224)
            at LoaderPlugin.fileProcessComplete (phaser_raw_script.js:120190)
            at LoaderPlugin.nextFile (phaser_raw_script.js:120134)

            Not sure where to go from here. This was after heading to the console as you advised in the tutorial.Nothing seemed wrong on the surface. But under the bush… Not sure if this hinders my progression in a significant way. I mean the cache part probably does I don\\\’t know.

          2. From the folder you sent me, I don’t encounter any issues. I would recommend clearing your browser’s cache of images and files. That should solve your cache entry missing problems. The AudioContext warning doesn’t really affect anything, from what I’ve seen with my own games. I wouldn’t worry about it unless you can’t play sounds. Try clearing your cache and let me know if it helps. ๐Ÿ™‚

  2. Relative linking it’s not working for me, it keep saying “404 not found” for these lines, but I have organized the directories like you.

    Any idea of why this is happening?

  3. Hello, I followed along exactly, up to part 2, but when i go to check by loading index.html i just get a blank screen. i went back and copy and pasted the code to be 100% sure were no mistakes. I also tried launching html.index from brackets and got the same results. i am not sure what to try next… Any advice?

  4. I lost you half way around …
    super(scene, x, y, key, “Player”);

    Could you provide codes for part3/part4/part5 for users to double check for any errors ?

    Thanks

  5. I tried following your steps, although most of it is copy pasted, I did make some changes based on my little knowledge of Phaser3. For some reason the player ship’s animations aren’t working in my case, the game loads properly though and the player also moves around perfectly, but it’s not animating the player. You can see my code at https://github.com/TheDrone7/spaceshooter/tree/master/public

    Please help me find the problem in my code.
    Thank you

    1. Hi Harmeet,

      Thanks for bringing this to my attention. I actually forgot to add the line for that into the tutorial, my apologies for that. You can add this.play(“sprPlayer”); to the end of the constructor of the Player class. Hope this helps! Thanks for catching this.

    1. Hi roner,

      More modular code could be achieved with a bundler such as Parser. I would refer you to Nรญcolas Lensen’s implementation of my course bundled with Parcel here. For the sake of helping beginners though, I wanted to keep the course simple and use a method anyone can use out of the box.

  6. Hello and thanks for this great tutorial!
    I’m having an issue with my code. The player sprite loads, but when I press any of the W,S,A,D keys, the player sprite disappears. I’ve scoured over my code a few times for typos/errors, but can’t seem to find whats going on.

        1. Hi Jarod,

          On line 15 of your Entities.js file, your line looks like this:

          this.setData('speed, 200');

          It should be:

          this.setData('speed', 200);

          That should fix it for you. Please let me know if you ever have more questions! ๐Ÿ™‚

          1. Thank you so much! For me, it’s always a tiny little error like this! I feel as if maybe I can learn a little about myself from this. Cheers from one Jaro/ed to another, and thanks again!

Leave a Reply

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