Game Development 22 min read

Build a Classic Plane‑Shooter Game with Phaser, Vue3 and TypeScript

This step‑by‑step tutorial shows how to create a browser‑based plane‑shooting game by setting up a Vue3 project, installing Phaser, organizing assets, implementing collision detection, designing preloader, main, and end scenes, encapsulating player, bullet, enemy, and explosion logic in reusable classes, and optimizing collision bounds for a polished HTML5 gaming experience.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Build a Classic Plane‑Shooter Game with Phaser, Vue3 and TypeScript

Project Setup

Create a Vue 3 project named plane-war and install the required dependencies:

npm create vue
cd plane-war
npm install
npm install phaser
npm run dev

The development server will serve the game at http://localhost:5173 (or the port shown in the console).

Asset Organization

Place all game assets under src/assets following this structure:

plane-war
├── src
│   ├── assets
│   │   ├── audio
│   │   │   ├── bgm.mp3
│   │   │   ├── boom.mp3
│   │   │   └── bullet.mp3
│   │   ├── images
│   │   │   ├── background.jpg
│   │   │   ├── boom.png
│   │   │   ├── bullet.png
│   │   │   ├── enemy.png
│   │   │   ├── player.png
│   │   │   └── sprites.png
│   │   └── json
│   │       └── sprites.json
│   ├── App.vue
│   └── main.ts

Game Scenes

Preloader : loads all assets, creates the start screen and defines the animation for the explosion.

Main : implements the scrolling background, player ship, bullet pool, enemy pool, explosion pool, score handling and game‑over logic.

End : shows a game‑over panel with the final score and a restart button.

Preloader Scene

import { Scene } from "phaser";
import backgroundImg from "../assets/images/background.jpg";
import enemyImg from "../assets/images/enemy.png";
import playerImg from "../assets/images/player.png";
import bulletImg from "../assets/images/bullet.png";
import boomImg from "../assets/images/boom.png";
import bgmAudio from "../assets/audio/bgm.mp3";
import boomAudio from "../assets/audio/boom.mp3";
import bulletAudio from "../assets/audio/bullet.mp3";

export class Preloader extends Scene {
  constructor() { super("Preloader"); }
  preload() {
    this.load.image("background", backgroundImg);
    this.load.image("enemy", enemyImg);
    this.load.image("player", playerImg);
    this.load.image("bullet", bulletImg);
    this.load.spritesheet("boom", boomImg, { frameWidth: 64, frameHeight: 48 });
    this.load.audio("bgm", bgmAudio);
    this.load.audio("boom", boomAudio);
    this.load.audio("bullet", bulletAudio);
  }
  create() {}
}

Main Scene

The main scene creates the game world and registers the core loop.

import { Scene, GameObjects, Physics } from "phaser";
import { Player } from "./Player";
import { Bullet } from "./Bullet";
import { Enemy } from "./Enemy";
import { Boom } from "./Boom";

let background, player, enemys, bullets, booms, scoreText;
let score = 0;

export class Main extends Scene {
  constructor() { super("Main"); }
  create() {
    const { width, height } = this.cameras.main;
    background = this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);
    player = new Player(this);
    enemys = this.physics.add.group({ frameQuantity: 30, key: "enemy", enable: false, active: false, visible: false, classType: Enemy });
    bullets = this.physics.add.group({ frameQuantity: 15, key: "bullet", enable: false, active: false, visible: false, classType: Bullet });
    booms = this.add.group({ frameQuantity: 30, key: "boom", active: false, visible: false, classType: Boom });
    scoreText = this.add.text(10, 10, "0", { fontFamily: "Arial", fontSize: 20 });
    this.addEvent();
  }
  update() { background.tilePositionY -= 1; }
  addEvent() {
    this.time.addEvent({
      delay: 400,
      callback: () => {
        for (let i = 0; i < 2; i++) enemys.getFirstDead()?.born();
        bullets.getFirstDead()?.fire(player.x, player.y - 32);
      },
      repeat: -1,
    });
    this.physics.add.overlap(bullets, enemys, this.hit, null, this);
    this.physics.add.overlap(player, enemys, this.gameOver, null, this);
  }
  hit(bullet, enemy) {
    enemy.disableBody(true, true);
    bullet.disableBody(true, true);
    booms.getFirstDead()?.show(enemy.x, enemy.y);
    scoreText.text = String(++score);
  }
  gameOver() {
    this.sys.pause();
    this.registry.set("score", score);
    this.scene.start("End");
  }
}

End Scene

import { Scene } from "phaser";

export class End extends Scene {
  constructor() { super("End"); }
  create() {
    const { width, height } = this.cameras.main;
    this.add.image(width / 2, height / 2, "sprites", "result").setScale(2.5);
    this.add.text(width / 2, height / 2 - 85, "Game Over", { fontFamily: "Arial", fontSize: 24 }).setOrigin(0.5);
    const score = this.registry.get("score");
    this.add.text(width / 2, height / 2 - 10, `Score: ${score}`, { fontFamily: "Arial", fontSize: 20 }).setOrigin(0.5);
    const button = this.add.image(width / 2, height / 2 + 50, "sprites", "button").setScale(3, 2).setInteractive();
    button.on("pointerdown", () => { this.scene.start("Main"); });
    this.add.text(button.x, button.y, "Restart", { fontFamily: "Arial", fontSize: 20 }).setOrigin(0.5);
  }
}

Game Object Classes

Player

import { Physics, Scene } from "phaser";
export class Player extends Physics.Arcade.Sprite {
  isDown = false;
  downX = 0;
  downY = 0;
  constructor(scene) {
    const { width, height } = scene.cameras.main;
    super(scene, width / 2, height - 80, "player");
    scene.add.existing(this);
    scene.physics.add.existing(this);
    this.setInteractive();
    this.setScale(0.5);
    this.setCollideWorldBounds(true);
    this.body.setSize(120, 120); // smaller hit box
    this.addEvent();
  }
  addEvent() {
    this.on("pointerdown", () => { this.isDown = true; this.downX = this.x; this.downY = this.y; });
    this.scene.input.on("pointerup", () => { this.isDown = false; });
    this.scene.input.on("pointermove", (pointer) => {
      if (this.isDown) {
        this.x = this.downX + pointer.x - pointer.downX;
        this.y = this.downY + pointer.y - pointer.downY;
      }
    });
  }
}

Bullet

import { Physics, Scene } from "phaser";
export class Bullet extends Physics.Arcade.Sprite {
  constructor(scene, x, y, texture) {
    super(scene, x, y, texture);
    scene.add.existing(this);
    scene.physics.add.existing(this);
    this.setScale(0.25);
  }
  fire(x, y) {
    this.enableBody(true, x, y, true, true);
    this.setVelocityY(-300);
    this.scene.sound.play("bullet");
  }
  preUpdate(time, delta) {
    super.preUpdate(time, delta);
    if (this.y <= -14) this.disableBody(true, true);
  }
}

Enemy

import { Physics, Math, Scene } from "phaser";
export class Enemy extends Physics.Arcade.Sprite {
  constructor(scene, x, y, texture) {
    super(scene, x, y, texture);
    scene.add.existing(this);
    scene.physics.add.existing(this);
    this.setScale(0.5);
    this.body.setSize(100, 60); // smaller hit box
  }
  born() {
    const x = Math.Between(30, 345);
    const y = Math.Between(-20, -40);
    this.enableBody(true, x, y, true, true);
    this.setVelocityY(Math.Between(150, 300));
  }
  preUpdate(time, delta) {
    super.preUpdate(time, delta);
    const { height } = this.scene.cameras.main;
    if (this.y >= height + 20) this.disableBody(true, true);
  }
}

Explosion (Boom)

import { GameObjects, Scene } from "phaser";
export class Boom extends GameObjects.Sprite {
  constructor(scene, x, y, texture) {
    super(scene, x, y, texture);
    this.on("animationcomplete-boom", this.hide, this);
  }
  show(x, y) {
    this.x = x; this.y = y;
    this.setActive(true);
    this.setVisible(true);
    this.play("boom");
    this.scene.sound.play("boom");
  }
  hide() {
    this.setActive(false);
    this.setVisible(false);
  }
}

Collision Optimization

Phaser uses the full sprite size for collision detection by default. To avoid premature hits, the hit boxes of the player and enemy sprites are reduced with this.body.setSize(width, height) in their constructors (120 × 120 for the player, 100 × 60 for the enemy).

Debugging

Enable physics debugging to visualise bodies:

new Game({
  physics: { default: "arcade", arcade: { debug: true } },
  // other config …
});

Demo & Repository

Live demo (run in a browser): https://yuhuo.online/plane-war/

Source code (Git repository): https://gitee.com/yuhuo520/plane-war

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptGame DevelopmentHTML5Vue3Phaser
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.