Bullets
Bullets are relatively simple, logic-wise. They all travel straight-forward, and de-activate themselves when they leave the screen.
At the top of our “src/main/states/gameplay/objects/bullets.asm” file we’ll setup some variables for bullets and include our tile data.
include "src/main/utils/hardware.inc"
include "src/main/utils/constants.inc"
SECTION "BulletVariables", WRAM0
wSpawnBullet: db
; how many bullets are currently active
wActiveBulletCounter:: db
; how many bullet's we've updated
wUpdateBulletsCounter: db
; Bytes: active, x , y (low), y (high)
wBullets:: ds MAX_BULLET_COUNT*PER_BULLET_BYTES_COUNT
SECTION "Bullets", ROM0
bulletMetasprite::
.metasprite1 db 0,0,8,0
.metaspriteEnd db 128
bulletTileData:: INCBIN "src/generated/sprites/bullet.2bpp"
bulletTileDataEnd::
We’ll need to loop through the bullet object pool in the following sections.
Initiating Bullets
In our “InitializeBullets” function, we’ll copy the tile data for the bullet sprites into VRAM, and set every bullet as inactive. Each bullet is 4 bytes, the first byte signaling if the bullet is active or not.
We’ll iterate through bullet object pool, named “wBullets”, and activate the first of the the four bytes. Then skipping the next 3 bytes, to go onto the next bullet. We’ll do this until we’ve looped for each bullet in our pool.
InitializeBullets::
xor a
ld [wSpawnBullet], a
; Copy the bullet tile data intto vram
ld de, bulletTileData
ld hl, BULLET_TILES_START
ld bc, bulletTileDataEnd - bulletTileData
call CopyDEintoMemoryAtHL
; Reset how many bullets are active to 0
xor a
ld [wActiveBulletCounter],a
ld b, a
ld hl, wBullets
ld [hl], a
InitializeBullets_Loop:
; Increase the address
ld a, l
add PER_BULLET_BYTES_COUNT
ld l, a
ld a, h
adc 0
ld h, a
; Increase how many bullets we have initailized
ld a, b
inc a
ld b, a
cp MAX_BULLET_COUNT
ret z
jp InitializeBullets_Loop
Updating Bullets
When we want to update each of bullets, first we should check if any bullets are active. If no bullets are active we can stop early.
UpdateBullets::
; Make sure we have SOME active enemies
ld a, [wSpawnBullet]
ld b, a
ld a, [wActiveBulletCounter]
or b
cp 0
ret z
; Reset our counter for how many bullets we have checked
xor a
ld [wUpdateBulletsCounter], a
; Get the address of the first bullet in hl
ld a, LOW(wBullets)
ld l, a
ld a, HIGH(wBullets)
ld h, a
jp UpdateBullets_PerBullet
If we have active bullets, we’ll reset how many bullets we’ve checked and set our “hl” registers to point to the first bullets address.
When were updating each bullet, we’ll check each byte, changing hl (the byte we want to read) as we go. At the start, “hl” should point to the first byte. “hl” should point to the first byte at the end too:
HL should point to the first byte at the end so we can easily do one of two things:
- deactivate the bullet
- jump to the next bullet (by simply adding 4 to hl)
For we each bullet, we’ll do the following:
- Check if active
- Get our x position, save into b
- Get our y scaled positon, save into c (low byte), and d (high byte)
- Decrease our y position to move the bullet upwards
- Reset HL to the first byte of our bullet
- Descale the y position we have in c & d, and jump to our deactivation code if c (the low byte) is high enough
- Draw our bullet metasprit, if it wasn’t previously deactivated
UpdateBullets_PerBullet:
; The first byte is if the bullet is active
; If it's NOT zero, it's active, go to the normal update section
ld a, [hl]
and a
jp nz, UpdateBullets_PerBullet_Normal
; Do we need to spawn a bullet?
; If we dont, loop to the next enemy
ld a, [wSpawnBullet]
and a
jp z, UpdateBullets_Loop
UpdateBullets_PerBullet_SpawnDeactivatedBullet:
; reset this variable so we don't spawn anymore
xor a
ld [wSpawnBullet], a
; Increase how many bullets are active
ld a, [wActiveBulletCounter]
inc a
ld [wActiveBulletCounter], a
push hl
; Set the current bullet as active
ld a, 1
ld [hli], a
; Get the unscaled player x position in b
ld a, [wPlayerPositionX]
ld b, a
ld a, [wPlayerPositionX+1]
ld d, a
; Descale the player's x position
; the result will only be in the low byt
srl d
rr b
srl d
rr b
srl d
rr b
srl d
rr b
; Set the x position to equal the player's x position
ld a, b
ld [hli], a
; Set the y position (low)
ld a, [wPlayerPositionY]
ld [hli], a
; Set the y position (high)
ld a, [wPlayerPositionY+1]
ld [hli], a
pop hl
UpdateBullets_PerBullet_Normal:
; Save our active byte
push hl
inc hl
; Get our x position
ld a, [hli]
ld b, a
; get our 16-bit y position
ld a, [hl]
sub BULLET_MOVE_SPEED
ld [hli], a
ld c, a
ld a, [hl]
sbc 0
ld [hl], a
ld d, a
pop hl; go to the active byte
; Descale our y position
srl d
rr c
srl d
rr c
srl d
rr c
srl d
rr c
; See if our non scaled low byte is above 160
ld a, c
cp 178
; If it's below 160, deactivate
jp nc, UpdateBullets_DeActivateIfOutOfBounds
Drawing the Bullets
We’ll draw our bullet metasprite like we drew the player, using our “DrawMetasprites” function. This function may alter the ‘h’ or ‘l’ registers, so we’ll push the hl register onto the stack before hand. After drawing, we’ll pop the hl register off of the stack to restore it’s value.
push hl
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Drawing a metasprite
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Save the address of the metasprite into the 'wMetaspriteAddress' variable
; Our DrawMetasprites functoin uses that variable
ld a, LOW(bulletMetasprite)
ld [wMetaspriteAddress], a
ld a, HIGH(bulletMetasprite)
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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pop hl
jp UpdateBullets_Loop
Deactivating the Bullets
If a bullet needs to be deactivated, we simply set it’s first byte to 0. At this point in time, the “hl” registers should point at our bullets first byte. This makes deactivation a really simple task. In addition to changing the first byte, we’ll decrease how many bullets we have that are active.
UpdateBullets_DeActivateIfOutOfBounds:
; if it's y value is grater than 160
; Set as inactive
xor a
ld [hl], a
; Decrease counter
ld a,[wActiveBulletCounter]
dec a
ld [wActiveBulletCounter], a
jp UpdateBullets_Loop
Updating the next bullet
After we’ve updated a single bullet, we’ll increase how many bullet’s we’ve updated. If we’ve updated all the bullets, we can stop our “UpdateBullets” function. Otherwise, we’ll add 4 bytes to the addressed stored in “hl”, and update the next bullet.
UpdateBullets_Loop:
; Check our counter, if it's zero
; Stop the function
ld a, [wUpdateBulletsCounter]
inc a
ld [wUpdateBulletsCounter], a
; Check if we've already
ld a, [wUpdateBulletsCounter]
cp MAX_BULLET_COUNT
ret nc
; Increase the bullet data our address is pointingtwo
ld a, l
add PER_BULLET_BYTES_COUNT
ld l, a
ld a, h
adc 0
ld h, a
Firing New Bullets
During the “UpdatePlayer” function previously, when use pressed A we called the “FireNextBullet” function.
This function will loop through each bullet in the bullet object pool. When it finds an inactive bullet, it will activate it and set it’s position equal to the players.
Our bullets only use one 8-bit integer for their x position, so need to de-scale the player’s 16-bit scaled x position
FireNextBullet::
; Make sure we don't have the max amount of enmies
ld a, [wActiveBulletCounter]
cp MAX_BULLET_COUNT
ret nc
; Set our spawn bullet variable to true
ld a, 1
ld [wSpawnBullet], a
ret
That’s it for bullets logic. Next we’ll cover enemies, and after that we’ll step back into the world of bullets with “Bullet vs Enemy” Collision.