SpaceShooter at Phaser 3

Hello! This is a translation of the course (link to the original at the end of the article), which covers the creation of a space shooter using Phaser 3. The course will consist of fourteen steps, in each of which a specific task will be solved when creating a project. Before starting the course, it is advisable to know the basics of JavaScript.





Step one. Configure a web server

The first thing to do is set up your web server. Despite the fact that phaser games run in the browser, unfortunately you cannot just run the html file locally directly from the file system. When requesting files over the http protocol, server security allows you to access only those files that you are allowed to. When downloading a file from the local file system (file: //), your browser restricts it heavily for obvious security reasons. Because of this, we will need to host our game on a local web server. You can use any web server you like, be it OpenServer or any other.





Step two. Create files and folders required

Find where your web server hosts the site files and create a folder with your project in it. Name it whatever is convenient for you. Inside the project, create an index.html file. Our index file is where we will declare the location of the phaser script and other game scripts.





Next, we need to create two new folders: content (sprites, audio, etc.) and js (phaser and game scripts). Now, inside the js folder, you need to create 4 files: SceneMainMenu.js, SceneMain.js, SceneGameOver.js, and game.js.





At the moment, the structure of our project should look like this:





project structure at the beginning of work
project structure at the beginning of work

content. , ().





:





Sprites (images)





  • sprBtnPlay.png ( "Play")





  • sprBtnPlayHover.png ( "Play" )





  • sprBtnPlayDown.png ( "Play" )





  • sprBtnRestart.png ( "Restart")





  • sprBtnRestartHover.png ( "Restart" )





  • sprBtnRestartDown ( "Restart" )





  • sprBg0.png ( )





  • sprBg1.png ( )





  • sprEnemy0.png ( )





  • sprEnemy1.png ( )





  • sprEnemy2.png ( )





  • sprLaserEnemy.png ( )





  • sprLaserPlayer.png ( )





  • sprExplosion.png ( )





  • sprPlayer.png ( )





Audio (.wav files)





  • sndExplode0.wav ( )





  • sndExplode1.wav ( )





  • sndLaser.wav ( )





  • sndBtnOver.wav ( )





  • sndBtnDown.wav ( )





.

Phaser. . phaser.js phaser.min.js . , phaser.js , . , - , phaser.min.js. . js .





. Index.html

, , index.html, . IDE .





index.html :





<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta lang="en-us">
    <title>Space Shooter</title>
    <script src="js/phaser.js"></script> <!--     ,    . -->
  </head>
  <body>
    <script src="js/Entities.js"></script>
    <script src="js/SceneMainMenu.js"></script>
    <script src="js/SceneMain.js"></script>
    <script src="js/SceneGameOver.js"></script>
    <script src="js/game.js"></script>
  </body>
</html>
      
      



. , JavaScript . ( Scene) game.js.





.

game.js :





var config = {}
      
      



, phaser. :





type: Phaser.WEBGL,
width: 480,
height: 640,
backgroundColor: "black",
physics: {
  default: "arcade",
  arcade: {
    gravity: { x: 0, y: 0 }
  }
},
scene: [],
pixelArt: true,
roundPixels: true
      
      



, , , WebGL, Canvas. , width height, , . backgroundColor . , physics, , , arcade. , - . physics, (gravity) . scene, , . , , Phaser (pixelArt roundPixels) , , .





scene: [
  SceneMainMenu,
  SceneMain,
  SceneGameOver
],
      
      



. Phaser, :





var game = new Phaser.Game(config);
      
      



game.js ! :





var config = {
    type: Phaser.WEBGL,
    width: 480,
    height: 640,
    backgroundColor: "black",
    physics: {
      default: "arcade",
      arcade: {
        gravity: { x: 0, y: 0 }
      }
    },
    scene: [
        SceneMainMenu,
        SceneMain,
        SceneGameOver
    ],
    pixelArt: true,
    roundPixels: true
}

var game = new Phaser.Game(config);
      
      



.

SceneMainMenu.js :





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



SceneMainMenu, Phaser.Scene. : constructor create. , ( ). :





super({ key: "SceneMainMenu" });
      
      



:





var someScene = new Phaser.Scene({ key: "SceneMainMenu" });
      
      



, Phaser, , , . create . create :





this.scene.start("SceneMain");
      
      



, , . . .





SceneMain.js SceneGameOver.js.





SceneMain.js:





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



SceneGameOver.js:





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



, , :





.

, SceneMain preload. constructor create. :





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



. , preload :





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



imageKey. . - , . , . , preload :





preload() {
  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
  });
}
      
      



, image, spritesheet. , , . Spritesheet - , . spritesheet .





. , . preload :





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



. .





, , . create() SceneMain :





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



- , . (, ), explosions. :





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



, :





this.scene.sfx.laser.play();
      
      



Game Over. SceneMainMenu.js (preload()) SceneMainMenu. , :





, . , . Chrome Firefox, F12, . (Console), ( .) , !





.

, js Entities.js. . , , . ., . Entities.js index.html SceneMainMenu.js. Entity.





class Entity {
	constructor(scene, x, y, key, type) {}
}
      
      



, . , , , , . , Phaser.Scene . Entity:





class Entity extends Phaser.GameObjects.Sprite
      
      



, super . , , , , , . , Phaser.GameObjects.Sprite .





(scene, x, y, key type, , .) super , :





super(scene, x, y, key);
      
      



super :





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



, . . , . . .





, . Entity Player , Entity. Player : scene, x, y key. super , :





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



, . / , . super :





this.setData("speed", 200);
      
      



:





this.play("sprPlayer");
      
      



, .





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



x y.





update(). update() moveRight. :





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



! . , . , . create . :





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

      
      



. SceneMain. . , , . , SceneMain . this.player , . :





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();
}
      
      



, this.player.update() , , , . create() SceneMain :





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



, W, S, A, D. ( .)





.

Entities.js . Entities.js , ChaserShip, GunShip CarrierShip:





class ChaserShip extends Entity {
  constructor(scene, x, y) {
    super(scene, x, y, "sprEnemy1", "ChaserShip");
  }
}

class GunShip extends Entity {
  constructor(scene, x, y) {
    super(scene, x, y, "sprEnemy0", "GunShip");
    this.play("sprEnemy0");
  }
}

class CarrierShip extends Entity {
  constructor(scene, x, y) {
    super(scene, x, y, "sprEnemy2", "CarrierShip");
    this.play("sprEnemy2");
  }
}
      
      



ChaserShip, GunShip CarrierShip Entity, . . super :





this.body.velocity.y = Phaser.Math.Between(50, 100);
      
      



50 100. , .





SceneMain.js. , , , , , . create this.keySpace :





this.enemies = this.add.group();
this.enemyLasers = this.add.group();
this.playerLasers = this.add.group();
      
      



, - , . . -, EnemyLaser . Entities.js. Entity.





class EnemyLaser extends Entity {
  constructor(scene, x, y) {
    super(scene, x, y, "sprLaserEnemy0");
    this.body.velocity.y = 200;
  }
}
      
      



, . , , .





this.shootTimer = this.scene.time.addEvent({
  delay: 1000,
  callback: function() {
    var laser = new EnemyLaser(
      this.scene,
      this.x,
      this.y
    );
    laser.setScale(this.scaleX);
    this.scene.enemyLasers.add(laser);
  },
  callbackScope: this,
  loop: true
});
      
      



, this.shootTimer. GunShip onDestroy. onDestroy - , Phaser, . , , . onDestroy GunShip :





if (this.shootTimer !== undefined) {
  if (this.shootTimer) {
    this.shootTimer.remove(false);
  }
}
      
      



:





, , . . , , , . SceneMain.js .





delay: 1000,
      
      



Entities.js, ChaserShip:





this.states = {
  MOVE_DOWN: "MOVE_DOWN",
  CHASE: "CHASE"
};
this.state = this.states.MOVE_DOWN;
      
      



: , -, MOVE_DOWN.





ChaserShip. - , -. , . Entities.js, ChaserShip :





if (!this.getData("isDead") && this.scene.player) {
  if (Phaser.Math.Distance.Between(
    this.x,
    this.y,
    this.scene.player.x,
  	this.scene.player.y
  ) < 320) {
    this.state = this.states.CHASE;
  }

  if (this.state == this.states.CHASE) {
    var dx = this.scene.player.x - this.x;
    var dy = this.scene.player.y - this.y;

    var angle = Math.atan2(dy, dx);

    var speed = 100;
    this.body.setVelocity(
      Math.cos(angle) * speed,
      Math.sin(angle) * speed
    );
  }
}
      
      



- . , 320 , . , - , ( ) :





if (this.x < this.scene.player.x) {
  this.angle -= 5;
} else {
  this.angle += 5;
} 
      
      



-, SceneMain.js getEnemiesByType. :





getEnemiesByType(type) {
  var arr = [];
  for (var i = 0; i < this.enemies.getChildren().length; i++) {
    var enemy = this.enemies.getChildren()[i];
    if (enemy.getData("type") == type) {
      arr.push(enemy);
    }
  }
  return arr;
}
      
      



. , , .





getEnemiesByType, spawner. :





:





var enemy = null;

if (Phaser.Math.Between(0, 10) >= 3) {
  enemy = new GunShip(
  	this,
  	Phaser.Math.Between(0, this.game.config.width),
		0
	);
} else if (Phaser.Math.Between(0, 10) >= 5) {
  if (this.getEnemiesByType("ChaserShip").length < 5) {
    enemy = new ChaserShip(
    	this,
    	Phaser.Math.Between(0, this.game.config.width),
			0
		);
	}
} else {
  enemy = new CarrierShip(
  	this,
  	Phaser.Math.Between(0, this.game.config.width),
		0
	);
}

if (enemy !== null) {
  enemy.setScale(Phaser.Math.Between(10, 20) * 0.1);
	this.enemies.add(enemy);
}
      
      



, , : GunShip, ChaserShip CarrierShip, . enemy, enemies. CarrierShip , , ChaserShip, . , . Entity, , , Phaser.GameObjects.Sprite , Phaser.GameObjects.Sprite.





this.enemies. :





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



, , , .





.

Player :





this.setData("isShooting"false);
this.setData("timerShootDelay"10);
this.setData("timerShootTick"this.getData("timerShootDelay") - 1);
      
      



, “ ”. . , , . “ ”:





if (this.getData("isShooting")) {
  if (this.getData("timerShootTick") < this.getData("timerShootDelay")) {
    //     timerShootTick  ,      timerShootDelay
    this.setData("timerShootTick"this.getData("timerShootTick") + 1);
  } else { //  " " :
    var laser = new PlayerLaser(this.scene, this.x, this.y);
    this.scene.playerLasers.add(laser);

    this.scene.sfx.laser.play(); //    
    this.setData("timerShootTick"0);
  }
}
      
      



, , Entities.js . Player EnemyLaser. , , , , . PlayerLaser , EnemyLaser. , . , , . :





class PlayerLaser extends Entity {
    constructor(scene, x, y) {
        super(scene, x, y, "sprLaserPlayer");
        this.body.velocity.y = -200;
    }
}
      
      



, , , - SceneMain.js :





if (this.keySpace.isDown) {
	this.player.setData("isShooting"true);
} else {
	this.player.setData("timerShootTick"this.player.getData("timerShootDelay") - 1);
	this.player.setData("isShooting"false);
}
      
      



, !





.

, , . , , . , , :





, . for, :





for (var i = 0; i < this.enemies.getChildren().length; i++) {
	var enemy = this.enemies.getChildren()[i];

	enemy.update();
}
      
      



enemy.update(), :





if (enemy.x < -enemy.displayWidth ||
	enemy.x > this.game.config.width + enemy.displayWidth ||
	enemy.y < -enemy.displayHeight * 4 ||
	enemy.y > this.game.config.height + enemy.displayHeight) {
    if (enemy) {
      if (enemy.onDestroy !== undefined) {
      	enemy.onDestroy();
      }

      enemy.destroy();
    }
}
      
      



:





        for (var i = 0; i < this.enemyLasers.getChildren().length; i++) {
            var laser = this.enemyLasers.getChildren()[i];
            laser.update();
        
            if (laser.x < -laser.displayWidth ||
                laser.x > this.game.config.width + laser.displayWidth ||
                laser.y < -laser.displayHeight * 4 ||
                laser.y > this.game.config.height + laser.displayHeight) {
                if (laser) {
                laser.destroy();
                }
            }
        }
    
        for (var i = 0; i < this.playerLasers.getChildren().length; i++) {
            var laser = this.playerLasers.getChildren()[i];
            laser.update();
        
            if (laser.x < -laser.displayWidth ||
                laser.x > this.game.config.width + laser.displayWidth ||
                laser.y < -laser.displayHeight * 4 ||
                laser.y > this.game.config.height + laser.displayHeight) {
                if (laser) {
                laser.destroy();
                }
            }
        }
      
      



.

, SceneMain.js create. , , . . , , , , . . :





this.physics.add.collider(this.playerLasers, this.enemies, function(playerLaser, enemy{

});
      
      



, , :





if (enemy) {
  if (enemy.onDestroy !== undefined) {
  	enemy.onDestroy();
  }

  enemy.explode(true);
  playerLaser.destroy();
}
      
      



, , explode - . , , Entities.js Entity. Entity explode. canDestroy . canDestroy , explode . explode :





explode(canDestroy) {
  if (!this.getData("isDead")) {
    //     
    this.setTexture("sprExplosion");  //       ,     this.anims.create     
    this.play("sprExplosion"); //  
    //         this.sfx  SceneMain
    this.scene.sfx.explosions[Phaser.Math.Between(0this.scene.sfx.explosions.length - 1)].play();
    if (this.shootTimer !== undefined) {
      if (this.shootTimer) {
      	this.shootTimer.remove(false);
      }
    }
    this.setAngle(0);
    this.body.setVelocity(00);
    this.on('animationcomplete'function({
      if (canDestroy) {
      	this.destroy();
      } else {
      	this.setVisible(false);
      }
    }, this);
    this.setData("isDead"true);
  }
}
      
      



, , , . , SceneMain.js :





if (!this.player.getData("isDead")) {
  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();
  }

  if (this.keySpace.isDown) {
  	this.player.setData("isShooting"true);
  }
  else {
  	this.player.setData("timerShootTick"this.player.getData("timerShootDelay") - 1);
  	this.player.setData("isShooting"false);
  }
}
      
      



. .





.

, , , . , , . , .





. . -, Entities.js. , . .





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



, , . , . . :





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



createLayers. , .





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



createLayers :





for (var i = 0; i < 2; i++) {
  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);
}
      
      



, . for. .





, , i.





createLayers .





this.createLayers();
      
      



SceneMain.js . this.player this.sfx.





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



, . Entities.js :





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



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



! , , .





, GameOver. SceneMainMenu , SceneMain. , SceneMainMenu. create:





this.sfx = {
	btnOverthis.sound.add("sndBtnOver"),
	btnDownthis.sound.add("sndBtnDown")
};
      
      



, .





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



SceneMain, . , this.btnPlay:





this.btnPlay.setInteractive();
      
      



, , over, out, down up. , . , , - pointerover. sprBtnPlayHover.png, . , :





this.btnPlay.on("pointerover"function({
	this.btnPlay.setTexture("sprBtnPlayHover"); //    
	this.sfx.btnOver.play(); //      
}, this);
      
      



pointerout. . :





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



, , , .





pointerdown. sprBtnPlayDown.png.





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



pointerup .





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



pointerup, . pointerup :





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



, !





, , . - . , . pointerup:





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



, . , title:





this.title.setOrigin(0.5);
      
      



This concludes the article. This is my first article on Habré and I tried to convey the meaning of the original as accurately as possible. If you notice inaccuracies or errors somewhere, write in the comments and we will discuss it.





Link to the original article





Link to the original source








All Articles