Enemies
Enemies in SHMUPS often come in a variety of types, and travel also in a variety of patterns. To keep things simple for this tutorial, we’ll have one enemy that flys straight downward. Because of this decision, the logic for enemies is going to be similar to bullets in a way. They both travel vertically and disappear when off screeen. Some differences to point out are:
- Enemies are not spawned by the player, so we need logic that spawns them at random times and locations.
- Enemies must check for collision against the player
- We’ll check for collision against bullets in the enemy update function.
Here are the RAM variables we’ll use for our enemies:
- wCurrentEnemyX & wCurrentEnemyY - When we check for collisions, we’ll save the current enemy’s position in these two variables.
- wNextEnemyXPosition - When this variable has a non-zero value, we’ll spawn a new enemy at that position
- wSpawnCounter - We’ll decrease this, when it reaches zero we’ll spawn a new enemy (by setting ‘wNextEnemyXPosition’ to a non-zero value).
- wActiveEnemyCounter - This tracks how many enemies we have on screen
- wUpdateEnemiesCounter - This is used when updating enemies so we know how many we have updated.
- wUpdateEnemiesCurrentEnemyAddress - When we check for enemy v. bullet collision, we’ll save the address of our current enemy here.
include "src/main/utils/hardware.inc"
include "src/main/utils/constants.inc"
SECTION "EnemyVariables", WRAM0
wCurrentEnemyX:: db
wCurrentEnemyY:: db
wSpawnCounter: db
wNextEnemyXPosition: db
wActiveEnemyCounter::db
wUpdateEnemiesCounter:db
wUpdateEnemiesCurrentEnemyAddress::dw
; Bytes: active, x , y (low), y (high), speed, health
wEnemies:: ds MAX_ENEMY_COUNT*PER_ENEMY_BYTES_COUNT
Just like with bullets, we’ll setup ROM data for our enemies tile data and metasprites.
SECTION "Enemies", ROM0
enemyShipTileData:: INCBIN "src/generated/sprites/enemy-ship.2bpp"
enemyShipTileDataEnd::
enemyShipMetasprite::
.metasprite1 db 0,0,4,0
.metasprite2 db 0,8,6,0
.metaspriteEnd db 128
Initializing Enemies
When initializing the enemies (at the start of gameplay), we’ll copy the enemy tile data into VRAM. Also, like with bullets, we’ll loop through and make sure each enemy is set to inactive.
InitializeEnemies::
ld de, enemyShipTileData
ld hl, ENEMY_TILES_START
ld bc, enemyShipTileDataEnd - enemyShipTileData
call CopyDEintoMemoryAtHL
xor a
ld [wSpawnCounter], a
ld [wActiveEnemyCounter], a
ld [wNextEnemyXPosition], a
ld b, a
ld hl, wEnemies
InitializeEnemies_Loop:
; Set as inactive
ld [hl], 0
; Increase the address
ld a, l
add PER_ENEMY_BYTES_COUNT
ld l, a
ld a, h
adc 0
ld h, a
inc b
ld a, b
cp MAX_ENEMY_COUNT
ret z
jp InitializeEnemies_Loop
Updating Enemies
When “UpdateEnemies” is called from gameplay, the first thing we try to do is spawn new enemies. After that, if we have no active enemies (and are not trying to spawn a new enemy), we stop the “UpdateEnemies” function. From here, like with bullets, we’ll save the address of our first enemy in hl and start looping through.
UpdateEnemies::
call TryToSpawnEnemies
; Make sure we have active enemies
; or we want to spawn a new enemy
ld a, [wNextEnemyXPosition]
ld b, a
ld a, [wActiveEnemyCounter]
or b
and a
ret z
xor a
ld [wUpdateEnemiesCounter], a
ld a, LOW(wEnemies)
ld l, a
ld a, HIGH(wEnemies)
ld h, a
jp UpdateEnemies_PerEnemy
When we are looping through our enemy object pool, let’s check if the current enemy is active. If it’s active, we’ll update it like normal. If it isn’t active, the game checks if we want to spawn a new enemy. We specify we want to spawn a new enemy by setting ‘wNextEnemyXPosition’ to a non-zero value. If we don’t want to spawn a new enemy, we’ll move on to the next enemy.
If we want to spawn a new enemy, we’ll set the current inactive enemy to active. Afterwards, we’ll set it’s y position to zero, and it’s x position to whatever was in the ‘wNextEnemyXPosition’ variable. After that, we’ll increase our active enemy counter, and go on to update the enemy like normal.
UpdateEnemies_PerEnemy:
; The first byte is if the current object is active
; If it's not zero, it's active, go to the normal update section
ld a, [hl]
and a
jp nz, UpdateEnemies_PerEnemy_Update
UpdateEnemies_SpawnNewEnemy:
; If this enemy is NOT active
; Check If we want to spawn a new enemy
ld a, [wNextEnemyXPosition]
and a
; If we don't want to spawn a new enemy, we'll skip this (deactivated) enemy
jp z, UpdateEnemies_Loop
push hl
; If they are deactivated, and we want to spawn an enemy
; activate the enemy
ld a, 1
ld [hli], a
; Put the value for our enemies x position
ld a, [wNextEnemyXPosition]
ld [hli], a
; Put the value for our enemies y position to equal 0
xor a
ld [hli], a
ld [hld], a
ld [wNextEnemyXPosition], a
pop hl
; Increase counter
ld a, [wActiveEnemyCounter]
inc a
ld [wActiveEnemyCounter], a
When We are done updating a single enemy, we’ll jump to the “UpdateEnemies_Loop” label. Here we’ll increase how many enemies we’ve updated, and end if we’ve done them all. If we still have more enemies left, we’ll increase the address stored in hl by 6 and update the next enemy.
The “hl” registers should always point to the current enemies first byte when this label is reached.
UpdateEnemies_Loop:
; Check our coutner, if it's zero
; Stop the function
ld a, [wUpdateEnemiesCounter]
inc a
ld [wUpdateEnemiesCounter], a
; Compare against the active count
cp MAX_ENEMY_COUNT
ret nc
; Increase the enemy data our address is pointingtwo
ld a, l
add PER_ENEMY_BYTES_COUNT
ld l, a
ld a, h
adc 0
ld h, a
For updating enemies, we’ll first get the enemies speed. Afterwards we’ll increase the enemies 16-bit y position. Once we’ve done that, we’ll descale the y position so we can check for collisions and draw the ennemy.
UpdateEnemies_PerEnemy_Update:
; Save our first bytye
push hl
; Get our move speed in e
ld bc, enemy_speedByte
add hl, bc
ld a, [hl]
ld e, a
; Go back to the first byte
; put the address toe the first byte back on the stack for later
pop hl
push hl
inc hl
; Get our x position
ld a, [hli]
ld b, a
ld [wCurrentEnemyX], a
; get our 16-bit y position
; increase it (by e), but also save it
ld a, [hl]
add 10
ld [hli], a
ld c, a
ld a, [hl]
adc 0
ld [hl], a
ld d, a
pop hl
; Descale the y psoition
srl d
rr c
srl d
rr c
srl d
rr c
srl d
rr c
ld a, c
ld [wCurrentEnemyY], a
Player & Bullet Collision
One of the differences between enemies and bullets is that enemies must check for collision against the player and also against bullets. For both of these cases, we’ll use a simple Axis-Aligned Bounding Box test. We’ll cover the specific logic in a later section.
If we have a collison against the player we need to damage the player, and redraw how many lives they have. In addition, it’s optional, but we’ll deactivate the enemy too when they collide with the player.
Our “hl” registers should point to the active byte of the current enemy. We push and pop our “hl” registers to make sure we get back to that same address for later logic.
UpdateEnemies_PerEnemy_CheckPlayerCollision:
push hl
call CheckCurrentEnemyAgainstBullets
call CheckEnemyPlayerCollision
pop hl
ld a, [wResult]
and a
jp z, UpdateEnemies_NoCollisionWithPlayer
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
push hl
call DamagePlayer
call DrawLives
pop hl
jp UpdateEnemies_DeActivateEnemy
If there is no collision with the player, we’ll draw the enemies. This is done just as we did the player and bullets, with the “DrawMetasprites” function.
UpdateEnemies_NoCollisionWithPlayer::
; See if our non scaled low byte is above 160
ld a, [wCurrentEnemyY]
cp 160
jp nc, UpdateEnemies_DeActivateEnemy
push hl
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; call the 'DrawMetasprites function. setup variables and call
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Save the address of the metasprite into the 'wMetaspriteAddress' variable
; Our DrawMetasprites functoin uses that variable
ld a, LOW(enemyShipMetasprite)
ld [wMetaspriteAddress+0], a
ld a, HIGH(enemyShipMetasprite)
ld [wMetaspriteAddress+1], a
; Save the x position
ld a, [wCurrentEnemyX]
ld [wMetaspriteX], a
; Save the y position
ld a, [wCurrentEnemyY]
ld [wMetaspriteY], a
; Actually call the 'DrawMetasprites function
call DrawMetasprites
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
pop hl
jp UpdateEnemies_Loop
Deactivating Enemies
Deactivating an enemy is just like with bullets. We’ll set it’s first byte to 0, and decrease our counter variable.
Here, we can just use the current address in HL. This is the second reason we wanted to keep the address of our first byte on the stack.
UpdateEnemies_DeActivateEnemy:
; Set as inactive
xor a
ld [hl], a
; Decrease counter
ld a, [wActiveEnemyCounter]
dec a
ld [wActiveEnemyCounter], a
jp UpdateEnemies_Loop
Spawning Enemies
Randomly, we want to spawn enemies. We’ll increase a counter called “wEnemyCounter”. When it reaches a preset maximum value, we’ll maybe try to spawn a new enemy.
Firstly, We need to make sure we aren’t at maximum enemy capacity, if so, we will not spawn enemy more enemies. If we are not at maximum capacity, we’ll try to get a x position to spawn the enemy at. If our x position is below 24 or above 150, we’ll also NOT spawn a new enemy.
All enemies are spawned with y position of 0, so we only need to get the x position.
If we have a valid x position, we’ll reset our spawn counter, and save that x position in the “wNextEnemyXPosition” variable. With this variable set, We’ll later activate and update a enemy that we find in the inactive state.
TryToSpawnEnemies::
; Increase our spwncounter
ld a, [wSpawnCounter]
inc a
ld [wSpawnCounter], a
; Check our spawn acounter
; Stop if it's below a given value
ld a, [wSpawnCounter]
cp ENEMY_SPAWN_DELAY_MAX
ret c
; Check our next enemy x position variable
; Stop if it's non zero
ld a, [wNextEnemyXPosition]
cp 0
ret nz
; Make sure we don't have the max amount of enmies
ld a, [wActiveEnemyCounter]
cp MAX_ENEMY_COUNT
ret nc
GetSpawnPosition:
; Generate a semi random value
call rand
; make sure it's not above 150
ld a, b
cp 150
ret nc
; make sure it's not below 24
ld a, b
cp 24
ret c
; reset our spawn counter
xor a
ld [wSpawnCounter], a
ld a, b
ld [wNextEnemyXPosition], a
ret