Pobtastic / Phaser Guide / Draw Countdown

Created Sun, 16 Feb 2025 21:58:09 +0100 Modified Thu, 20 Feb 2025 15:27:57 +0000

Introduction

Now it’s starting to feel like a game! With the draw countdown, the bandits drawing their weapons, and the “BANG!” animation - this is really coming together!

I’ve moved a few things around, as I’ve made a technical decision to use: this.scene.start('GameOverScene') to completely come out of the game (in MainScene) once the player has been shot. This seemed like a good decision, else I’d need to cancel the timers, etc - and now, I can just rely on the scene change to run housekeeping for me.

So … Now, the asset preloading and animation creation happens in a “Pre-Game” BootScene:

class BootScene extends Phaser.Scene {
  constructor() {
    super('BootScene');
  }
  preload() {
    // Preload the bandit assets.
    for (let i = 1; i <= 3; i++) {
      // There are four frames for each bandit.
      for (let f = 1; f <= 4; f++) {
        this.load.image(`bandit-${i}-${f}`, `/images/westbank/bandit-${i}-${f}.png`);
      }
    }
    // Preload the font assets.
    for (let i = 0; i <= 9; i++) {
      this.load.image(`char-${i}`, `/images/westbank/char-${i}.png`);
    }
    // Preload the score/ lives assets.
    const uiElements = ['score-text', 'lives-text', 'lives'];
    uiElements.forEach(element => {
      this.load.image(element, `/images/westbank/${element}.png`);
    });
    // Preload the bang assets.
    for (let i = 1; i <= 6; i++) {
      this.load.image(`bang-${i}`, `/images/westbank/bang-${i}.png`);
    }
  }
  create() {
    // Create the bandit animations.
    for (let i = 1; i <= 3; i++) {
      const frameNames = Array.from({length: 4}, (_, f) => `bandit-${i}-${f+1}`);
      this.anims.create({
        key: `bandit-${i}-anim`,
        frames: frameNames.map(frameName => ({ key: frameName })),
        frameRate: 2,
        repeat: 0
      });
    }

    // Create the bang animation.
    this.anims.create({
      key: 'bang-anim',
      frames: Array.from({ length: 6 }, (_, i) => ({ key: `bang-${i + 1}` })),
      frameRate: 8,
      repeat: 0
    });
    // Start the game.
    this.scene.start('MainScene');
  }
}
If this took any longer, I’d perhaps use this Scene to display a loading bar?

It’s pretty fast though, so I don’t feel like it needs it for this short game.

The MainScene now obviously doesn’t need those parts any more, and of course, all of the methods I had before are still there, e.g. printAt(), decreaseCountdown(), etc.

The only big differences are:

class MainScene extends Phaser.Scene {
...
..
.
  create() {
    // Clear down the bandits.
    this.bandits = [];
    // Create and place the bandit sprites.
    for (let i = 1; i <= 3; i++) {
      const bandit = this.add.sprite(
        8 * ((i * 9) - 2) * this.gameScale,
        8 * 12 * this.gameScale,
        `bandit-${i}-1`
      );
      this.bandits.push(bandit);
    }
    // Cycle through each bandit and set him up.
    this.bandits.forEach((bandit, index) => {
      bandit.frameNames = Array.from({length: 4}, (_, f) => `bandit-${index + 1}-${f+1}`);
      // Property for how long before this bandit will draw their weapon.
      bandit.drawTimer = this.time.delayedCall(
        // 5 seconds + 0 - 3 second delay.
        Phaser.Math.Between(0, 30) * 100 + 5000,
        () => this.banditDraw(index),
        [],
        this
      );
      // Stub property for holding a timer for how long before the bandit fires.
      bandit.fireTimer = false;
      // Helper property to note that the bandit has drawn their weapon.
      bandit.hasDrawn = false;
    });
  }
  banditDraw(index) {
    const bandit = this.bandits[index];
    // Clear the draw timer reference.
    bandit.drawTimer = null;
    // Set the bandit draw sprite.
    bandit.setTexture(`bandit-${index + 1}-2`);
    // Set that this bandit has now drawn.
    bandit.hasDrawn = true;
    // Initiate the "time-to-fire" countdown.
    bandit.fireTimer = this.time.delayedCall(
      // Allow 2 seconds for the player to react.
      2000,
      () => this.gameOver(),
      [],
      this
    );
  }
  gameOver() {
    this.scene.start('GameOverScene');
  }
}
There are a few things to go through here.

  1. Equally, I could have just started each bandit.fireTimer on completion of the game countdown timer. I just considered the game countdown as scene decoration and just adding 5 seconds to the bandit.fireTimer so we know that it’ll begin after the game countdown has finished just seemed neat enough.
  2. I’ve created bandit.hasDrawn here, this will be used in-game for a check to see if you fire too early (which you’re penalised for), just it’s unused here.
  3. After the bandit has drawn their weapon, there’s a small delay before they fire - I’m going to implement a “level selection” I think and have this configurable.

And then finally, I’ve added in the “BANG!” animation which also appears in the game:

class GameOverScene extends Phaser.Scene {
  constructor() {
    super('GameOverScene');
  }
  create() {
    // Place the bang animation.
    const bang = this.add.sprite(
      config.width / 2,
      config.height / 2,
      'bang-1'
    );
    // And then play it.
    bang.play('bang-anim');

    // Return to main scene after animation.
    bang.on('animationcomplete', () => {
      // Introduce a slight delay for cosmetic reasons.
      this.time.delayedCall(200, () => {
        this.scene.start('MainScene');
      });
    });
  }
}

There’s no keyboard/ mouse interaction - this just demos the whole loop cycling through over-and-over:

West Bank Disassembly