Espamática
ZX SpectrumRetroZ80 Assembly

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

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.

ASM
; -------------------------------------------------------------------
; 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.

ASM
; 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:

ASM
; -------------------------------------------------------------------
; 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.

ASM
; 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.

ASM
; System variable containing current colour attributes
ATTR_T: EQU $5c8f

We open the graph.asm file and implement the Ink routine.

ASM
; -------------------------------------------------------------------
; 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.

ASM
PrintShip:
ld   a, $07
call Ink

We load the white ink, LD A, $07, into A and call CALL INK to change it.

ASM
ld   bc, (shipPos)
call At

We load the position of the ship into BC, LD BC, (shipPos), and position the cursor, CALL At.

ASM
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:

ASM
; -------------------------------------------------------------------
; 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.

ASM
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:

ASM
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:

ASM
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:

ASM
ld   c, COR_X - MAX_X
call At

Replacement from INC B to JR NZ, printFrame_loop.

ASM
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:

ASM
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.

ASM
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.

ASM
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).

ASM
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.

ASM
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.

ASM
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.

ASM
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:

ASM
; -------------------------------------------------------------------
; 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:

ASM
call CheckCtrl

Below, in the part where we have the includes, we add the include for the ctrl.asm file.

ASM
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:

ASM
; 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.

ASM
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.

ASM
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:

ASM
; -------------------------------------------------------------------
; 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:

ASM
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).

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.

ASM
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.

ASM
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.

ASM
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:

ASM
; -------------------------------------------------------------------
; 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.

ASM
; -------------------------------------------------------------------
; 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:

ASM
ld   bc, (shipPos)
call DeleteShip
call PrintShip

by:

ASM
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.

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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies

Descubre más desde Espamática

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo