<footer><div id=version></div></footer>
html, body {
  height: 100%;
}

body {
  margin: 0;
  padding: 0;
  background: #111;
  color: #eee;
  font: caption;
}

#version {
  position: absolute;
  left: 0;
  top: 0;
  padding: 0;
  background: rgba(0, 0, 0, 0.5)
}
/* global colors, Phaser */

let bulletPlasma;
let bullets;
let enemy;
let enemyBullets;
let enemyFiring;
let enemyMoving;
let player;
let stars;
let text;

const config = {
  width: 512,
  height: 512,
  pixelArt: true,
  scene: {
    preload: preload,
    create: create,
    update: update
  },
  physics: {
    default: "arcade",
    arcade: {
      debug: false
    }
  },
  loader: {
    baseURL: "https://labs.phaser.io",
    crossOrigin: "anonymous"
  }
};

function preload() {
  this.load.image("bullet", "assets/sprites/bullets/bullet7.png");
  this.load.image("enemyBullet", "assets/sprites/bullets/bullet6.png");
  this.load.image("ship", "assets/sprites/bsquadron1.png");
  this.load.image("starfield", "assets/skies/starfield.png");
  this.load.spritesheet("enemy", "assets/sprites/bsquadron-enemies.png", {
    frameWidth: 192,
    frameHeight: 160
  });
}

function create() {
  stars = this.add.blitter(0, 0, "starfield");
  stars.create(0, 0);
  stars.create(0, -512);

  bullets = this.physics.add.group({
    name: "bullets",
    collideWorldBounds: true,
    enable: false
  });
  bullets.createMultiple({
    key: "bullet",
    quantity: 5,
    active: false,
    visible: false
  });

  enemyBullets = this.physics.add.group({
    name: "enemyBullets",
    collideWorldBounds: true,
    enable: false
  });
  enemyBullets.createMultiple({
    quantity: 5,
    key: "enemyBullet",
    active: false,
    visible: false
  });

  enemy = this.physics.add.sprite(256, 128, "enemy", 1);
  enemy.body.setSize(160, 64);
  enemy.state = 10;

  enemyMoving = this.tweens.add({
    targets: enemy.body.velocity,
    props: {
      x: { from: 150, to: -150, duration: 4000 },
      y: { from: 50, to: -50, duration: 2000 }
    },
    ease: "Sine.easeInOut",
    yoyo: true,
    repeat: -1
  });

  enemyFiring = this.time.addEvent({
    delay: 750,
    loop: true,
    callback: fireBulletFromEnemy
  });

  player = this.physics.add.image(256, 448, "ship");

  bulletPlasma = this.add.particles("bullet").createEmitter({
    alpha: { start: 1, end: 0, ease: "Cubic.easeIn" },
    blendMode: 3,
    frequency: -1,
    lifespan: 500,
    radial: false,
    scale: { start: 1, end: 5, ease: "Cubic.easeOut" }
  });

  text = this.add.text(0, 32, "", { font: "16px monospace", fill: colors.cssColors.lime });

  this.physics.add.overlap(enemy, bullets, overlapEnemyWithBullet);
  this.physics.add.overlap(player, enemyBullets, overlapPlayerWithBullet);

  this.physics.world.on("worldstep", worldStep, this);

  this.input.on("pointermove", movePlayerFromPointer);
  this.input.on("pointerdown", fireBulletFromPlayer);
}

function update() {
  stars.y += 1;
  stars.y %= 512;

  text.setText([poolInfo(bullets), poolInfo(enemyBullets)]);
}

function worldStep() {
  bullets.getChildren().forEach(updateBullet, this);
  enemyBullets.getChildren().forEach(updateBullet, this);
}

function updateBullet(bullet) {
  bullet.state -= bullet.body.newVelocity.length();

  if (bullet.state <= 0) {
    bullet.disableBody(true, true);
  }
}

function movePlayerFromPointer(pointer) {
  player.x = pointer.worldX;
}

function fireBulletFromEnemy() {
  fireBulletFromGroup(enemyBullets, enemy.x, enemy.y + 32, 0, 150);
}

function fireBulletFromPlayer() {
  fireBulletFromGroup(bullets, player.x, player.y, 0, -300);
}

function fireBulletFromGroup(group, x, y, vx, vy) {
  const bullet = group.getFirstDead(false);

  if (bullet) {
    fireBullet(bullet, x, y, vx, vy);
  }
}

function fireBullet(bullet, x, y, vx, vy) {
  bullet.enableBody(true, x, y, true, true);
  bullet.setVelocity(vx, vy);

  // Range (px)
  bullet.state = 300;
}

function overlapEnemyWithBullet(_enemy, bullet) {
  const { x, y } = bullet.body.center;

  _enemy.state -= 1;
  bullet.disableBody(true, true);
  bulletPlasma.setSpeedY(0.2 * bullet.body.velocity.y).emitParticleAt(x, y);

  if (_enemy.state <= 0) {
    _enemy.setFrame(3);
    _enemy.body.checkCollision.none = true;
    enemyFiring.remove();
    enemyMoving.stop();
  }
}

function overlapPlayerWithBullet(_player, bullet) {
  const { x, y } = bullet.body.center;

  bullet.disableBody(true, true);
  bulletPlasma.setSpeedY(0.2 * bullet.body.velocity.y).emitParticleAt(x, y);
}

function poolInfo(group) {
  return `${group.name} ${group.getLength()} (${group.countActive(
    true
  )}:${group.countActive(false)})`;
}

document.getElementById("version").textContent = "Phaser v" + Phaser.VERSION;

new Phaser.Game(config);
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.jsdelivr.net/npm/phaser@3.24.1/dist/phaser.js
  2. https://cdn.jsdelivr.net/npm/@samme/colors@1.2.0/dist/colors.umd.js