Pobtastic / Goldfish Game / Enemies

Created Sun, 21 Apr 2024 09:45:03 +0100 Modified Thu, 12 Dec 2024 14:35:47 +0000

Introduction

This is going to be very similar to the goldfish code, except there are several different “types” of enemies. For ease of referencing them, we’ll set up an array of the available types:

  constructor() {
    super('MainScene');

    // Define each selectable enemy type, with their animation frames.
    this.enemyTypes = [
      { key: 'dolphin', images: ['dolphin-1', 'dolphin-2', 'dolphin-3', 'dolphin-4'] },
      { key: 'marlin', images: ['marlin-1', 'marlin-2', 'marlin-3', 'marlin-4'] },
      { key: 'sea-snake', images: ['sea-snake-1', 'sea-snake-2', 'sea-snake-3', 'sea-snake-4'] },
      { key: 'squid', images: ['squid-1', 'squid-2', 'squid-3', 'squid-4'] }
    ];
  }

This instantly helps with preloading the images; let’s loop through them to declare that each should be preloaded:

  preload() {
    // Loop through every enemy type.
    this.enemyTypes.forEach(type => {

      // Loop through every animation image frame.
      type.images.forEach(image => {
        this.load.image(image, `/images/booty/${image}.png`);
      })
    });
  }

We can use the same trick for creating the animation frames in the create method:

  this.enemyTypes.forEach(type => {

    // Add the width of the enemy to this.enemyTypes, we'll use this in several
    // places and this is a good enough place to set it.
    type.width = this.textures.get(type.images[0]).getSourceImage().width;

    // Create the enemy animation.
    const frames = type.images.map(image => ({ key: image }));
    this.anims.create({
      key: type.key,
      frames: frames,
      frameRate: 10,
      repeat: -1
    });
  });

Identically to the goldfish, we’re going to manage all the enemies in a group. This doesn’t really differ so much from the goldfish group other than that the enemies aren’t identical sizes and obviously frames.

  this.enemyGroup = this.physics.add.group({
    createCallback: (enemy) => {
      this.initialiseEnemy(enemy);
    }
  });

The enemies appear a little more often than the goldfish so the chances of them overlapping are fairly high. It looks a bit rubbish when they do, so let’s ensure it doesn’t ever happen:

  // Set up an event listener for an 'overlap' event.
  // This event is triggered when any physics body overlaps with another
  // physics body.
  this.physics.add.overlap(this.enemyGroup, this.enemyGroup, function (enemy1, enemy2) {

    // Enemies at move at the same speed, so this event will only ever occur on
    // enemy creation. Therefore, just destroy the enemy which is off-screen.
    if (enemy1.inCamera) {
      enemy1.destroy();
    }
    else {
      enemy2.destroy();
    }
  });

  // Set up an event listener for the 'worldbounds' event.
  // This event is triggered when any physics body hits the boundaries of the physics world.
  this.physics.world.on('worldbounds', (body) => {

    // Check if the enemy is moving out of the right-hand boundary.
    // The 'blocked.right' property indicates the body has hit the right edge of the world bounds.
    if (body.blocked.right && this.enemyTypes.some(type => type.key === body.gameObject.anims.currentAnim.key)) {

      // Destroy the enemy if it hits the right boundary to prevent it from moving right forever.
      // This will free it up, so it can be recreated.
      body.gameObject.destroy();
    }
  });

This is more or less identical to the goldfish createCallback() function:

  initialiseEnemy(enemy) {
    enemy.setOrigin(0, 0);
    enemy.setX(-enemy.width);
    enemy.setCollideWorldBounds(true);
    enemy.setActive(false);
    enemy.body.onWorldBounds = true;
    enemy.body.setBoundsRectangle(
      new Phaser.Geom.Rectangle(
        -enemy.width,
        8 * 8 * this.gameScale,
        config.width + enemy.width * 2,
        8 * 12 * this.gameScale
      )
    );
  }

And, right at the bottom of the create() method, same as for the goldfish, we kick off the first spawn event.

  this.scheduleNextSpawnEnemy();

Which looks like this!

  // Spawns the enemy, and sets up the next spawning event.
  scheduleNextSpawnEnemy() {
    this.time.addEvent({
      delay: Phaser.Math.Between(1, 10) * 500,
      callback: () => {
        this.spawnEnemy(),
        this.scheduleNextSpawnEnemy()
      },
      callbackScope: this
    });
  }

  // Spawns an enemy.
  spawnEnemy() {

    // Ensure there's only ever a maximum of 10 enemies on-screen at any point.
    if (this.enemyGroup.getTotalUsed() < 10) {

      // Pick a random enemy.
      let chooseEnemy = Phaser.Utils.Array.GetRandom(this.enemyTypes);

       // Find the first inactive enemy, and set it to appear off-screen at a
      // random character block height on the left.
      let enemy = this.enemyGroup.getFirstDead(
        true,
        -chooseEnemy.width,
        Phaser.Math.Between(8, 18) * 8 * this.gameScale,
        chooseEnemy.images[0]
      );

      // If creation was successful, then make it active, set the speed and
      // play the animation.
      if (enemy) {
        this.initialiseEnemy(enemy);
        enemy.setActive(true);
        enemy.setVelocityX(100);
        enemy.play(chooseEnemy.key);
      }
    }
  }

Booty Disassembly