top of page
Search
  • cedarcantab

Space Invaders in Phaser 3 (Part 9): Drawing the Shields


I am sure you would be familiar with hte 4 shields in the game. The distinguishing feature is the way the shields get damaged gradually as the alien bombs drop upon them, affording the player less and less cover with time. The player can also shoot up at the shield, creating a narrow "window" through which it can shoot but still maintain an element of protection.



In this post, I will document the challenges I had in re-creating the shields, up to the point of being able for the player to shoot holes in the shield.


The original code

The narrative on the shields by Computer Archeology starts with the following code.


DrawShieldPl1:
; Draw the shields for player 1 (draws it in the buffer in the player's data area).
;
01EF: 21 42 21        LD      HL,$2142            ; Player 1 shield buffer (remember between games in multi-player)
01F2: C3 F8 01        JP      $01F8               ; Common draw point

It looks like there is a data table at $2142 that is used to remember various things about each player 1, including the state of the shields of each player.


Drawing the shields

The code which draws the shields is below


01F8: 0E 04           LD      C,$04               ; Going to draw 4 shields
01FA: 11 20 1D        LD      DE,$1D20            ; Shield pixel pattern
01FD: D5              PUSH    DE                  ; Hold the start for the next shield
01FE: 06 2C           LD      B,$2C               ; 44 bytes to copy
0200: CD 32 1A        CALL    BlockCopy           ; Block copy DE to HL (B bytes)
0203: D1              POP     DE                  ; Restore start of shield pattern
0204: 0D              DEC     C                   ; Drawn all shields?
0205: C2 FD 01        JP      NZ,$01FD            ; No ... go draw them all
0208: C9              RET                         ; Done

The code above first sets register C to 4 as a counter to draw 4 shields.

Then register DE is set to point to the location holding the shield pattern, which is as below.

1D20: FF 0F FF 1F FF 3F FF 7F FF FF FC FF F8 FF F0 FF F0 FF F0 FF F0 FF                                    
1D36: F0 FF F0 FF F0 FF F8 FF FC FF FF FF FF FF FF 7F FF 3F FF 1F FF 0F 

There is 44 bytes of data. When one looks at the above in binary, it begins to make sense.

ShieldImage:
; Shield image pattern. 2 x 22 = 44 bytes.
;
;************....
;*************...
;**************..
;***************.
;****************
;..**************
;...*************
;....************
;....************
;....************
;....************
;....************
;....************
;....************
;...*************
;..**************
;****************
;****************
;***************.
;**************..
;*************...
;************....

When rotated anti-clockwise 90 degrees, you see the familiar shape.







The BlockCopy code, if you remember, copies specified number of bytes from location specified by DE to location specified by HL. In this particular case, the code copies the clean shield data image to player specific memory.


There is another piece of code to "dump" what's in the player's data bank to screen, and vice-versa (since when swapping between players, need to remember the damaged state of the shields).


CopyShields:
; A is 1 for screen-to-buffer, 0 for to buffer-to-screen
; HL is screen coordinates of first shield. There are 23 rows between shields.
; DE is sprite buffer in memory.
;
021E: 32 81 20        LD      (tmp2081),A         ; Remember copy/restore flag
0221: 01 02 16        LD      BC,$1602            ; 22 rows, 2 bytes/row (for 1 shield pattern)
0224: 21 06 28        LD      HL,$2806            ; Screen coordinates
0227: 3E 04           LD      A,$04               ; Four shields to move
0229: F5              PUSH    AF                  ; Hold shield count
022A: C5              PUSH    BC                  ; Hold sprite-size
022B: 3A 81 20        LD      A,(tmp2081)         ; Get back copy/restore flag
022E: A7              AND     A                   ; Not zero ...
022F: C2 42 02        JP      NZ,$0242            ; ... means remember shidles
0232: CD 69 1A        CALL    RestoreShields      ; Restore player's shields
0235: C1              POP     BC                  ; Get back sprite-size
0236: F1              POP     AF                  ; Get back shield count
0237: 3D              DEC     A                   ; Have we moved all shields?
0238: C8              RET     Z                   ; Yes ... out
0239: D5              PUSH    DE                  ; Hold sprite buffer
023A: 11 E0 02        LD      DE,$02E0            ; Add 2E0 (23 rows) to get to ...
023D: 19              ADD     HL,DE               ; ... next shield on screen
023E: D1              POP     DE                  ; restore sprite buffer
023F: C3 29 02        JP      $0229               ; Go back and do all
;
0242: CD 7C 14        CALL    RememberShields     ; Remember player's shields
0245: C3 35 02        JP      $0235               ; Continue with next shield

The above code should hold clues as to where the shields should be drawn and hence I have spent some time trying to understand it.


First, the "size" of the image is loaded into register BC, as $16=22 rows of $02 bytes.


Then the "location" for the first shield of $2806 is loaded to register-HL. Then depending on the setting of register A, the code either jumps to code that reads the screen and records the data into the player's data buffer, or if A is zero, the shield image is written from the player's data buffer to the screen.


RestoreShields is called to actually draw the shield to the screen, to the location specified by register-HL. The below code draws the shield line by line, each time bumping register HL (the location on screen) by $20 (=32) - since each row (in the unrotated screen) consists of 32 bytes.


RestoreShields:
; Logically OR the player's shields back onto the playfield
; DE = sprite
; HL = screen
; C = bytes per row
; B = number of rows
1A69: C5              PUSH    BC                  ; Preserve BC
1A6A: E5              PUSH    HL                  ; Hold for a bit
1A6B: 1A              LD      A,(DE)              ; From sprite
1A6C: B6              OR      (HL)                ; OR with screen
1A6D: 77              LD      (HL),A              ; Back to screen
1A6E: 13              INC     DE                  ; Next sprite
1A6F: 23              INC     HL                  ; Next on screen
1A70: 0D              DEC     C                   ; Row done?
1A71: C2 6B 1A        JP      NZ,$1A6B            ; No ... do entire row
1A74: E1              POP     HL                  ; Original start
1A75: 01 20 00        LD      BC,$0020            ; Bump HL by ...
1A78: 09              ADD     HL,BC               ; ... one screen row
1A79: C1              POP     BC                  ; Restore
1A7A: 05              DEC     B                   ; Row counter
1A7B: C2 69 1A        JP      NZ,RestoreShields   ; Do all rows
1A7E: C9              RET             

Once this code is executed, The RED highlighted line bumps up the screen location address by another 23 rows; ie the spacing between each shield is 23 rows.


The logic makes sense. But I am most interested in the location of the shields. $2806 in the unrotated screen is the 5th byte from the left (counting most left byte as the 1st byte) and 10th row from the top (counting the very top row as 1). Thinking in terms of the "1st bit", that pixel (which would be the bottom left hand corner of the shield image) in the rotated screen would be 10th pixel from the left, 33nd pixel from the bottom or (9, 32). In the Javascript world coordinate system this would be (9, 224) -- I think -- to complete the thinking, the top left hand corner of the 22x16 pixel image would be (9, 208) -- I think. Unfortunately, this does not seem correct....the first shield seems far close to the left hand side, and it seems too low, when you compare it to Youtube videos. Again, I would be grateful if someone could tell me where I am going wrong.


Damaging the shields

The CopyShields code is written such that as well as writing the shield data from the player's data bank to the screen, it can read the screen and write back to the player's data bank. This is so that the damaged state of the shield can be stored for each player. Most importantly, it means that the "state" of the shield is held directly in the video RAM, and damaging the shield is achieved by the very fact of writing the shot explosion image on top of the shield. This is a very efficient way of storing the state of the shields. Since each shield is comprised of 22x16 pixels, and there are 4 four of them, that's 22x16x4 = 1,408 pixels - each can be solid or damaged - that need to be remembered. As far as I can work out, I can not use the same approach in Javascript/Phaser 3.


Creating this in Javascript/Phaser

Now, it would be so easy to create a png image (22 pixels wide, 16 pixels high) of the above, loaded as a texture and display on the screen. However, in the end I have decided to create a dedicated shield object which is an array which consists of the "bit-map" image of the above, and a method to draw the shield bit-by-bit using graphics function of Phaser (just like drawing the 1-pixel line at the bottom of the screen).


I did this because I needed a way to add damage to the shields on a pixel by pixel basis. I tried doing this initially by creating each shield as 22x16=352 single pixel objects x 4 shields = 1,408 objects (!) and running Phaser's collision detection routine - but it simply did not work (at least in Codepen, complaining that the code had gone into an infinite loop).


The code for the Shield "object" I ended up with, is as follows.


class Shield {
  // each shield is 22 x 16 = 352 "bricks".
  constructor (scene, index) {
    this.scene = scene;
    this.bitMapArray;
    this.index = index;
    this.leftX = Shield.SHIELD_POS_X[this.index]
  }
  
  static WIDTH = 22;
  static HEIGHT = 16;
  static SHIELD_POS_X = [35, 80, 125, 170]  ; // X-coordinate of top left corner of the 4 shields
  static SHIELD_TOP = 185; // 185
  static SHIELD_BOTTOM = 201;   // 201 
  static DAMAGE = [
    "  x   ",
    "x   x ",
    "  xx x",
    " xxxx ",
    "x xxx ",
    " xxxxx",
    "x xxx ",
    " x x x"
  ]

  restoreShield() {
    this.bitMapArray = [];
    for (let i = 0; i < 16; i++) {
      const rowArray = [];
      for (let j = 0; j < 22; j++) {
        if (this.scene.textures.getPixel(j, i, 'shield')["g"] === 255) {
          rowArray.push(1);
          this.scene.graphics.fillStyle(0x00ff00).fillPoint(this.leftX + j, Shield.SHIELD_TOP+i);  
        } else rowArray.push(0)
      }
      this.bitMapArray.push(rowArray);
    }
  }

  checkBrick(x,y) {
    if (this.safe(x,y)) return (this.bitMapArray[y][x] === 1)
  }

  safe(x,y) {
    return (x>=0 && x<Shield.WIDTH && y>=0 && y<Shield.HEIGHT)
  }
  
  damageShield(bombX, bombY) {
    for (let row = 0; row < 8; row++) {
      const y = bombY - 3 + row;
      if (y < 0 || y >= Shield.HEIGHT) {
        continue;
      }
      for (let col = 0; col < 6; col++) {
        const x = bombX - 1 + col;
        if (x < 0 || x >= Shield.WIDTH) {
          continue;
        }
        if (Shield.DAMAGE[row][col] === "x") {
          this.scene.graphics.fillStyle(0x000000).fillPoint(this.leftX + x, Shield.SHIELD_TOP + y);
          this.bitMapArray[y][x] = 0;
        }
      }
    }
  }
}

Maintaining the shield status as an 22x16 array

I tried several different methods to maintain the status of the shields, but ended up with what might look like an primitive method. I simply created an 22x16 array called this.bitMapArray which comprises of 1's and 0's. The equivalent of the original code's shield template at $1D20, is the shield image texture, loaded in the preloader. I use the Phaser getPixel function to read the image pixel by pixel and if it's green, fill the array with 1, otherwise 0 by the resetShield() method - this would be the equivalent of RestoreShields in the original code.


Based on the above, I had to basically create the logic for damaging the shields from scratch.


Yes, in comparison with the original code, this is enormously inefficient...I would be very interested in knowing better ways to write this in Javascript.


Damaging the shield

I wrote a method called damageShield which takes an (x,y) coordinate and damages the shield at that coordinate. The x,y is relative to the top-left hand corner of the shield. It works through the damage pattern, defined as an array of strings called DAMAGE, and changes the this.bitMapArray's relevant "pixel" to zero - this is equivalent to the original code overwriting the shield image with the explosion image. At the same time, it draw a black pixel where relevant.


Again, this seems enormously complex for such a simple task - if there is an easier way to do it, I would love to find out.


To support the collision detection routine, I wrote a simple method called checkBrick() which checks whether a particular pixel of the shield is 1 or 0 and returns true or false respectively.


Collision detection

The most troublesome part was the collision detection. As stated above, I was unable to use the Phaser's built in collision detection, presumably because there's simply to many objects. So I wrote a crude collision detection routine customised to the bullet class, within the bullet class itself.


When the bullet is fired, I call a method I called this.getShieldID() which identifies which shield (of the four) the bullet x-coordinate overlaps with (since the bullet only travels straight up, no need to do this any more than once).


  getShieldID() {
    // if there's no shield in the way of bullet, return -1, otherwise return id of relevant shield
    this.shieldInSight = -1;
    Shield.SHIELD_POS_X.forEach((x,i) => {
      if (this.x >= x && this.x < x + Shield.WIDTH) this.shieldInSight = i;
    });
  }

Then, I wrote a method referred as overlapShield() which checks whether the bullet overlaps the area occupied by the shield identified above, as it flies up.


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

If the bullet is overlapping the shield position, then it goes and checks the "brick" at the tip of the bullet. If it is "filled", then the damageShield method is called at the point. This is obviously a bit different from the original code in that it only checks the "tip" of the bullet whereas the original code checks the entire area occupied by the bullet. However, it seems to work well enough.


Here's the CODEPEN, in which you are now able to shoot the aliens, score, earn extra life at 1000 and 15000, and the player's shot will damage the shields.






9 views0 comments
記事: Blog2_Post
bottom of page