Espamática
ZX SpectrumRetroZ80 Assembly

ZX Spectrum Assembly, Space Battle – 0x05 Interruptions and shot

In this 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.

The ZX Spectrum generates a total of fifty interrupts per second on PAL systems, sixty on NTSC systems.

Create the folder Step05 and copy the files const.asm, ctrl.asm, game.asm, graph.asm, main.asm, print.asm and var.asm from the folder Step04.

Before we continue, let’s look at the size of the program, which is about 1600 bytes.

Translation by Felipe Monge Corbalán

Interruptions

The routine that is executed when an interrupt is generated is implemented in the int.asm file, so we create it.

Following the Compiler Software course, we will load $28 (40) into register I and our routine at address $7e5c (32348), giving us four hundred and nineteen bytes for the routine. Since the program loads at $5dad (23981), we have eight thousand three hundred and sixty-seven bytes for our game.

We open main.asm and before Main_loop we add the lines that prepare the interrupts.

ASM
di
ld   a, $28
ld   i, a
im   2
ei

We disable interrupts, DI, load $28 (40) into register A, LD A, $28, and load I, LD I, A. We change the interrupt mode to mode two, IM 2, and enable interrupts, EI.

We are going to implement the routine in the int.asm file, so open it and add the following lines:

ASM
org  $7e5c

Isr:
push hl
push de
push bc
push af

We load the Isr routine at address $7E5C (32348), ORG $7E5C and preserve HL, DE, BC and AF, PUSH HL, PUSH DE, PUSH BC, PUSH AF.

We do nothing more for the moment and leave.

ASM
Isr_end:
pop  af
pop  bc
pop  de
pop  hl
ei
reti

Get AF, BC, DE and HL, POP AF, POP BC, POP DE, POP HL, enable interrupts, EI, and exit, RETI.

Now we go to main.asm and just before END Main we include the file int.asm.

Compile, load into the emulator and test. Nothing seems to happen, but look at how much space our program now occupies. It takes up a whopping nine thousand bytes. Because we’ve loaded the routine at address $7E5C, PASMO fills all the space from where the program ended before to where it ends now with zeros, that’s why it takes up so much space. Normally this doesn’t matter when loading on an emulator, we can load it immediately, but on a ZX Spectrum we are adding unnecessary loading time.

We compile into multiple files

To prevent our program from growing like this, we will compile the int.asm file separately from the rest. We will also dispense with the loader generated by PASMO and make our own.

Creation of the loader

From the emulator we will generate a BASIC loader and save it as loader.tap. The code of the loader looks like this:

VB
10 CLEAR 23980
20 LOAD ""CODE 
30 LOAD ""CODE 32348
40 RANDOMIZE USR 23981

We save our loader with the following instruction:

VB
SAVE "SPACEWAR" LINE 10

When the program is loaded, line ten is auto-executed.

Compilation in several files

We will compile the int.asm file separately, which will generate the int.tap file, and the rest of the program will generate martian.tap. Then we will concatenate the loader.tap, martian.tap and int.tap files into SpaceBattle.tap.

Since it is tedious to do this every time we want to compile and see the results, we will create a script. For those of you using Windows, create the file make.bat in the Paso05 folder, for those of you using Linux, run it from the command line:

ShellScript
touch make

Then grant permissions to run.

ShellScript
chmod +x make

Before we continue, open the main.asm file and delete the include of the int.asm file near the end.

We can now edit make or make.bat. On Windows and Linux, the first two lines are the same.

ShellScript
pasmo --name Martian --tap main.asm martian.tap martian.log
pasmo --name Int --tap int.asm int.tap int.log

First we compile the programme, then we compile the Int.asm file and generate the int.tap file. Note that instead of –tapbas we put –tap because we did the BASIC loader by hand.

Finally, we combine loader.tap, martian.tap and int.tap into SpaceBattle.tap.

For Linux users, add this line to the end of your make file:

ShellScript
cat loader.tap martian.tap int.tap > SpaceBattle.tap

For those of you using Windows, the line is as follows:

ShellScript
copy /b loader.tap+martian.tap+int.tap SpaceBattle.tap

From here, the way to compile will be to run make or make.bat, and in the emulator we will load the file SpaceBattle.tap.

We compile, load in the emulator and see that everything is the same, but the size of SpaceBattle.tap is less than two thousand bytes.

We slow down the ship

We saw in the previous chapter that the ship was moving too fast. To solve this, we will use the interrupts to move the ship a maximum of fifty times per second (on PAL systems, sixty on NTSC), i.e. we will move the ship when the interruption is triggered. We open var.asm and add the following at the top:

ASM
; -------------------------------------------------------------------
; Indicators
;
; Bit 0 -> ship must be moved 0 = No, 1 = Yes
; -------------------------------------------------------------------
flags:
db $00

In int.asm, add the following lines after PUSH AF:

ASM
ld   hl, flags
set  00, (hl)

We load the flags memory address into HL, LD HL, flags, and set the zero bit to one, SET $00, (HL).

Now go to the game.asm file and add the following lines just below the MoveShip tag:

ASM
ld   hl, flags             ; HL = address flags
bit  $00, (hl)             ; Check bit 0
ret  z                     ; Bit 0 = 0? Yes, it comes out 
res  $00, (hl)             ; Bit 0 = 0

We load the flags memory address into HL, LD HL, flags, we check if we need to move the ship, BIT $00, (HL), and if not we exit, RET Z. If we do need to move the ship we set the bit to zero, SET $00, (HL), this way we will not move the ship again until an interrupt occurs and we set the zero bit of flags back to one.

This will make our ship move fifty times per second (or sixty, depending on the system). We compile and…

Actually, we have a compilation error.

ERROR on line N of file Int.asm
ERROR: Symbol ‘flags’ is not defined

So far we have included all the .asm files we have in main.asm, but we have removed the include from int.asm to compile it separately, so the flags tag is not known in int.asm. The solution is simple, in int.asm we need to replace LD HL, flags with LD HL, memoryaddress.

Let’s have a look at the line we used to compile.

ShellScript
pasmo --name Martian --tap main.asm martian.tap martian.log

The last parameter, martian.log, creates a file of that name in which we can see what memory address the tags are at. In this case, flags is at memory address $5EB5, so just go to int.asm and replace LD HL, flags with LD HL, $5EB5.

Now yes, we compile, load in the emulator and see that the ship moves slower, but we still have a problem; put a NOP at the beginning of main.asm, just before the Main tag.

Compile and you will see that it works fine. Load it into the emulator and it stopped working. If you go to martian.log, you will see that the flags tag is at address $5EB6, but in the int.asm file the value loaded in HL is $5EB5. Every time we change some code, it is very likely that the flags address will change, so we need to make sure that we change it in int.asm as well.

To prevent the flags address from changing, we open var.asm, cut the flags declaration and paste it at the top of main.asm, below ORG $5DAD. This ensures that flags is always located at $5DAD. Don’t forget to replace $5EB5 with $5DAD in int.asm.

We compile, load it into the emulator and everything works, but this is because we initialised flags to zero, which is equivalent to NOP, which takes four clock cycles to execute, nothing more. If we had started it with a different value, say $C9, it would return to BASIC on execution because $C9 is RET. Try it and see.

This has an easy solution, before the flags tag we add JR Main, but as we only need the flags tag and it is zero, there is no problem, but be careful with this.

It is not necessary to add JR Main, but if you do, note that the flags address changes.

We implement the shot

As with the ship, we include the constants needed for the shot in const.asm.

ASM
; Shot character code and top
FIRE_GRAPH: EQU $91
FIRE_TOP_T: EQU COR_Y

Also in var.asm we add the tag to store the shot position.

ASM
; -------------------------------------------------------------------
; Shot
; -------------------------------------------------------------------
firePos:
dw $0000

In print.asm we will implement the routine that paints the shot.

ASM
PrintFire:
ld   a, $02
call Ink

We load red ink in A, LD A, $02, and call change, CALL Ink.

ASM
ld   bc, (firePos) 
call At

We load into BC the position of the shot, LD BC, (firePos), and call to position the cursor, CALL At.

ASM
ld   a, FIRE_GRAPH 
rst  $10 

ret

We load the shot character in A, LD A, FIRE_GRAPH, paints it, RST $10, and exit, RET.

The final aspect of the routine is as follows:

ASM
; -------------------------------------------------------------------
; Paints the shot at the current position.
; Alters the value of the AF and BC registers.
; -------------------------------------------------------------------
PrintFire:
ld   a, $02                ; A = red ink
call Ink                   ; Change ink

ld   bc, (firePos)         ; BC = shot position
call At                    ; Position cursor

ld   a, FIRE_GRAPH         ; A = shot character
rst  $10                   ; Paints it

ret

We implement the routine that moves the shot in game.asm.

ASM
MoveFire:
ld   hl, flags
bit  $01, (hl)
jr   nz, moveFire_try
bit  $02, d
ret  z
set  $01, (hl)
ld   bc, (shipPos)
inc  h
jr   moveFire_print

We load the flag address into HL, LD HL, flags, then check if bit one (shot) is set, BIT $01, (HL), and if so we jump, JR NZ, moveFire_try. If bit one is not set, we check if the trigger control has been pressed, BIT $02, D, and if not we exit, RET Z. If it has, we set the trigger bit in flags, SET $01, (HL), load the ship position in BC, LD BC, (shipPos), point it to the top line, INC B, and jump to paints it, JR moveFire_print.

ASM
moveFire_try:
ld   bc, (firePos)
call DeleteChar
inc  b
ld   a, FIRE_TOP_T
sub  b
jr   nz, moveFire_print
res  $01, (hl)

ret

If the fire was active, we load its position in BC, LD BC, (firePos), delete the fire, CALL DeleteChar, point B to the top line, INC B, load the top stop of the fire in A, LD A, FIRE_TOP_T, and subtract B, SUB B. If the result is not zero, the top stop has not been reached and we jump, JR NZ, moveFire_print. If we have reached the top, we deactivate the shot, RES $01, (HL), and exit, RET.

ASM
moveFire_print:
ld   (firePos), bc
call PrintFire

ret

If we have not reached the top or have just activated the shot, we update the position of the shot, LD (firePos), BC, paint it, CALL PrintFire, and exit, RET.

The final aspect of the routine is as follows:

ASM
; -------------------------------------------------------------------
; Move the shot
;
; Input: D -> Controls status
; Alters the value of the AF, BC and HL registers.
; -------------------------------------------------------------------
MoveFire:
ld   hl, flags             ; HL = address flags
bit  $01, (hl)             ; Active fire?
jr   nz, moveFire_try      ; If active, jumps
bit  $02, d                ; Trigger control active?
ret  z                     ; If not active, exits
set  $01, (hl)             ; Enables trigger bit in flags
ld   bc, (shipPos)         ; BC = ship position
inc  b                     ; B = top line
jr   moveFire_print        ; Paint shot

moveFire_try:
ld   bc, (firePos)         ; BC = shot position
call DeleteChar            ; Delete shot
inc  b                     ; B = top line
ld   a, FIRE_TOP_T         ; A = upper shot top
sub  b                     ; A = A - B (Y-coordinate shot)
jr   nz, moveFire_print    ; A!= B, does not reach top, jumps
res  $01, (hl)             ; Deactivates shot

ret

moveFire_print:
ld   (firePos), bc         ; Update shot position
call PrintFire             ; Paint shot

ret

It’s time to test the shot, so we open main.asm and at the beginning, in the flags declaration, we add the comment for bit one.

ASM
; Bit 1 -> shot is active 0 = No, 1 = Yes

We locate Main_loop, and between the lines CALL CheckCtrl and CALL MoveShip we add the call to the shot movement.

ASM
call MoveFire

We compile, load the emulator and see the results.

You can’t see it in the picture, but in the emulator you can see that it looks like it’s firing in bursts, and if you leave it pressed it looks like it doesn’t stop firing. This is an optical effect caused by the camera moving faster than the ULA refreshes the screen. We leave it like that to make it look like we are shooting several times at once.

ZX Spectrum Assembly, Space Battle

We started working with interrupts, timed the ship’s movement and implemented triggering, as well as compiling the program into several files and customising the loader.

In the next chapter of ZX Spectrum Assembly, we will add the enemies.

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