ZX Spectrum Assembly, Space Battle – 0x04 Ship
In this chapter of ZX Spectrum Assembly, we will implement the ship, its movement and therefore the controls.
Create the folder Step04 and copy the files const.asm, graph.asm, main.asm, print.asm and var.asm from Step03.
Translation by Felipe Monge Corbalán
Table of contents
- On-screen positioning
- We paint the ship
- We move the ship
- ZX Spectrum Assembly, Space Battle
- Useful links
On-screen positioning
So far we have been using the AT control character and coordinates to position ourselves on the screen, but this is slow.
We will open graph.asm and at the top of the file we will implement a routine that does the same thing, but is faster.
; -------------------------------------------------------------------
; Position the cursor at the specified coordinates.
;
; Input: B = Y-coordinate (24 to 3).
; C = X-coordinate (32 to 1).
; Alters the value of the AF and BC registers.
; -------------------------------------------------------------------
At:
push de ; Preserves DE
push hl ; Preserves HL
call $0a23 ; Call ROM routine
pop hl ; Retrieve HL
pop de ; Retrieve DE
ret
In this routine we use the ROM routine that positions the cursor. We store the value of DE, PUSH DE, and the value of HL, PUSH HL. Once this is done, we call the ROM routine, CALL $0A23, and retrieve the value of HL, POP HL, and also the value of DE, POP DE. Finally we exit, RET.
In the comments you can see that the routine also changes the value of AF and BC, but we don’t keep them; we won’t be affected by that, and so we save two PUSH and two POP.
Another thing to note, a hint is given in the comments, is that for the ROM routine the upper left corner is at coordinates Y=24 and X=32, so we will work with the coordinates inverted with respect to the AT instruction.
Open the file const.asm and add the coordinate constants.
; Screen coordinates for ROM routine positioning the cursor
; The play area (the frame).
COR_X: EQU $20 ; X-coordinate of the upper left corner
COR_Y: EQU $18 ; Y-coordinate of upper left corner
MIN_X: EQU $00 ; To be subtracted from COR_X for X upper left corner
MIN_Y: EQU $00 ; To be subtracted from COR_Y for Y upper left corner
MAX_X: EQU $1f ; To be subtracted from COR_X for X bottom right corner
MAX_Y: EQU $15 ; To be subtracted from COR_Y for Y lower right corner
The EQU directive does not compile, it does not expand the binary; it replaces the tag with its value wherever it is found.
We paint the ship
The first thing we’re going to do is place the ship in our game area; it will move from left to right at the bottom of the game area.
Since the ship is a mobile element, we need to know its current and initial position, as we saw with the paddles in ZX-Pong.
We open the var.asm file and add the following lines after the game information title declarations:
; -------------------------------------------------------------------
; Statements of the graphics of the different characters
; and the coordinate configuration (Y, X)
; -------------------------------------------------------------------
; -------------------------------------------------------------------
; Nave
; -------------------------------------------------------------------
shipPos:
dw $0511
In the case of the ship, we only define the position, DW $0511, a two-byte value, first the Y-coordinate and then the X-coordinate, which we will load into BC during the game to position the ship. The position $0511 is the result of subtracting 19 ($13) and 15 ($0f) from the upper left corner used by the ROM routine ($1820).
We open const.asm and add constants for the ship character, the start position and the left and right stops.
; Character code of the ship, starting position and buffers
SHIP_GRAPH: EQU $90
SHIP_INI: EQU $0511
SHIP_TOP_L: EQU $1e
SHIP_TOP_R: EQU $01
To paint the correct colours, and to avoid repeating the code, we implement a routine to change the ink colour in the graph.asm file. This routine receives the value of the colour in A.
Before implementing the routine, we add the constant memory location where the current colour attributes are stored. These attributes are those used by RST $10 to assign the colour to the character it is drawing.
; System variable containing current colour attributes
ATTR_T: EQU $5c8f
We open the graph.asm file and implement the Ink routine.
; -------------------------------------------------------------------
; Change the ink
;
; Input: A -> Ink colour
; Alters the value of the A register.
; -------------------------------------------------------------------
Ink:
exx ; Preserves BC, DE and HL
ld b, a ; B = ink
ld a, (ATTR_T) ; A = current attributes
and $f8 ; A = Ink 0
or b ; A = Ink received
ld (ATTR_T), a ; Current attributes = A
exx ; Retrieves BC, DE and HL
ret
If we need to rely on the B register, the first thing we do is to preserve its value with the EXX instruction, which exchanges the value of the BC, DE and HL registers with the alternative registers ‘BC, ‘DE and ‘HL, with only one byte and four clock cycles, which is faster and occupies less than if we used the stack.
We load the value of the colour into B, LD B, A, load the current attributes into A, LD A, (ATTR_T), discard the colour, AND $F8, add the colour, OR B, and load it into the current attributes, LD (ATTR_T), A. Finally, we retrieve the value of the BC, DE and HL registers, EXX, and exit, RET.
We need a routine to paint the ship, which we implement in the file print.asm.
PrintShip:
ld a, $07
call Ink
We load the white ink, LD A, $07, into A and call CALL INK to change it.
ld bc, (shipPos)
call At
We load the position of the ship into BC, LD BC, (shipPos), and position the cursor, CALL At.
ld a, SHIP_GRAPH
rst $10
ret
We load the ship character in A, LD A, SHIP_GRAPH, paint it, RST $10, and exit, RET.
We already have the routine that paints the ship, which looks like this:
; -------------------------------------------------------------------
; Paints the ship at the current position.
; Alters the value of the A and BC registers.
; -------------------------------------------------------------------
PrintShip:
ld a, $07 ; A = white ink
call Ink ; Change ink
ld bc, (shipPos) ; BC = ship position
call At ; Position cursor
ld a, SHIP_GRAPH ; A = ship character
rst $10 ; Pinta ship
ret
Before we leave the Print.asm file, we return to the routine that draws the frame, specifically the part where we create the loop that draws the sides.
printFrame_loop:
ld a, $16 ; A = control character AT
rst $10 ; Paints it
ld a, b ; A = line
rst $10 ; Paints it
ld a, $00 ; A = column
rst $10 ; Paints it
ld a, $99 ; A = left lateral character
rst $10 ; Paints it
ld a, $16 ; A = control character AT
rst $10 ; Paints it
ld a, b ; A = line
rst $10 ; Paints it
ld a, $1f ; A = column
rst $10 ; Paints it
ld a, $9a ; A = right-hand side character
rst $10 ; Paints it
inc b ; B = next line
ld a, b ; A = B
cp $14 ; A = 20?
jr nz, printFrame_loop ; A != 20, continue with loop
We see that the six lines following printFrame_loop position the cursor to paint on the left side, then six lines following printFrame_loop do the same for the right side.
I’m using Visual Studio Code with the Z80 Assembly meter extension, so I know that this routine, from printFrame_loop to JR NZ, printFrame_loop, consumes one hundred and sixty-five clock cycles and twenty-eight bytes.
Since we’ve already implemented a routine that positions the cursor, we’ve just implemented At, so let’s replace these lines with calls to that routine.
Above printFrame_loop we modify the LD B line, $01, and leave it as follows:
ld b, COR_Y - $01
Remember that the ROM routine works with inverted coordinates. We point B to line one, LD B, COR_Y – $01.
We delete the first six lines below printFrame_loop and replace them with the following:
ld c, COR_X - MIN_X
call At
A few lines down, we delete from LD A, $16 to RST $10 just above LD A, $9A, and replace these lines with the following:
ld c, COR_X - MAX_X
call At
Replacement from INC B to JR NZ, printFrame_loop.
dec b
ld a, COR_Y - MAX_Y + $01
sub b
jr nz, printFrame_loop
The final appearance of the modified part is as follows:
ld b, COR_Y - $01 ; B = line 1
printFrame_loop:
ld c, COR_X - MIN_X ; C = column 0
call At ; Position cursor
ld a, $99 ; A = left lateral character
rst $10 ; Paints it
ld c, COR_X - MAX_X ; C = column 31
call At ; Position cursor
ld a, $9a ; A = right-hand side character
rst $10 ; Paints it
dec b ; B = B - 1
ld a, COR_Y - MAX_Y + $01 ; A = line 20
sub b ; Subtract next line
jr nz, printFrame_loop ; A - B != 0, continue loop
At first glance, the routine is shorter, consuming twenty-two bytes and one hundred and eleven clock cycles, but beware, all that glitters is not gold, to these clock cycles must be added the clock cycles of the At routine, which are sixty-nine (the bytes are not added because we are going to use the routine from more places).
When all the values are added up, the routine takes twenty-two bytes and each iteration of the loop consumes one hundred and eighty clock cycles, fifteen more than the previous implementation, although we have saved six bytes. In addition, the ROM routine takes less time than positioning using the AT control code.
What do we do now? How do we leave it?
Since the routine that paints the border is not critical, as we only paint it at the beginning of each level, and four clock cycles will not be noticed, we decided to keep the six-byte saving and stick with the new implementation.
We just need to paint the ship, we go to main.asm and just below CALL PrintInfoGame we add the call to paint the ship.
call PrintShip
We compile, load the emulator and see the results.

We move the ship
The ship must move in response to some action by the player, in our case by pressing three keys: Z to move the ship left, X to move the ship right and V to shoot.
We create the ctrl.asm file and implement the routine that reads the keyboard and returns the control keys pressed.
The routine we are going to implement returns the pressed keys in register D, similar to what was done in ZX-Pong; it sets bit zero to one if the Z key was pressed, bit one if the X key was pressed and bit two if the V key was pressed.
CheckCtrl:
ld d, $00
ld a, $fe
in a, ($fe)
We set D to zero, LD D, $00, load the Cs-V half-stack into A, LD A, $FE, and read the keyboard, IN A, ($FE).
checkCtrl_fire:
bit $04, a
jr nz, checkCtrl_left
set $02, d
We check if the V key has been pressed, BIT $04, A. If it has not, we move on to check if the X key has been pressed, JR NZ, checkCtrl_left. If it was pressed, we set bit two of register D to one to indicate that the trigger was pressed, SET $02, D.
When we read from the keyboard, the status of the keys of the half stack read is in register A, with one for the keys that have not been pressed and zero for the keys that have been pressed (bit zero refers to the key furthest from the centre of the keyboard and four to the nearest).
The BIT instruction evaluates the status of the specified bit, $04, of the specified register, A, and, depending on whether it is set to zero or one, activates or deactivates the Z flag. The SET instruction sets the specified bit, $02, of the specified register, D, to one. The RES instruction is the opposite of SET, it sets the bit to zero. RES and SET do not affect the F register.
checkCtrl_left:
bit $01, a
jr nz, checkCtrl_right
set $00, d
We check if the Z key has been pressed, BIT $01, A. If it has not, we move on to check if the X key has been pressed, JR NZ, checkCtrl_right. If it was pressed, we set the zero bit of the D register to one to indicate that the left key was pressed, SET $00, D.
checkCtrl_right:
bit $02, a
ret nz
set $01, d
We check if the X key has been pressed, BIT $02, A. If it has not been pressed, we exit, RET NZ. If it was pressed, we set bit one of register D to one to indicate that it was pressed correctly, SET $01, D.
checkCtrl_testLR:
ld a, d
and $03
sub $03
ret nz
ld a, d
and $04
ld d, a
checkCtrl_end:
ret
Finally, we check if left and right are pressed at the same time, in which case we disable both.
We load A with the value of D, LD A, D, we leave the value of the bits zero and one, AND $03, and we subtract three, SUB $03. If the result is not zero, we exit, RET NZ, because the two bits weren’t set to one and we don’t have to do anything.
If we have not exited, we load the value of D back into A, LD A, D, keeping only the value of bit two (trigger), AND $04, and load the value into D, LD D, A, thus disabling the simultaneous left and right keystrokes. Finally we exit, RET.
The last tag, checkCtrl_end, is not necessary, although we include it to make it clear where the routine ends.
The final aspect of the routine is as follows:
; -------------------------------------------------------------------
; Evaluates whether any of the arrow keys have been pressed.
; The arrow keys are:
; Z -> Left
; X -> Right
; V -> Shot
;
; Return: D -> Keys pressed.
; Bit 0 -> Left
; Bit 1 -> Right
; Bit 2 -> Shot
;
; Alters the value of the registers A and D
; -------------------------------------------------------------------
CheckCtrl:
ld d, $00 ; D = 0
ld a, $fe ; A = half-stack Cs-V
in a, ($fe) ; Read keyboard
checkCtrl_fire:
bit $04, a ; V pressed?
jr nz, checkCtrl_left ; Not pressed, skip
set $02, d ; Set bit 2 of D
checkCtrl_left:
bit $01, a ; Z pressed?
jr nz, checkCtrl_right ; Not pressed, skip
set $00, d ; Set bit 0 of D
checkCtrl_right:
bit $02, a ; X pressed?
ret nz ; Not pressed, exits
set $01, d ; Set bit 1 of D
checkCtrl_testLR:
ld a, d ; A = D
and $03 ; Keeps bits 0 and 1
sub $03 ; Active both bits?
ret nz ; Both not active (!=0), outputs
ld a, d ; A = D
and $04 ; Keeps bit 2
ld d, a ; D = A
checkCtrl_end:
ret
We open main.asm and in each iteration of the Main_loop we will call the routine we have just implemented. Just below the Main_loop tag, we add the following line:
call CheckCtrl
Below, in the part where we have the includes, we add the include for the ctrl.asm file.
include "ctrl.asm"
We compile and check that it compiles well; there are no errors.
When the ship is moved, we first erase it from the current position and repaint it at the new position. In const.asm, we add the following constant to the constants we defined for the ship:
; Character code of the blank character
WHITE_GRAPH: EQU $9e
We open print.asm and at the top we implement the routine that deletes the ship. As this routine will also be used to delete the enemies and the shot, it will be given the coordinates of the character we want to delete in BC.
DeleteChar:
call At
As DeleteChar receives the coordinates of the character to be deleted in BC, the first step is to position the cursor, CALL At.
ld a, WHITE_GRAPH
rst $10
ret
We load the character in bank A, LD A, WHITE_GRAPH, and paint it, RST $10, erasing the painted character at these coordinates.
The final aspect of the routine is as follows:
; -------------------------------------------------------------------
; Deletes a character from the display
;
; Input: BC -> Y/X coordinates of the character
; Alters the value of the AF registers
; -------------------------------------------------------------------
DeleteChar:
call At ; Position cursor
ld a, WHITE_GRAPH ; A = white character
rst $10 ; Paints it and erases the ship
ret
Open main.asm and add the following lines just below CALL CheckCtrl to see if it works:
ld bc, (shipPos)
call DeleteChar
call PrintShip
With these lines we erase and paint the ship in each iteration of Main_loop. If we compile and load in the emulator, we will see that the ship is blinking constantly, a sign that the erasing is working.
Let’s move the ship. We create a new file, game.asm, and implement the routine that changes the position of the ship and paints it. The routine gets the state of the controls in the D register (first we add the include from game.asm to main.asm).
MoveShip:
ld bc, (shipPos)
bit $01, d
jr nz, moveShip_right
We load the ship’s position into BC, LD BC, (shipPos), check if the right-hand control is pressed, BIT $01, D, and if so we jump to the part that controls the right movement, JR NZ, moveShip_right.
bit $00, d
ret z
We check that the left-hand control has been pressed, BIT $00, D, and if it has not, we quit, RET Z.
moveShip_left:
ld a, SHIP_TOP_L + $01
sub c
ret z
call DeleteChar
inc c
ld (shipPos), bc
jr moveShip_print
If the left-hand control was pressed, we check if we can move the ship. We load into A the stop at which the ship can move to the left, LD A, SHIP_TOP_L + $01, and subtract the current column from the ship’s position, SUB C. If the result is zero, the ship is already at the stop and cannot move to the right, so we exit, RET Z.
If we don’t exit, we delete the ship, CALL DeleteChar, point C to the column just to the left, INC C, update the new position of the ship in memory, LD (shipPos), BC, and jump to the end of the routine, JR moveShip_print.
The routine that controls the movement of the ship to the right is almost the same as the one that controls the movement to the left, so we will only highlight and explain the changes.
moveShip_right:
ld a, SHIP_TOP_R + $01 ; Change!
sub c
ret z
call DeleteChar
dec c ; Change!
ld (shipPos), bc
moveShip_print:
call PrintShip
ret
In A we load the stop to which the ship can move to the right, LD A, SHIP_TOP_R + 01. In C we load the column to the right of the current position, DEC C. Finally, we paint the ship, CALL PrintShip.
The final aspect of the routine is as follows:
; -------------------------------------------------------------------
; Move the ship
;
; Input: D -> Controls status
; Alters the value of the AF and BC registers.
; -------------------------------------------------------------------
MoveShip:
ld bc, (shipPos) ; BC = ship position
bit $01, d ; Go right?
jr nz, moveShip_right ; If it goes right, jump
bit $00, d ; Go left?
ret z ; Does not go left, exits
moveShip_left:
ld a, SHIP_TOP_L + $01 ; A = stop for left-hand ship
sub c ; A = A - C (column ship)
ret z ; A = C? Yes, it comes out
call DeleteChar ; Delete ship
inc c ; C = left column of current
ld (shipPos), bc ; Update ship position
jr moveShip_print ; Jump to the end of the routine
moveShip_right:
ld a, SHIP_TOP_R + $01 ; A = stop for right-hand ship
sub c ; A = A - C (column ship)
ret z ; A = C? Yes, it comes out
call DeleteChar ; Delete ship
dec c ; C = right column of current
ld (shipPos), bc ; Update ship position
moveShip_print:
call PrintShip ; Paints ship
ret
Before continuing, remember that I commented earlier that At changed the value of the BC and AF registers, but did not affect us. Now that At is called from multiple locations, the change to BC does affect us. The solution is as simple as adding PUSH BC and POP BC to At to preserve and restore the value of BC.
However, we will do another implementation that saves bytes and clock cycles.
; -------------------------------------------------------------------
; Position the cursor at the specified coordinates.
;
; Input: B = Y-coordinate (24 to 3).
; C = X-coordinate (32 to 1).
; Alters the value of the AF register
; -------------------------------------------------------------------
At:
push bc ; We preserve the value of BC
exx ; We preserve the value of BC, DE and HL
pop bc ; Retrieve the value of BC
call $0a23 ; Call the ROM routine
exx ; Retrieve the value of BC, DE and HL
ret
We keep the value of BC where the coordinates are, PUSH BC, keep the value of BC, DE and HL by swapping them with the alternative registers, EXX, retrieve BC (coordinates), POP BC, and call the ROM routine that positions the cursor, CALL $0A23.
At this point the values of BC, DE and HL have changed, we retrieve them from the alternate registers, EXX, and exit, RET.
The routine now occupies eight bytes and takes fifty-six clock cycles to execute, compared to ten bytes and ninety clock cycles if we use the stack for the three registers.
It’s time to see if the ship is moving. We go back to main.asm and replace the lines we added earlier:
ld bc, (shipPos)
call DeleteShip
call PrintShip
by:
call MoveShip
We compile, load the emulator and see the results.

The ship already moves left and right, but we have the same problem we had in ZX-Pong, it moves very fast, faster than the ZX-Pong paddles, because they moved pixel by pixel and the ship moves character by character.
We could solve it in the same way as we did then, by not moving the ship in each iteration of the loop, but since we are going to use the interrupts for other things, we are going to do it through them, and so we see something we didn’t see in ZX-Pong.
ZX Spectrum Assembly, Space Battle
In the next chapter of ZX Spectrum Assembly, we will implement interrupts; if you want to know more about them, read the chapter dedicated to them in the Compiler Software course, and how to implement them in 16K.
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.