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