ZX Spectrum Assembly, Space Battle – 0x06 Enemies
In this chapter of ZX Spectrum Assembly, we will add the enemies.
We create the folder Step06, copy from Step05 loader.tap, const.asm, ctrl.asm, game.asm, graph.asm, int.asm, main.asm, print.asm, var.asm and make or make.bat.
Translation by Felipe Monge Corbalán
Tabla de contenidos
- We define enemies
- We paint the enemies
- We move the enemies
- ZX Spectrum Assembly, Space Battle
- Useful links
We define enemies
Enemies are moving objects, and as such we need to know their current and starting positions. In total we will have a maximum of twenty enemies on the screen, and we will use two bytes to specify the current position of the enemy and some other settings we need.
Open var.asm and add the enemy configuration after the frame definition.
; -------------------------------------------------------------------
; Enemy configuration
;
; 2 bytes per enemy.
; -------------------------------------------------------------------
; Byte 1 | Byte 2
; -------------------------------------------------------------------
; Bit 0-4: Y position | Bit 0-4: Position X
; Bit 5: Free | Bit 5: Free
; Bit 6: Free | Bit 6: Direction X 0 = Left 1 = Right
; Bit 7: Active 0/1 | Bit 7: Direction Y 0 = Up 1 = Down
; -------------------------------------------------------------------
enemiesConfig:
db $96, $dd, $96, $d7, $96, $d1, $96, $cb, $96, $c5
db $93, $9d, $93, $97, $93, $91, $93, $8b, $93, $85
db $90, $dd, $90, $d7, $90, $d1, $90, $cb, $90, $c5
db $8d, $9d, $8d, $97, $8d, $91, $8d, $8b, $8d, $85
enemiesConfigIni:
db $96, $dd, $96, $d7, $96, $d1, $96, $cb, $96, $c5
db $93, $9d, $93, $97, $93, $91, $93, $8b, $93, $85
db $90, $dd, $90, $d7, $90, $d1, $90, $cb, $90, $c5
db $8d, $9d, $8d, $97, $8d, $91, $8d, $8b, $8d, $85
enemiesConfigEnd:
In the first byte we have the Y coordinate, bits zero to four, and whether the enemy is active or not, bit seven. In the second byte we have the X coordinate, bits zero to four, the horizontal direction, bit six, and the vertical direction, bit seven.
Bits six and seven of the second byte contain the direction of the enemy:
- 00b $00 Left / Top
- 01b $01 Left / Bottom
- 10b $02 Right / Top
- 11b $03 Right / Down
We add the definition of the enemy graphics before enemiesConfig.
; -------------------------------------------------------------------
; Enemy graphics
;
; 00 Up-Left
; 01 Up-Right
; 10 Down-Left
; 11 Down-Right
; -------------------------------------------------------------------
enemiesGraph:
db $9f, $a0, $a1, $a2
If we look at the UDGs of the enemies, everything fits.
Going back to the enemy definition, we have twenty enemies in total, divided into four rows and five enemies per row.
The definition of the enemies from left to right and from top to bottom, remembering that we are working with inverted coordinates, is as follows:
Hexadecimal | Binary | Definition |
$96, $dd | 10010110, 11011101 | Active Line 22 Down/Right Column 29 |
$96, $d7 | 10010110, 11010111 | Active Line 22 Down/Right Column 23 |
$96, $d1 | 10010110, 11010001 | Active Line 22 Line/Right Column 17 |
$96, $cb | 10010110, 11001011 | Active Line 22 Down/Right Column 11 |
$96, $c5 | 10010110, 11000101 | Active Line 22 Down/Right Column 5 |
$93, $9d | 10010011, 10011101 | Active Line 19 Down/Left Column 29 |
$93, $97 | 10010011, 10010111 | Active Line 19 Down/Left Column 23 |
$93, $91 | 10010011, 10010001 | Active Line 19 Down/Left Column 17 |
$93, $8b | 10010011, 10001011 | Active Line 19 Down/Left Column 11 |
$93, $85 | 10010011, 10000101 | Active Line 19 Down/Left Column 5 |
90, $dd | 10010000, 11011101 | Active Line 16 Down/Right Column 29 |
$90, $d7 | 10010000, 11010111 | Active Line 16 Down/Right Column 23 |
$90, $d1 | 10010000, 11010001 | Active Line 16 Down/Right Column 17 |
90, $cb | 10010000, 11001011 | Active Line 16 Down/Right Column 11 |
$90, $c5 | 10010000, 11000101 | Active Line 16 Down/Right Column 5 |
$8d, $9d | 10001101, 10011101 | Active Line 13 Down/Left Column 29 |
$8d, $97 | 10001101, 10010111 | Active Line 13 Down/Left Column 23 |
$8d, $91 | 10001101, 10010001 | Active Line 13 Down/Left Column 17 |
$8d, $8b | 10001101, 10001011 | Active Line 13 Down/Left Column 11 |
$8d, $85 | 10001101, 10000101 | Active Line 13 Down/Left Column 5 |
Once the graphics and their configuration have been defined, we go on to paint them.
We paint the enemies
The routine that paints the enemies is in print.asm.
PrintEnemies:
ld a, $06
call Ink
ld hl, enemiesConfig
ld d, $14
We load the yellow ink in A, LD A, $06, change the ink, CALL Ink, load the address of the enemy configuration in HL, LD HL, enemiesConfig, and the number of enemies in D, LD D, $14.
printEnemies_loop:
bit $07, (hl)
jr z, printEnemies_endLoop
Check if the enemy is active, BIT $07, (HL), if not jump, JR Z, printEnemies_endLoop.
push hl
ld a, (hl)
and $1f
ld b, a
We keep the value of HL, PUSH HL, load the first byte of the enemy configuration into A, LD A, (HL), keep the Y coordinate, AND $1F, and load it into B, LD B, A.
inc hl
ld a, (hl)
and $1f
ld c, a
call At
We point HL to the second byte of the enemy configuration, INC HL, load the value into A, LD A, (HL), leave the X coordinate, AND $1F, load it into C, LD C, A, and position the cursor, CALL At.
ld a, (hl)
and $c0
rlca
rlca
ld c, a
ld b, $00
We load the second byte of the enemy configuration into A, LD A, (HL), leave the address bits, AND $c0, pass the value to bits zero and one, RLCA, RLCA, load it into C, LD C, A, and set B to zero, LD B, $00.
ld hl, enemiesGraph
add hl, bc
ld a, (hl)
rst $10
We load in HL the direction in which we define the enemy figures, LD HL, enemiesGraph, we add the direction of the enemy (left, up, etc.), ADD HL, BC, we load in A the enemy figure to be painted, LD A, (HL), and we paint it, RST $10.
pop hl
printEnemies_endLoop:
inc hl
inc hl
dec d
jr nz, printEnemies_loop
ret
We take the value of HL, POP HL, point it to the first byte of the configuration of the next enemy, INC HL, INC HL, subtract one from D, DEC D, and continue until D is zero and we have traversed all the enemies. Finally we exit, RET.
The final aspect of the routine is as follows:
; -------------------------------------------------------------------
; Paint the enemies
;
; Alters the value of the AF, BC, D and HL registers.
; -------------------------------------------------------------------
PrintEnemies:
ld a, $06 ; A = yellow ink
call Ink ; Change ink
ld hl, enemiesConfig ; HL = enemy configuration
ld d, $14 ; D = 20 enemies
printEnemies_loop:
bit $07, (hl) ; Active enemy?
jr z, printEnemies_endLoop ; If not active, skip
push hl ; Preserves HL
ld a, (hl) ; A = 1st byte value
and $1f ; A = Y coordinate
ld b, a ; B = A
inc hl ; HL = 2nd byte config
ld a, (hl) ; A = 2nd byte value
and $1f ; A = X coordinate
ld c, a ; C = A
call At ; Position cursor
ld a, (hl) ; A = 2nd byte value
and $c0 ; A = direction (left...)
rlca ; Sets value in bits 0 and 1
rlca
ld c, a ; Load the value in C
ld b, $00 ; Sets B to zero
ld hl, enemiesGraph ; HL = enemy graph
add hl, bc ; Add direction (left...)
ld a, (hl) ; A = enemy graph
rst $10 ; Paints it
pop hl ; Retrieve HL
printEnemies_endLoop:
inc hl ; HL = 1st byte configuration
inc hl ; next enemy
dec d ; D = D - 1
jr nz, printEnemies_loop ; Loop as long as D != 0
ret
To test if this works, we add the following lines to main.asm, just before Main_loop:
ld a, $01
call LoadUdgsEnemies
call PrintEnemies
We load level one into A, LD A, $01, load the level’s enemy graphics into udgsExtension, CALL LoadUdgsEnemies, and print them, CALL PrintEnemies.
We compile, load into the emulator and see the results.
We move the enemies
The enemies are not moved every iteration of the loop, as we do with the trigger, but every N interrupts. So we add another comment in the flag tag, at the top of main.asm.
; Bit 2 -> enemies must be moved 0 = No, 1 = Yes
The next thing to do is to set the boundaries of the screen as far as the enemies can go. We set them in const.asm.
; Enemies' boundaries
ENEMY_TOP_T: EQU COR_Y - MIN_Y
ENEMY_TOP_B: EQU COR_Y - MAX_Y + $01
ENEMY_TOP_L: EQU COR_X - MIN_X
ENEMY_TOP_R: EQU COR_X - MAX_X
The boundaries we have set are top, bottom, left and right.
For the enemies to move every N interrupts, and as in flags we will use a bit to indicate whether they should move or not, in the int.asm file we will activate this bit. We add the following line under SET $00, (HL):
set 02, (hl)
We are now ready to implement the routine that moves the enemies in game.asm.
MoveEnemies:
ld hl, flags
bit $02, (hl)
ret z
res 2, (hl)
We load the address of the flags into HL, LD HL, flags. We must check if the enemy movement bit is set, BIT $02, (HL), and if it is not, we exit, RET Z. If it is set, we disable it so that it does not happen in the next iteration of Main_loop, RES $02, (HL).
ld d, $14
ld hl, enemiesConfig
moveEnemies_loop:
bit $07, (hl)
jr z, moveEnemies_ endLoop
We load the number of enemies in D, LD D, $14, the address of the configuration in HL, LD HL, enemiesConfig, see if the enemy is active, BIT $07, (HL). If the enemy is not active, we jump to the next one, JR Z, moveEnemies_loopEnd.
push hl
ld a, (hl)
and $1f
ld b, a
inc hl
ld a, (hl)
and $1f
ld c, a
call DeleteChar
pop hl
We keep the value of HL, PUSH HL, load the first byte of the enemy configuration into A, LD A, (HL), keep the Y-coordinate, AND $1F, and load it into B, LD B, A.
We point HL to the second byte of the enemy configuration, INC HL, load the value into A, LD A, (HL), keep the X-coordinate, AND $1F, and load it into C, LD C, A.
Delete the enemy, CALL DeleteChar, restore HL, POP HL.
ld b, (hl)
inc hl
ld c, (hl)
We load the first byte of the enemy configuration into B, LD B, (HL), point HL to the second byte, INC HL, and load it into C, LD C, (HL).
moveEnemies_X:
ld a, c
and $1f
bit $06, c
jr nz, moveEnemies_X_right
We load the value of the second byte of the configuration into A, LD A, C, and keep the X coordinate, AND $1F.
We check the horizontal direction bit of the enemy, BIT $06, C. If it is set to one, the enemy moves to the right and jumps, JR Z, moveEnemies_X_right. If not, the enemy moves to the left.
moveEnemies_X_left:
inc a
sub ENEMY_TOP_L
jr z, moveEnemies_X_leftChg
inc c
jr moveEnemies_Y
moveEnemies_X_leftChg:
set $06, c
jr moveEnemies_Y
We increment A and so point to the column to the left of the current one, INC A, subtract the top left, SUB ENEMY_TOP_L, and if the result is zero, it has reached the top and jumps to change direction, JR Z, moveEnemies_X_leftChg.
If the direction is not to be changed, point C to the column to the left of the current column, INC C, and jump to the vertical move handle, JR moveEnemies_Y.
If the direction is to be changed, set bit six of C to set the horizontal direction to the right, SET $06, C, and jump to the vertical move handle, JR moveEnemies_Y.
If the enemy does not move to the left, it will move to the right.
moveEnemies_X_right:
dec a
sub ENEMY_TOP_R
jr z, moveEnemies_X_rightChg
dec c
jr moveEnemies_Y
moveEnemies_X_rightChg:
res $06, c
We decrement A to point to the column to the right of the current one, DEC A, subtract the right top, SUB ENEMY_TOP_R, and if the result is zero, it has reached the top and jumps to change direction, JR Z, moveEnemies_X_rightChg.
If the direction is not to be changed, point C to the right column, DEC C, and jump to vertical movement, JR moveEnemies_Y.
If you want to change the direction, turn off bit six of C to set the horizontal direction to the left, RES $06, C, and start the vertical movement.
moveEnemies_Y:
ld a, b
and $1f
bit $07, c
jr nz, moveEnemies_Y_down
We load the value of the first byte of the configuration into A, LD A, B, and leave the Y coordinate, AND $1F.
We check bit seven of C to get the vertical direction, BIT $07, C, and if it is one, the enemy moves down, JR NZ, moveEnemies_Y_down, and jumps.
If the bit is zero, the enemy moves up.
moveEnemies_Y_up:
inc a
sub ENEMY_TOP_T
jr z, moveEnemies_Y_upChg
inc b
jr moveEnemies_endMove
moveEnemies_Y_upChg:
set $07, c
jr moveEnemies_endMove
We increment A to point to the line above the current line, INC A, subtract the top, SUB ENEMY_TOP_T, and if it is zero, we have reached the top and must jump, JR Z, moveEnemies_Y_upChg, to change direction.
If we have not reached the top, we increment B to point to the line above the current line, INC B, and jump to the end of the loop, JR moveEnemies_endMove.
If the direction is to be changed, we set bit seven of C to change the direction down, SET $07, C, and jump to the end of the loop, JR moveEnemies_endMove.
If the enemy does not move up, it moves down.
moveEnemies_Y_down:
dec a
sub ENEMY_TOP_B
jr z, moveEnemies_Y_downChg
dec b
jr moveEnemies_endMove
moveEnemies_Y_downChg:
res $07, c
We decrement A to point to the line below the current one, DEC A, subtract the top from the bottom, SUB ENEMY_TOP_B, and if it is zero, we have reached the top and jump because we have to change direction, JR Z, moveEnemies_Y_downChg.
If we have not reached the top, we decrement B to point to the line below the current line, DEC B, and jump to the end of the loop, JR moveEnemies_endMove.
If we want to change the direction, we disable bit seven of C and increment the direction, RES $07, C.
moveEnemies_endMove:
ld (hl), c
dec hl
ld (hl), b
moveEnemies_endLoop:
inc hl
inc hl
dec d
jr nz, moveEnemies_loop
We update in memory the second byte of the configuration, LD(HL), C, point HL to the first byte, DEC HL, and update in memory, LD(HL), B.
We point HL to the first byte of the next enemy configuration, INC HL, INC HL, decrement D, DEC D, and continue in the loop until D is zero and we have traversed all twenty enemies, JR Z, moveEnemies_loop.
moveEnemies_end:
call PrintEnemies
ret
We print the enemies with the new positions, CALL PrintEnemies, and exit with RET.
We have implemented the routine that moves the enemies, whose final appearance is as follows:
; -------------------------------------------------------------------
; Moves enemies.
;
; Alters the value of the AF, BC, D and HL registers.
; -------------------------------------------------------------------
MoveEnemies:
ld hl, flags ; HL = address flags
bit $02, (hl) ; Bit 2 active?
ret z ; Not active, exits
res $02, (hl) ; Disables bit 2
ld d, $14 ; D = number of enemies (20)
ld hl, enemiesConfig ; HL = configuration address
moveEnemies_loop:
bit $07, (hl) ; Active enemy?
jr z, moveEnemies_endLoop ; Not active, jumps
push hl ; Preserves HL
ld a, (hl) ; A = value 1st byte config
and $1f ; A = Y coordinate
ld b, a ; B = A
inc hl ; HL = 2nd byte config
ld a, (hl) ; A = value 2nd byte config
and $1f ; A = X coordinate
ld c, a ; C = A
call DeleteChar ; Deletes enemy
pop hl ; Retrieve HL
ld b, (hl) ; B = value 1st byte config
inc hl ; HL = 2nd byte config
ld c, (hl) ; C = 2nd byte config value
moveEnemies_X:
ld a, c ; A = C
and $1f ; A = X coordinate
bit $06, c ; Evaluates horizontal direction
jr nz, moveEnemies_X_right ; != 0, right, jump
moveEnemies_X_left:
inc a ; A = previous column
sub ENEMY_TOP_L ; A = A - left stop
jr z, moveEnemies_X_leftChg ; = 0, stop has been reached, skip
inc c ; C = previous column
jr moveEnemies_Y ; Jump to vertical movement
moveEnemies_X_leftChg:
set $06, c ; Horizontal dir = right
jr moveEnemies_Y ; Jump to vertical movement
moveEnemies_X_right:
dec a ; A = back column
sub ENEMY_TOP_R ; A = A - right stop
jr z, moveEnemies_X_rightChg ; = 0, has reached stop, skip
dec c ; C = back column
jr moveEnemies_Y ; Jump to vertical movement
moveEnemies_X_rightChg:
res $06, c ; Horizontal dir = left
moveEnemies_Y:
ld a, b ; A = first byte config
and $1f ; A = Y coordinate
bit $07, c ; Evaluates vertical direction
jr nz, moveEnemies_Y_down ; != 0, downwards, jump
moveEnemies_Y_up:
inc a ; A = previous line
sub ENEMY_TOP_T ; A = A - top top
jr z, moveEnemies_Y_upChg ; = 0, stop has been reached, skip
inc b ; B = back line
jr moveEnemies_endMove ; Jump to end loop
moveEnemies_Y_upChg:
set $07, c ; Vertical dir = down
jr moveEnemies_endMove ; Jump to end loop
moveEnemies_Y_down:
dec a ; A = back line
sub ENEMY_TOP_B ; A = A - stop below
jr z, moveEnemies_Y_downChg ; = 0, has arrived, jump
dec b ; Aim B at the back line
jr moveEnemies_endMove ; Jumps to the end of the loop
moveEnemies_Y_downChg:
res $07, c ; Vertical dir = top
moveEnemies_endMove:
ld (hl), c ; Update 2nd byte config
dec hl ; HL = 1st byte config
ld (hl), b ; Update 1st byte config
moveEnemies_endLoop:
inc hl
inc hl ; HL 1st byte config
; next enemy
dec d ; D = D - 1
jr nz, moveEnemies_loop ; Until D = 0 (20 enemies)
moveEnemies_end:
call PrintEnemies ; Paint enemies
ret
Now it’s time to see how the enemies move. Open main.asm and in the Main_loop tag, just below CALL MoveShip, add the following line:
call MoveEnemies
We compile, load in the emulator and see the results.
How’s it going? Are the enemies moving? Yes, they’re moving, but they’re moving too fast and the firing has slowed down. We need to slow down the movement of the enemies, and we’re going to do that from the interrupt routine, so let’s go to the int.asm file.
We are going to do something similar to what we did in ZX-Pong, we are going to add a counter at the end of the file to control when we activate the movement of the enemies.
countEnemy: db $00
Between the lines SET $00, (HL) and SET $02, (HL) we now implement the use of this counter.
ld a, (countEnemy)
inc a
ld (countEnemy), a
sub $02
jr nz, Isr_end
ld (countEnemy), a
We load the value of the counter into A, LD A, (countEnemy), increment A, INC A, and update the counter, LD (countEnemy), A, in memory. We subtract from A the value that the counter must reach to activate the movement, SUB $03, and if it has not reached it, we jump to the end of the routine, JR NZ, Isr_end.
If it has reached the value, we set it to zero, LD (countEnemy), A, and set the bit to move the enemies, SET $02, (HL).
Compile and load into the emulator. We have regained the firing speed and the enemies are still moving fast.
ZX Spectrum Assembly, Space Battle
We have all the elements of the game in motion.
In the next chapter of ZX Spectrum Assembly, we will include the collisions of the shot with the enemies, the enemies with the ship and the level changes.
Download the source code from here.
Useful links
ZX Spectrum Assembly, Space Battle by Juan Antonio Rubio García.
Translation by Felipe Monge Corbalán.
This work is licensed to Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Any comments are always welcome.