top of page
Search
cedarcantab

Space Invaders in Phaser 3 (Part 11): Alien Bomb - the Rolling Shot

In this post I document the challenges in my attempt to replicate the alien shots. Having reviewed the original code in more detail, I ended up re-writing the code. Hence the CODEPEN at the bottom of this post is quite different from the one posted in Part 0 of this series.


General understanding of the alien shots

There are three different alien bomb types: "rolling", "plunger" and "squiggly", referred to by Computer Archeology ("CA") as game objects 2, 3 and 4. Not only do they have different images (4 frame of animation each), they behave differently.


Rolling Alien Shot: Always dropped from the alien nearest to the player.





Plunger Alien Shot: Dropped from a predefined alien columns list and is not used when there is only one alien left.





Squiggly Alien Shot: Dropped from a predefined alien columns list and is not used when the flying saucer is being shown.





Given the different behaviour, for this post I will focus on just the first of the three - the Rolling shot, particularly since it's the most heavily commented by Computer Archeology ("CA").


Game Object Descriptor

Let's start by looking at the game object descriptor



CA adds the following explanation:

  1. The 2-byte value at 2038 is where the firing-column-table-pointer would be (see other shots ... next game objects). This shot doesn't use that table. It targets the player specifically. Instead the value is used as a flag to have the shot skip its first attempt at firing every time it is reinitialized (when it blows up).

  2. The task-timer at 2032 is copied to 2080 in the game loop. The flag is used as a synchronization flag to keep all the shots processed on separate interrupt ticks. This has the main effect of slowing the shots down.

    • When the timer is 2 the squiggly-shot/saucer (object 4 ) runs.

    • When the timer is 1 the plunger-shot (object 3) runs.

    • When the timer is 0 this object, the rolling-shot, runs.

The 2nd point in particular is saying that the in updating the 3 alien shot objects, only 1 is processed in each frame. In otherwords, each shot type is processed every 3 frames. What this means is that, even though the delta Y is 4 (in normal circumstances), the alien shot is moved 4 pixels only every 3 frames. In Phaser I have implemented in terms of the Phaser object body's velocity - hence the speed is 4 * (60/3) = 80 pixels per second.


CA also gives the following overall description of the handler routine common to all 3 shots.


The common handler (in addition to moving each shot) also initiates the alien shots. Each shot has a move-counter that starts when the shot is dropped and counts up with each step as it falls. The game keeps a constant reload rate that determines how fast the aliens can fire. The game takes the smallest count of the other two shots and compares it to the reload rate. If it is too soon since the last shot then no shot is fired.

The reload rate gets faster as the game progresses. The code uses the upper two digits of the player's score as a lookup into the table at 1AA1 and sets the reload rate to the value in the table at 1CB8. The reload rate for anything below 0x0200 is 0x30. From 0x0200 to 0x1000 the delay drops to 0x10, then 0x0B at from 0x1000 to 0x2000. From 0x2000 to 0x3000 the rate is 8 and then maxes out above 0x3000 at 7. With a little flying-saucer-luck you will reach 0x3000 in 2 or 3 racks.

The above provides a lot of information. Now let's look at the task handler specific to the rolling shot, which is situated $0476, before we go onto the common handler.


The task handler

The "task timer at $2032" (referred as obj2TimerExtra) is first reset to initial value of 2.


I don't fully understand the code in RED, but I assume this is what's described by CA above, to make it skip the first attempt.


The Blue code gets the shot step count for all 3 alien shot types.


Then HandleAlienShot - the guts of the alien shot handling routine - gets called.


0476: E1              POP     HL                  ; Game object data
0477: 3A 32 1B        LD      A,($1B32)           ; Restore delay from ...
047A: 32 32 20        LD      (obj2TimerExtra),A  ; ... ROM mirror (value 2)
047D: 2A 38 20        LD      HL,(rolShotCFirLSB) ; Get pointer to ...
0480: 7D              LD      A,L                 ; ... column-firing table.
0481: B4              OR      H                   ; All zeros?
0482: C2 8A 04        JP      NZ,$048A            ; No ... must be a valid column. Go fire.
0485: 2B              DEC     HL                  ; Decrement the counter
0486: 22 38 20        LD      (rolShotCFirLSB),HL ; Store new counter value (run the shot next time)
0489: C9              RET                         ; And out

048A: 11 35 20        LD      DE,$2035            ; Rolling-shot data structure
048D: 3E F9           LD      A,$F9               ; Last picture of "rolling" alien shot
048F: CD 50 05        CALL    ToShotStruct        ; Set code to handle rolling-shot
0492: 3A 46 20        LD      A,(pluShotStepCnt)  ; Get the plunger-shot step count
0495: 32 70 20        LD      (otherShot1),A      ; Hold it
0498: 3A 56 20        LD      A,(squShotStepCnt)  ; Get the squiggly-shot step count
049B: 32 71 20        LD      (otherShot2),A      ; Hold it
049E: CD 63 05        CALL    HandleAlienShot     ; Handle active shot structure
04A1: 3A 78 20        LD      A,(aShotBlowCnt)    ; Blow up counter
04A4: A7              AND     A                   ; Test if shot has cycled through blowing up
04A5: 21 35 20        LD      HL,$2035            ; Rolling-shot data structure
04A8: C2 5B 05        JP      NZ,FromShotStruct   ; If shot is still running, copy the updated data and out

HandleAlienShot

The commentary by CA of this code is reproduced below.


HandleAlienShot:
; Each of the 3 shots copy their data to the 2073 structure (0B bytes) and call this.
; Then they copy back if the shot is still active. Otherwise they copy from the mirror.
;
; The alien "fire rate" is based on the number of steps the other two shots on the screen have made. The smallest number-of-steps is compared to the reload-rate. If it is too soon then no shot is made. The reload-rate is based on the player's score. The MSB is looked up in a table to get the reload-rate. The smaller the rate the faster the aliens fire. Setting rate this way keeps shots from walking on each other.
;

The important bits from the earlier part of the routine is as shown below;


The first check is the status of the shot - is it blowing up or active? If active, move it.

0563: 21 73 20        LD      HL,$2073            ; Start of active shot structure
0566: 7E              LD      A,(HL)              ; Get the shot status
0567: E6 80           AND     $80                 ; Is the shot active?
0569: C2 C1 05        JP      NZ,$05C1            ; Yes ... go move it

The code at $05C1 basically moves the alien shot down the screen, and increments the step count. It also checks the flag set by collision detection, and if this is set, tries to figure out what the alien shot has hit, primarily by the position of the alien shot, by a bunch of including below. $15 = 21, $1E = 30. The 1-pixel line representing the ground is drawn at the 16th row from the bottom, so I do not fully understand the significant of 21, but it is certainly below the player. I definitely do not understand the significance of checking against 30.


05F3: 3A 7B 20        LD      A,(alienShotYr)     ; Shot's Y coordinate
05F6: FE 15           CP      $15                 ; Still in the active playfield?
05F8: DA 12 06        JP      C,$0612             ; No ... end it
05FB: 3A 61 20        LD      A,(collision)       ; Did shot collide ...
05FE: A7              AND     A                   ; ... with something?
05FF: C8              RET     Z                   ; No ... we are done here
0600: 3A 7B 20        LD      A,(alienShotYr)     ; Shot's Y coordinate
0603: FE 1E           CP      $1E                 ; Is it below player's area?
0605: DA 12 06        JP      C,$0612             ; Yes ... end it

Continuing on with the HandleAlienShot, the code highlighted in RED checks the step count of the "other" 2 shots. If zero, it is ignored. If it is not zero, it is compared against aShotReloadRate.


; Make sure it isn't too soon to fire another shot
057C: 3A 70 20        LD      A,(otherShot1)      ; Get the step count of the 1st "other shot"
057F: A7              AND     A                   ; Any steps made?
0580: CA 89 05        JP      Z,$0589             ; No ... ignore this count
0583: 47              LD      B,A                 ; Shuffle off step count
0584: 3A CF 20        LD      A,(aShotReloadRate) ; Get the reload rate (based on MSB of score)
0587: B8              CP      B                   ; Too soon to fire again?
0588: D0              RET     NC                  ; Yes ... don't fire
0589: 3A 71 20        LD      A,(otherShot2)      ; Get the step count of the 2nd "other shot"
058C: A7              AND     A                   ; Any steps made?
058D: CA 96 05        JP      Z,$0596             ; No steps on any shot ... we are clear to fire
0590: 47              LD      B,A                 ; Shuffle off step count
0591: 3A CF 20        LD      A,(aShotReloadRate) ; Get the reload rate (based on MSB of score)
0594: B8              CP      B                   ; Too soon to fire again?
0595: D0              RET     NC                  ; Yes ... don't fire
0596: 23              INC     HL                  ; 2075
0597: 7E              LD      A,(HL)              ; Get tracking flag
0598: A7              AND     A                   ; Does this shot track the player?
0599: CA 1B 06        JP      Z,$061B             ; Yes ... go make a tracking shot;

Then the code to actually handle the rolling shot is called. The centre of the player object is obtained by adding 8 (half of display width) to the player's X coordinate, and loaded into register-A.


Based on the player's X-coordinate (contained in A), FindColumn is called to find the lowest alien in the relevant column, The result is stored in register-C, which is loaded into A and compared against $0C = 12. I think this is checking to see if the column found is outside the range of the rack (since there are only 11 columns). If it is a valid column (ie less than 12) then go back to $05A5 with that column. Otherwise, load 11 (ie the right most column) and go back to $05A5 (which goes onto find a live alien in that column and do all the shooting stuff).


; Start a shot right over the player
061B: 3A 1B 20        LD      A,(playerXr)        ; Player's X coordinate
061E: C6 08           ADD     A,$08               ; Center of player
0620: 67              LD      H,A                 ; To H for routine
0621: CD 6F 15        CALL    FindColumn          ; Find the column
0624: 79              LD      A,C                 ; Get the column right over player
0625: FE 0C           CP      $0C                 ; Is it a valid column?
0627: DA A5 05        JP      C,$05A5             ; Yes ... use what we found
062A: 0E 0B           LD      C,$0B               ; Else use ...
062C: C3 A5 05        JP      $05A5               ; ... as far over as we can

The code for "FindColumn" is as follows. There are bits of this that I don't fully understand, but I think the key is it keeps incrementing from the original Player X-coordinate (of the rotated screen) by 16 (width of each alien) until it finds something.


FindColumn:
; H contains a Xr coordinate. Find the column number within the rack that corresponds to the Xr coordinate. Return the column coordinate in H and the column number in C.
;
156F: 3A 0A 20        LD      A,(refAlienXr)      ; Reference alien Yn coordinate
1572: CD 54 15        CALL    Cnt16s              ; Count 16s to bring Y to target Y
1575: DE 10           SBC     A,$10               ; Subtract off extra 16
1577: 67              LD      H,A                 ; To H
1578: C9              RET                         ; Done

1579: 3E 01           LD      A,$01               ; Mark flying ...
157B: 32 85 20        LD      (saucerHit),A       ; ... saucer has been hit
157E: C3 45 15        JP      $1545               ; Remove player shot

With the alien to make the shot, now in register-HL, the following code is executed.


05A9: CD 7A 01        CALL    GetAlienCoords      ; Get coordinates of alien (lowest alien in firing column)
05AC: 79              LD      A,C                 ; Offset ...
05AD: C6 07           ADD     A,$07               ; ... Y by 7
05AF: 67              LD      H,A                 ; To H
05B0: 7D              LD      A,L                 ; Offset ...
05B1: D6 0A           SUB     $0A                 ; ... X down 10
05B3: 6F              LD      L,A                 ; To L
05B4: 22 7B 20        LD      (alienShotYr),HL    ; Set shot coordinates below alien
;
05B7: 21 73 20        LD      HL,$2073            ; Alien shot status
05BA: 7E              LD      A,(HL)              ; Get the status
05BB: F6 80           OR      $80                 ; Mark this shot ...
05BD: 77              LD      (HL),A              ; ... as actively running
05BE: 23              INC     HL                  ; 2074 step count
05BF: 34              INC     (HL)                ; Give this shot 1 step (it just started)
05C0: C9              RET                         ; Out

Shot Reload Rate

The common handler references something called the shot reload rate. CA gives the following explanation.

ShotReloadRate:
; The tables at 1CB8 and 1AA1 control how fast shots are created. The speed is based on the upper byte of the player's score. For a score of less than or equal 0200 then the fire speed is 30. For a score less than or equal 1000 the shot speed is 10. Less than or equal 2000 the speed is 0B. Less than or equal 3000 is 08. And anything above 3000 is 07.
;
; 1CB8: 02 10 20 30
;
1AA1: 30 10 0B 08                           
1AA5: 07           ; Fastest shot firing speed

Javascript version

In my Javascript version, I have implemented the shot reload rate code by creating an array, as follows:

RELOADRATE_SCORE = [200, 1000, 2000, 3000, Infinity];
RELOADRATE_TIMER = [48, 16, 11 ,8, 7];

And a method within the MainGame scene function to get the reload rate, as follows.

 updateReloadRate() {
    this.reloadRate = RELOADRATE_TIMER[RELOADRATE_SCORE.findIndex((x)=>{return x>=this.ship.score})];
  }

The reloadrate is update within the main game scene every time score is updated (called initially at the beginning of each game from the main game scene, and then from within incrementScore method within Player object), so this.reloadRate is always ready to be referred to.

Javascript version of the Game Objects


Starting with the easy bits...I create separate objects for the 3 types of bombs. However, I have create a Phaser group to house the 3 bullets so that I can carry out the collision detection on all 3 bullets at the same time using Phaser's built-in collision detection function.


    this.invaderBombs = this.add.group();
    this.rolling = new Rolling(this,200,10);
    this.squiggle = new Squiggle(this, 200, 10)
    this.plunger = new Plunger(this, 200, 10)
    this.invaderBombs.add(this.squiggle);
    this.invaderBombs.add(this.plunger);
    this.invaderBombs.add(this.rolling);
  

The 3 separate bomb classes are extended from a base bomb class.


Base Bomb Class

The basic structure is very similar to the bullet class. The code to check for overlap with the shield is very similar but a little bit different. I should really re-factor this with the bullet method.


class Bomb extends Phaser.Physics.Arcade.Sprite {

  constructor(scene, x, y, texture) {
    super(scene, x, y, texture);
    this.scene = scene;
    scene.add.existing(this);
    scene.physics.add.existing(this);
    this.status;
    this.shotStepCount; // this is used to time the dropping of the next bomb
    this.shotFireColumn; // index into the table that holds which column of the rack to shoot from
    this.bombSpeed; 
    this.shieldInSight;
    this.setStatus(Bomb.Status.STANDBY);
  }
  
  static Status = {
    STANDBY: 0,
    DROPPED: 1,
    EXPLODE: 2
  }
  static EXPLOSION_DURATION = 300;
  
  preUpdate(time, delta) {
    super.preUpdate(time,delta);
    switch (this.status) {
      case Bomb.Status.DROPPED:
        if (this.y >= Ground.HEIGHT) {
          this.setStatus(Bomb.Status.EXPLODE);
          this.scene.ground.damage(this.x);
        }
        this.overlapShield();
        break;
    }
  }

  setStatus(newStatus) {
    this.status = newStatus;  
    switch (this.status) {
      case Bomb.Status.STANDBY:
        this.shotStepCount = 0;
        this.disableBody(true,true);;
        break;
      case Bomb.Status.DROPPED:
        this.getShieldID();
        this.bombSpeed = 80;
        this.body.setVelocity(0, this.bombSpeed);
        break;
      case Bomb.Status.EXPLODE:
        this.body.stop();
        this.stop(); // stop the bomb animation to replace by explosion which is a single frame image
        this.setTexture('bombExplode');
        this.scene.time.delayedCall(Bomb.EXPLOSION_DURATION, this.setStatus, [Bomb.Status.STANDBY], this)
        break;
    }
  }
  
  dropBomb(x,y) {
    this.enableBody(true, x, y, true, true);
    this.setStatus(Bomb.Status.DROPPED);
  }
 
  isActive() {
    return (this.status !== Bomb.Status.STANDBY)
  }
  
  getShieldID() {
    this.shieldInSight = -1; // -1 means there is no shield directly below the bomb 
    Shield.SHIELD_POS_X.forEach((x,i) => {
      if ((this.getBottomLeft().x >= x && this.getBottomLeft().x < x + Shield.WIDTH) ||
          (this.getBottomRight().x >= x && this.getBottomRight().x < Shield.WIDTH))
        this.shieldInSight = i;
    });    
  }  
  
  getFireColumn() {
    const colIndex = this.colIndex;
    this.colIndex = (this.colIndex <= this.COLFIRETABLE.length-1) ? this.colIndex+1 : 0; 
    return this.COLFIRETABLE[colIndex]
  }

   overlapShield() {
    // first check whether bomb overlaps the "area" occupied by the shield
    if (this.status === Bomb.Status.DROPPED && this.shieldInSight >-1 && this.y-4 >= Shield.SHIELD_TOP && this.y+4 < Shield.SHIELD_BOTTOM) {
      for (let i=-4; i<4; i++) {
      const checkX = Math.floor(this.x-this.scene.shields[this.shieldInSight].leftX);
      const checkY = Math.floor(this.y+i-Shield.SHIELD_TOP);
        if (this.scene.shields[this.shieldInSight].checkBrick(checkX, checkY)) {
          this.scene.shields[this.shieldInSight].damageShield(checkX, checkY);          
          this.setStatus(Bomb.Status.STANDBY);
          return
        };
      }
    }
  }
} // end of Bullet Class

Extend the base class to create the Rolling Shot game object

The bomb specific bits are: set the type, and the texture. Also set a this.skip flag to replicate the original code skips the shot handle, the first time after the bomb exploded.

class Rolling extends Bomb {
  constructor(scene,x,y) {
    super(scene, x, y, "rolling");
    this.type = "rolling";
    this.skip = true; // for the rolling shot, first attempt to launch the bomb is skipped
  }

  dropBomb(x,y) {
    super.dropBomb(x,y);
    this.skip = true;
    this.play('fireRolling')
  }
}

Now, the most difficult part - the equivalent of the handleAlienShot routine.


Handling the Alien Shots

First I created the following method which is called by the update loop of the main game scene. I have replicated the logic of the original.


  handleBombTask() {
    switch (this.bombTaskTimer) {
      case 0:
        // run the rolling shot task - below
        if (this.rolling.skip) {
          this.rolling.skip = false;
        } else  {
          this.otherShot1 = this.plunger.shotStepCount;
          this.otherShot2 = this.squiggle.shotStepCount;
          this.bombColumn = this.rack.findColumn();
          this.handleAlienShot(this.rolling);
        }
        break;
      case 1:
        // run the plunger shot task - below
        break;
      case 2:
        // run the squiggle shot/saucer task - below
        break;
    }
    this.bombTaskTimer = this.bombTaskTimer <= 1 ? this.bombTaskTimer + 1 : 0;
    this.bombColumn = undefined;
  }

To find the column with live aliens above the player, I have written the following method, within the rack object (one that controls the rack of aliens). I had to create another property of the invader class called column to get this to work. This is different from the original code which has a "reference alien" against which all the other aliens behave. In my code the 55 aliens are basically their own independent game objects.


  findColumn() {
    const potentials = this.getMatching('status', Invader.Status.ALIVE);
    for (let i = 0; i < potentials.length; i++) {
      const potential = potentials[i];
      if (Math.abs(this.scene.ship.x - potential.x) < potential.displayWidth/2) {        
        return potential.column    
      }
    }
    // if get here, there are no invaders directly above the player. Simply return the right most column
    return 10
  }

Then, below represents my equivalent of handleAlienShot routine. Broadly, it follows the same logic.

  handleAlienShot(bomb) {
    if (bomb.isActive()) {
      // if the bomb is already active, update the stepCount but do no need to do any more here
      bomb.shotStepCount ++;  
      return false;
    } 
    if (this.otherShot1 !==0 && this.reloadRate > this.otherShot1) return false; // if not enough time since last bomb dropped, do no more
    if (this.otherShot2 !==0 && this.reloadRate > this.otherShot2) return false; // if not enough time since last bomb dropped, do no more
    // get to here means we can fire (so long as there is an alien that can shoot)
         let shooter = this.rack.getAlienCoords(this.bombColumn);
        if (shooter) bomb.dropBomb(shooter.x, shooter.y)
  }

The code equivalent to the getAlienCoords routine of the original is a method within the rack object, as below.


  getAlienCoords(column) {
    const potentials = this.getMatching('column', column);
    if (potentials.length === 0) {
      return undefined
    } else {
      for (let i=0; i<potentials.length; i++) {
        if (potentials[i].isAlive()) return  {x: potentials[i].x, y: potentials[i].y+7}        
      }
    }
    return undefined;
  };

Collision detection with Phaser's built-in functions

The collision detection of the bombs against the shields are custom methods as shown above. However, I have used the built-in Phaser collision detection for: (i) bombs against the player, and (ii) player's bullet against bomb, by creating the following colliders within the main game scene.


this.physics.add.overlap(this.ship, this.invaderBombs,  this.playerHit, null, this);
    this.physics.add.overlap(this.bullet, this.invaderBombs,  this.bulletVsBomb, null, this);

The methods to handle when collision is detected as as follows.


When player is hit by bomb

 playerHit(player, bomb) {
    if (player.status === Player.Status.NORMAL) {
      this.playerExplodeSFX.play();
      player.setStatus(Player.Status.EXPLODE);
      bomb.disableBody(true,true)
    }    
    this.playScreen.renderLives(player.lives);
    if (player.lives > 0) {
      this.setStatus(MainGame.Status.RESPAWN);
    } else {
      this.setStatus(MainGame.Status.GAMEOVER);
    }
  }

When player's bullethits an alien bomb

  bulletVsBomb(bullet, bomb) {
    if (bullet.isActive()) {
      bomb.setStatus(Bomb.Status.STANDBY);
      bullet.setStatus(Bullet.Status.EXPLODE);
    }
  }

The game as it stands

In running the code (CODEPEN below), it seems to behave as expected. The rolling shot is dropped above the player, if there is any live alien above it. Otherwise, it is dropped from the right most column. The player can "shoot" the alien bomb, and when the bomb hits the player, you lose a life.


So far so good, but we'll see what happens when the other 2 bomb types are added.





2 views0 comments

Recent Posts

See All

p2 naive broadphase

var Broadphase = require('../collision/Broadphase'); module.exports = NaiveBroadphase; /** * Naive broadphase implementation. Does N^2...

sopiro motor constranit

import { Matrix2, Vector2 } from "./math.js"; import { RigidBody } from "./rigidbody.js"; import { Settings } from "./settings.js";...

Comments


記事: Blog2_Post
bottom of page