The Player

The player’s logic is pretty simple. The player can move in 4 directions and fire bullets. We update the player by checking our input directions and the A button. We’ll move in the proper direction if its associated d-pad button is pressed. If the A button is pressed, we’ll spawn a new bullet at the player’s position.

Our player will have 3 variables:

  • wePlayerPositionX - a 16-bit scaled integer
  • wePlayerPositionY - a 16-bit scaled integer
  • wPlayerFlash - a 16-bit integer used when the player gets damaged

⚠️ NOTE: The player can move vertically AND horizontally. So, unlike bullets and enemies, it’s x position is a 16-bit scaled integer.

These are declared at the top of the “src/main/states/gameplay/objects/player.asm” file

include "src/main/utils/hardware.inc"
include "src/main/utils/hardware.inc"
include "src/main/utils/constants.inc"

SECTION "PlayerVariables", WRAM0

; first byte is low, second is high (little endian)
wPlayerPositionX:: dw
wPlayerPositionY:: dw

mPlayerFlash: dw

Well draw our player, a simple ship, using the previously discussed metasprites implementation. Here is what we have for the players metasprites and tile data:

SECTION "Player", ROM0

playerShipTileData: INCBIN "src/generated/sprites/player-ship.2bpp"
playerShipTileDataEnd:

playerTestMetaSprite::
    .metasprite1    db 0,0,0,0
    .metasprite2    db 0,8,2,0
    .metaspriteEnd  db 128

Initializing the Player

Initializing the player is pretty simple. Here’s a list of things we need to do:

  • Reset oir wPlayerFlash variable
  • Reset our wPlayerPositionX variable
  • Reset our wPlayerPositionU variable
  • Copy the player’s ship into VRAM

We’ll use a constant we declared in “src/main/utils/constants.inc” to copy the player ship’s tile data into VRAM. Our enemy ship and player ship both have 4 tiles (16 bytes for each tile). In the snippet below, we can define where we’ll place the tile data in VRAM relative to the _VRAM constant:

RSRESET
DEF spriteTilesStart            RB _VRAM
DEF PLAYER_TILES_START          RB 4*16
DEF ENEMY_TILES_START           RB 4*16
DEF BULLET_TILES_START          RB 0

Here’s what our “InitializePlayer” function looks like. Recall, this was called when initiating the gameplay game state:

InitializePlayer::

    xor a
    ld [mPlayerFlash], a
    ld [mPlayerFlash+1], a

    ; Place in the middle of the screen
    xor a
    ld [wPlayerPositionX], a
    ld [wPlayerPositionY], a

    ld a, 5
    ld [wPlayerPositionX+1], a
    ld [wPlayerPositionY+1], a

    
CopyPlayerTileDataIntoVRAM:
    ; Copy the player's tile data into VRAM
	ld de, playerShipTileData
	ld hl, PLAYER_TILES_START
	ld bc, playerShipTileDataEnd - playerShipTileData
    call CopyDEintoMemoryAtHL

    ret

Updating the Player

We can break our player’s update logic into 2 parts:

  • Check for joypad input, move with the d-pad, shoot with A
  • Depending on our “wPlayerFlash” variable: Draw our metasprites at our location

Checking the joypad is done like the previous tutorials, we’ll perform bitwise “and” operations with constants for each d-pad direction.

UpdatePlayer::

UpdatePlayer_HandleInput:

	ld a, [wCurKeys]
	and PADF_UP
	call nz, MoveUp

	ld a, [wCurKeys]
	and PADF_DOWN
	call nz, MoveDown

	ld a, [wCurKeys]
	and PADF_LEFT
	call nz, MoveLeft

	ld a, [wCurKeys]
	and PADF_RIGHT
	call nz, MoveRight

	ld a, [wCurKeys]
	and PADF_A
	call nz, TryShoot

For player movement, our X & Y are 16-bit integers. These both require two bytes. There is a little endian ordering, the first byte will be the low byte. The second byte will be the high byte. To increase/decrease these values, we add/subtract our change amount to/from the low byte. Then afterwards, we add/subtract the remainder of that operation to/from the high byte.

MoveUp:

    ; decrease the player's y position
    ld a, [wPlayerPositionY]
    sub PLAYER_MOVE_SPEED
    ld [wPlayerPositionY], a

    ld a, [wPlayerPositionY]
    sbc 0
    ld [wPlayerPositionY], a

    ret

MoveDown:

    ; increase the player's y position
    ld a, [wPlayerPositionY]
    add PLAYER_MOVE_SPEED
    ld [wPlayerPositionY], a

    ld a, [wPlayerPositionY+1]
    adc 0
    ld [wPlayerPositionY+1], a

    ret

MoveLeft:

    ; decrease the player's x position
    ld a, [wPlayerPositionX]
    sub PLAYER_MOVE_SPEED
    ld [wPlayerPositionX], a

    ld a, [wPlayerPositionX+1]
    sbc 0
    ld [wPlayerPositionX+1], a
    ret

MoveRight:

    ; increase the player's x position
    ld a, [wPlayerPositionX]
    add PLAYER_MOVE_SPEED
    ld [wPlayerPositionX], a

    ld a, [wPlayerPositionX+1]
    adc 0
    ld [wPlayerPositionX+1], a

    ret

When the player wants to shoot, we first check if the A button previously was down. If it was, we won’t shoot a new bullet. This avoids bullet spamming a little. For spawning bullets, we have a function called “FireNextBullet”. This function will need the new bullet’s 8-bit X coordinate and 16-bit Y coordinate, both set in a variable it uses called “wNextBullet”

TryShoot:
	ld a, [wLastKeys]
	and PADF_A
    ret nz

    jp FireNextBullet

After we’ve potentially moved the player and/or shot a new bullet. We need to draw our player. However, to create the “flashing” effect when damaged, we’ll conditionally NOT draw our player sprite. We do this based on the “wPlayerFlash” variable.

  • If the “wPlayerFlash” variable is 0, the player is not damaged, we’ll skip to drawing our player sprite.
  • Otherwise, decrease the “wPlayerFlash” variable by 5.
    • We’ll shift all the bits of the “wPlayerFlash” variable to the right 4 times
    • If the result is less than 5, we’ll stop flashing and draw our player metasprite.
    • Otherwise, if the first bit of the decscaled “wPlayerFLash” variable is 1, we’ll skip drawing the player.

*NOTE: The following resumes from where the “UpdatePlayer_HandleInput” label ended above.

    ld a, [mPlayerFlash+0]
    ld b, a

    ld a, [mPlayerFlash+1]
    ld c, a

UpdatePlayer_UpdateSprite_CheckFlashing:

    ld a, b
    or c
    jp z, UpdatePlayer_UpdateSprite

    ; decrease bc by 5
    ld a, b
    sub 5
    ld b, a
    ld a, c
    sbc 0
    ld c, a
    

UpdatePlayer_UpdateSprite_DecreaseFlashing:

    ld a, b
    ld [mPlayerFlash], a
    ld a, c
    ld [mPlayerFlash+1], a

    ; descale bc
    srl c
    rr b
    srl c
    rr b
    srl c
    rr b
    srl c
    rr b

    ld a, b
    cp 5
    jp c, UpdatePlayer_UpdateSprite_StopFlashing


    bit 0, b
    jp z, UpdatePlayer_UpdateSprite

UpdatePlayer_UpdateSprite_Flashing:

    ret
UpdatePlayer_UpdateSprite_StopFlashing:

    xor a
    ld [mPlayerFlash],a
    ld [mPlayerFlash+1],a

If we get past all of the “wPlayerFlash” logic, we’ll draw our player using the “DrawMetasprite” function we previously discussed.

UpdatePlayer_UpdateSprite:

    ; Get the unscaled player x position in b
    ld a, [wPlayerPositionX+0]
    ld b, a
    ld a, [wPlayerPositionX+1]
    ld d, a
    
    srl d
    rr b
    srl d
    rr b
    srl d
    rr b
    srl d
    rr b

    ; Get the unscaled player y position in c
    ld a, [wPlayerPositionY+0]
    ld c, a
    ld a, [wPlayerPositionY+1]
    ld e, a

    srl e
    rr c
    srl e
    rr c
    srl e
    rr c
    srl e
    rr c
    
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Drawing the palyer metasprite
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


    ; Save the address of the metasprite into the 'wMetaspriteAddress' variable
    ; Our DrawMetasprites functoin uses that variable
    ld a, LOW(playerTestMetaSprite)
    ld [wMetaspriteAddress+0], a
    ld a, HIGH(playerTestMetaSprite)
    ld [wMetaspriteAddress+1], a


    ; Save the x position
    ld a, b
    ld [wMetaspriteX], a

    ; Save the y position
    ld a, c
    ld [wMetaspriteY], a

    ; Actually call the 'DrawMetasprites function
    call DrawMetasprites;

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    ret

That’s the end our our “UpdatePlayer” function. The final bit of code for our player handles when they are damaged. When an enemy damages the player, we want to decrease our lives by one. We’ll also start flashing by giving our ‘mPlayerFlash’ variable a non-zero value. In the gameplay game state, if we’ve lost all lives, gameplay will end.

DamagePlayer::

    

    xor a
    ld [mPlayerFlash], a
    inc a
    ld [mPlayerFlash+1], a

    ld a, [wLives]
    dec a
    ld [wLives], a

    ret

That’s everything for our player. Next, we’ll go over bullets and then onto the enemies.