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.