top of page
Search
cedarcantab

Space Invaders in Phaser 3 (Part 3): Making the Player shoot

Updated: Jan 15, 2022

In the previous post I documented how I created the Space Invader' player which can move left and right by pressing the cursor keys. In this post I will explain how I made the player shoot bullets.


Moving the player shot fly!

The following from Computer Archeology ("CA") is relevant.


The player's shot uses the Y coordinate and interrupt id to decide when to run. The shot can be in several states: just starting, moving up screen, blowing up because of hitting an alien, or blowing because of hitting something else.

The shot's deltaY (rotated screen) is a constant 4-pixels per interrupt. The shot covers the entire screen at a rate of 60Hz*4 pixels = 240 pixels/sec. 

This can be achieved in Javascript simply by creating a Phaser sprite with a arcade.physics body, and setting the body's velocityY as -240 (i.e. up the screen).


However, let's study the original code.


Looking at the game object descriptor

As with the player object, we'll start by looking at the object descriptor.

Let's note down the key information which might be relevant in translating into Javascript:

  • the object handler starts at $03BB

  • the status can be one of: 0, 1, 2, 3 or 4; ie 5 states.

  • blow up timer is set to $10 = 16, which would be 16x1/60 seconds = 267 milli-seconds

  • object image is located at $1C90, and image size is 1 byte

  • Y-coordinate is $28 = 40. This would put the bullet at 8 pixels "above" the player, which would make sense since the bullet image is 8 bits (even though only 4 bits are used). In our Javascript world, the Y-coordinate would be the player's Y-coordinate - half of the sprite's height; hence 228 - 2 = 226.

Player Shot and Explosion Sprite

The Player Shot Sprite and the Player Shot Exploding sprite are as below.





The player shot texture as an image of 1x8 pixels, and an image of 8x8 for the explosion. There is only 1 image for the player shot explosion (i.e. no animation).


Looking at the code

The first few lines of the "task" is reproduced below. The status of the shot is checked and the jumps are made to the relevant code.


GameObj1:
; Game object 1: Move/draw the player shot
;
; This task executes at either mid-screen ISR (if it is on the top half of the non-rotated screen) or
; at the end-screen ISR (if it is on the bottom half of the screen).
;
03BB: 11 2A 20        LD      DE,$202A            ; Object's Yn coordiante
03BE: CD 06 1A        CALL    CompYToBeam         ; Compare to screen-update location
03C1: E1              POP     HL                  ; Pointer to task data
03C2: D0              RET     NC                  ; Make sure we are in the right ISR

03C3: 23              INC     HL                  ; Point to 2025 ... the shot status
03C4: 7E              LD      A,(HL)              ; Get shot status
03C5: A7              AND     A                   ; Return if ...
03C6: C8              RET     Z                   ; ... no shot is active
;
03C7: FE 01           CP      $01                 ; Shot just starting (requested elsewhere)?
03C9: CA FA 03        JP      Z,InitPlyShot       ; Yes ... go initiate shot
;
03CC: FE 02           CP      $02                 ; Progressing normally?
03CE: CA 0A 04        JP      Z,MovePlyShot       ; Yes ... go move it
;
03D1: 23              INC     HL                  ; 2026
03D2: FE 03           CP      $03                 ; Shot blowing up (not because of alien)?
03D4: C2 2A 04        JP      NZ,$042A            ; No ... try other options
;

When the shot is first fired...

The following code "InitPlyShot" is called. This code gets the player's X coordinate, and addes 8 to it to arrive at the X-coordinate of the shot (since the player sprite is 16 pixels wide). In Phaser, we dont need to do this becaue all sprites have their "origin" in the centre of the image.


InitPlyShot:
03FA: 3C              INC     A                   ; Type is now ...
03FB: 77              LD      (HL),A              ; ... 2 (in progress)
03FC: 3A 1B 20        LD      A,(playerXr)        ; Players Y coordinate
03FF: C6 08           ADD     A,$08               ; To center of player
0401: 32 2A 20        LD      (obj1CoorXr),A      ; Shot's Y coordinate
0404: CD 30 04        CALL    ReadPlyShot         ; Read 5 byte structure
0407: C3 00 14        JP      DrawShiftedSprite   ; Draw sprite and out

Moving the bullet

"MovePlyShot" is the code to move the bullet. The first line calls ReadPlyShot which loads register HL with the location of of player shot object descriptor table ($2027) and then calls ReadDesc code, which in turn loads the actual various bits of data into resgiters E, D, A, C, B, H.


MovePlyShot:
040A: CD 30 04        CALL    ReadPlyShot         ; Read the shot structure
040D: D5              PUSH    DE                  ; Hold pointer to sprite image
040E: E5              PUSH    HL                  ; Hold sprite coordinates
040F: C5              PUSH    BC                  ; Hold sprite size (in B)
0410: CD 52 14        CALL    EraseShifted        ; Erase the sprite from the screen
0413: C1              POP     BC                  ; Restore size
0414: E1              POP     HL                  ; Restore coords
0415: D1              POP     DE                  ; Restore pointer to sprite image
0416: 3A 2C 20        LD      A,(shotDeltaX)      ; DeltaX for shot
0419: 85              ADD     A,L                 ; Move the shot ...
041A: 6F              LD      L,A                 ; ... up the screen
041B: 32 29 20        LD      (obj1CoorYr),A      ; Store shot's new X coordinate
041E: CD 91 14        CALL    DrawSprCollision    ; Draw sprite with collision detection
0421: 3A 61 20        LD      A,(collision)       ; Test for ...
0424: A7              AND     A                   ; ... collision
0425: C8              RET     Z                   ; No collision ... out

ReadDesc

Since this code is called so often, I have reproduced it here in full.

ReadDesc:
; Load 5 bytes sprite descriptor from [HL]
1A3B: 5E              LD      E,(HL)              ; Descriptor ...
1A3C: 23              INC     HL                  ; ... sprite ...
1A3D: 56              LD      D,(HL)              ; ...
1A3E: 23              INC     HL                  ; ... picture
1A3F: 7E              LD      A,(HL)              ; Descriptor ...
1A40: 23              INC     HL                  ; ... screen ...
1A41: 4E              LD      C,(HL)              ; ...
1A42: 23              INC     HL                  ; ... location
1A43: 46              LD      B,(HL)              ; Number of bytes in sprite
1A44: 61              LD      H,C                 ; From A,C to ...
1A45: 6F              LD      L,A                 ; ... H,L
1A46: C9              RET                         ; Done
  • $2027/$2028 hold the location to the image texture -> this is loaded into register ED

  • $2029 holds the Y-coordinate -> store in register A

  • $202A: holds the X-coordinate -> load into C

  • $202B: holds the image size (in this case, 1, since 1 byte only) -> load into B

  • then the pixel coordinates are transferred to HL to compelte.

The really interesting bit happens in the DrawSprCollision code.


Draw the player shot

DrawSprCollision:
1491: CD 74 14        CALL    CnvtPixNumber       ; Convert pixel number to coord and shift
1494: AF              XOR     A                   ; Clear the ...
1495: 32 61 20        LD      (collision),A       ; ... collision-detection flag
1498: C5              PUSH    BC                  ; Hold count
1499: E5              PUSH    HL                  ; Hold screen
149A: 1A              LD      A,(DE)              ; Get byte
149B: D3 04           OUT     (SHFT_DATA),A       ; Write first byte to shift register
149D: DB 03           IN      A,(SHFT_IN)         ; Read shifted pattern
149F: F5              PUSH    AF                  ; Hold the pattern
14A0: A6              AND     (HL)                ; Any bits from pixel collide with bits on screen?
14A1: CA A9 14        JP      Z,$14A9             ; No ... leave flag alone
14A4: 3E 01           LD      A,$01               ; Yes ... set ...
14A6: 32 61 20        LD      (collision),A       ; ... collision flag
14A9: F1              POP     AF                  ; Restore the pixel pattern
14AA: B6              OR      (HL)                ; OR it onto the screen
14AB: 77              LD      (HL),A              ; Store new screen value
14AC: 23              INC     HL                  ; Next byte on screen
14AD: 13              INC     DE                  ; Next in pixel pattern
14AE: AF              XOR     A                   ; Write zero ...
14AF: D3 04           OUT     (SHFT_DATA),A       ; ... to shift register
14B1: DB 03           IN      A,(SHFT_IN)         ; Read 2nd half of shifted sprite
14B3: F5              PUSH    AF                  ; Hold pattern
14B4: A6              AND     (HL)                ; Any bits from pixel collide with bits on screen?
14B5: CA BD 14        JP      Z,$14BD             ; No ... leave flag alone
14B8: 3E 01           LD      A,$01               ; Yes ... set ...
14BA: 32 61 20        LD      (collision),A       ; ... collision flag
14BD: F1              POP     AF                  ; Restore the pixel pattern
14BE: B6              OR      (HL)                ; OR it onto the screen
14BF: 77              LD      (HL),A              ; Store new screen pattern
14C0: E1              POP     HL                  ; Starting screen coordinate
14C1: 01 20 00        LD      BC,$0020            ; Add 32 ...
14C4: 09              ADD     HL,BC               ; ... to get to next row
14C5: C1              POP     BC                  ; Restore count
14C6: 05              DEC     B                   ; All done?
14C7: C2 98 14        JP      NZ,$1498            ; No ... do all rows
14CA: C9              RET                         ; Done

In the first line, some complicated stuff to convert the pixel based coordinate (currently stored in register HL) to something that the hardware can understand.


CnvtPixNumber:
; Convert pixel number in HL to screen coordinate and shift amount.
; HL gets screen coordinate.
; Hardware shift-register gets amount.
1474: 7D              LD      A,L                 ; Get X coordinate
1475: E6 07           AND     $07                 ; Shift by pixel position
1477: D3 02           OUT     (SHFTAMNT),A        ; Write shift amount to hardware
1479: C3 47 1A        JP      ConvToScr           ; HL = HL/8 + 2000 (screen coordinate)

It seems the first stage is to convert the pixel coordinate by converting the "pixels" into "bytes" by diving it by 8 then offsetting that by $2000 (as a starter).


Going back to the DrawSprCollision code, it seems the lines highlighted in red reads what's on the screen, to see if there's anything there and set's a collision flag ($2061) before actually writing the sprite image to the screen. Each row of the image data (1 byte) can span across 2 bytes so it does what I've described twice, before moving onto the next row of sprite image data by adding $20 (32) bytes to the screen location (since there are 32 bytes across each row of the unrotated screen).


When the bullet hits something...

After checking the status of the shot and it is neither "just been shot" nor "flying" then we reach the following code.

; Shot blowing up because it left the playfield, hit a shield, or hit another bullet
03D7: 35              DEC     (HL)                ; Decrement the timer
03D8: CA 36 04        JP      Z,EndOfBlowup       ; If done then
03DB: 7E              LD      A,(HL)              ; Get timer value
03DC: FE 0F           CP      $0F                 ; Starts at 10 ... first decrement brings us here
03DE: C0              RET     NZ                  ; Not the first time ... explosion has been drawn

The most interesting code comes later. As indicated by the number of possible states, the player shot can "die" in a variety of circumstances; i) when it hits an invader, ii) when it hits an invader bomb, or when it reaches the top of the screen without hitting anything, ie "miss".


The following piece of code appears to handle these states.

PlayerShotHit:
; The player's shot hit something (or is being removed from play)
;
14D8: 3A 25 20        LD      A,(plyrShotStatus)  ; Player shot flag
14DB: FE 05           CP      $05                 ; Alien explosion in progress?
14DD: C8              RET     Z                   ; Yes ... ignore this function
14DE: FE 02           CP      $02                 ; Normal movement?
14E0: C0              RET     NZ                  ; No ... out
;
14E1: 3A 29 20        LD      A,(obj1CoorYr)      ; Get Yr coordinate of player shot
14E4: FE D8           CP      $D8                 ; Compare to 216 (40 from Top-rotated)
14E6: 47              LD      B,A                 ; Hold value for later
14E7: D2 30 15        JP      NC,$1530            ; Yr is within 40 from top initiate miss-explosion (shot flag 3)
14EA: 3A 02 20        LD      A,(alienIsExploding); Is an alien ...
14ED: A7              AND     A                   ; ... blowing up?
14EE: C8              RET     Z                   ; No ... out
;
14EF: 78              LD      A,B                 ; Get original Yr coordinate back to A
14F0: FE CE           CP      $CE                 ; Compare to 206 (50 from rotated top)
14F2: D2 79 15        JP      NC,$1579            ; Yr is within 50 from top? Yes ... saucer must be hit
14F5: C6 06           ADD     A,$06               ; Offset to coordinate for wider "explosion" picture
14F7: 47              LD      B,A                 ; Hold that
14F8: 3A 09 20        LD      A,(refAlienYr)      ; Ref alien Y coordianate

The 4 lines in RED (I think) means the following:

  • Load the Y-coordinate of the game player object into register A

  • Compare register A to $D8 = 216 (or in the rotated Space Invader world, 40 pixels from the ("top" of the screen)

  • Load it into B-register for later use

  • If indeed the Y-coordinate is 40, jump to the code below

; Player shot leaving playfield, hitting shield, or hitting an alien shot
1530: 3E 03           LD      A,$03               ; Mark ...
1532: 32 25 20        LD      (plyrShotStatus),A  ; ... player shot hit something other than alien
1535: C3 4A 15        JP      $154A               ; Finish up

This code appears to set the "status" of the player shot (plyrShotStatus) to 3, which presumably means it's got to the top of the screen without hitting anything.


The sprite in the SI world is 1x8, and the origin of the sprite is the top. On the other hand, the sprite I have created in Phaser is 1x4, and the origin is the middle. Hence, 40 from the top in SI world is 34 in my Phaser SI world.


Hitting the saucer

For reference by later post on the flying saucer, the code in BLUE seems to be a similar code to the above, except it compares the Y-coordinate to $CE = 206 or 50 from the top of the rotated Space Invader screen.



Javascript/Phaser 3 version

As with the player object, I have based the player shot on a extended Phaser object. I have defined only 3 states; the 3 states referred to in CA - "just starting", "moving up screen", "blowing up" - I have implemented as "STANDBY", "FIRED" and "EXPLODE".


When the shot explodes, rather than play a Phaser animation (as in the Player), since there is only 1 frame I have simply changed the texture and wait 300 milli-seconds (just rounded up from the original game's design of 267 milli-seconds) before making the shot disappear.


class Bullet extends Phaser.Physics.Arcade.Sprite {
  
  constructor(scene, x, y) {
    super(scene, x, y, 'playerBullet');
    this.scene = scene;
    scene.add.existing(this);
    scene.physics.add.existing(this);
    this.status; 
    this.setStatus(Bullet.Status.STANDBY);
  }

  static Status = {
    STANDBY: 0,
    FIRED: 1,
    EXPLODE: 2
  }
  static SPEED = -240;
  static EXPLOSION_DURATION = 300;

  preUpdate(time, delta) {
    super.preUpdate(time, delta);
    switch (this.status) {
      case Bullet.Status.FIRED:
        if (this.y < SKY_BOUNDARY) this.setStatus(Bullet.Status.EXPLODE);
        break;
    }
  }
  
  isActive() {
    return (this.status === Bullet.Status.FIRED);
  }
  
  setStatus(newStatus) {
    this.status = newStatus;
    switch (this.status) {      
      case Bullet.Status.STANDBY:
        this.disableBody(true,true);
        break;
      case Bullet.Status.FIRED:
        this.enableBody(true, this.scene.ship.x,this.scene.ship.y - this.scene.ship.displayHeight / 2, true, true);
        this.setTexture('playerBullet');
        this.body.setVelocity(0, Bullet.SPEED);
        break;
      case Bullet.Status.EXPLODE:
        this.body.stop()
        this.setTexture('pBulletExplode')  
        this.scene.time.delayedCall(Bullet.EXPLOSION_DURATION, this.setStatus, [Bullet.Status.STANDBY], this)
        break;
    }
  }  
} // end of Bullet Class

Here's the above code in CODEPEN.



14 views0 comments

Comments


記事: Blog2_Post
bottom of page