Espamática
ZX SpectrumRetroZ80 Assembly

ZX Spectrum Assembly, Space Battle – 0x0E Difficulty, mute and loading screen

In this chapter of ZX Spectrum Assembly, we will allow you to choose between five difficulty levels, mute the music during the game and include the loading screen.

Create the folder Step14 and copy the files loader.tap, const.asm, ctrl.asm, game.asm, graph.asm, int.asm, main.asm, make or make.bat, print.asm and var.asm from the folder Step13.

Translation by Felipe Monge Corbalán

Table of contents

Difficulty

Depending on the difficulty level selected, we will change the behaviour of the enemies and the number of lives as follows:

  • Level one: Enemies do not reach our ship’s position. The number of simultaneous shots fired by the enemies is one.
  • Level two: Enemies do not reach our ship’s position. The number of simultaneous enemy shots is five.
  • Level three: The number of simultaneous shots is one.
  • Level four: The number of simultaneous shots is five. Each time you complete a level, you start with five lives, Galactic Plague style.
  • Level five: the number of simultaneous shots is five.

As we are giving the option of choosing between five difficulty levels, we need to modify the start screen.

We go to var.asm and modify title and firstScreen, leaving them as follows:

ASM
title:
db $10, $02, $16, $00, $0a, "SPACE BATTLE", $0d, $0d, $ff

firstScreen:
db $10, $06, "  Alien ships attack the Earth,", $0d
db "the future depends on you.", $0d, $0d
db "  Destroy all enemies that you", $0d
db "can, and protect the planet.", $0d, $0d
db $10, $03, $16, $08, $02, "Z - Left"
db $16, $08, $14, "X - Right"
db $0d, $0d, $16, $0a, $02, "V - Shot"
db $16, $0a, $14, "M - Sound", $0d, $0d
db $10, $04, "1 - Keyboard      3 - Sinclair 1", $0d, $0d
db "2 - Kempston      4 - Sinclair 2", $0d, $0d
db $10, $07, $16, $10, $08, "5 - Difficulty", $0d, $0d
db $10, $05, "  Aim, shoot and dodge the enemy", $0d
db "ships, defeat and release to the", $0d
db "planet of the threat."
db $ff

As we add new options, we remove a line break in the title and several in the rest of the screen to make everything fit.

We continue in var.asm, locate the enemiesColor tag and under the value (DB $06) add the tag we will use to store the selected difficulty.

ASM
hardness:
db $03

Now that we have the modified splash screen, we need to display the chosen difficulty level. We go to print.asm and after the PrintFrame routine we implement the routine that paints the difficulty:

ASM
; -------------------------------------------------------------------
; Paints the selected difficulty in the menu
;
; Alters the value of the AF and BC registers.
; -------------------------------------------------------------------
PrintHardness:
ld   a, $02                ; A = red ink
call Ink                   ; Change ink
ld   b, $08                ; B = coord Y (inverted)
ld   c, $09                ; C = coord X (inverted)
call At                    ; Position cursor
ld   a, (hardness)         ; A = hardness
add  a, '0'                ; A = A + character 0
rst  $10                   ; Paints the difficulty

ret

At this stage we have a certain level of knowledge, so we will only explain the routine in detail.

We set the ink to red (2), position the cursor, load the difficulty, add the character ‘0’ to get the difficulty character and paint it.

We continue in print.asm, find the PrintFirstScreen routine and after the fifth line, CALL PrintString, we add the call to paint the selected difficulty.

ASM
call PrintHardness

We compile, load the emulator and see the results.

We removed the line breaks, added a new control button to turn the music on and off, and added the option to select the difficulty level.

Now we add the implementation of the difficulty selection. Continuing with print.asm, locate the printFirstScreen_end tag, and just above it we have the JR C line, printFirstScreen_op. Just above this we add the following lines:

ASM
jr   nc, printFirstScreen_end
rra

If key 4 has been pressed, we go to the end of the routine, JR NC, printFirstScreen_end. If not, we turn A to the right to see if key 5 has been pressed.

The next line was already there, JR C, printFirstScreen_op, and we leave it as it is, if the 5 has not been pressed it jumps and continues in the loop.

Below this line we add the following lines, which will be executed if the 5 is pressed:

ASM
ld   a, (hardness)
inc  a
cp   $06
jr   nz, printFirstScreen_opCont
ld   a, $01

We load the difficulty level selected in A, LD A, (hardness), increment it, INC A, check if it has reached six, CP $06, skip if not, JR NZ, printFirstScreen_opCont, and if it has, set it to one, LD A, $01.

ASM
printFirstScreen_opCont:
ld   (hardness), a
call PrintHardness
jr   printFirstScreen_op

We update the difficulty in memory, LD (hardness), A, paint it, CALL PrintHardess, and continue in the loop waiting for a key to be pressed from 1 to 4.

The look and feel of the routine is as follows:

ASM
; -------------------------------------------------------------------
; Display screen and selection of controls.
;
; Alters the value of the AF, BC and HL registers.
; -------------------------------------------------------------------
PrintFirstScreen:
call CLS                   ; Clear screen
ld   hl, title             ; HL = title definition
call PrintString           ; Print title
ld   hl, firstScreen       ; HL = screen definition
call PrintString           ; Paint screen
call PrintHardness         ; Paint difficulty

printFirstScreen_op:
ld   b, $01                ; B = 1, option keys
ld   a, $f7                ; A = half-row 1-5
in   a, ($fe)              ; Read keyboard
rra                        ; Rotate A right, 1 pressed?
jr   nc, printFirstScreen_end ; No carry, click and skip
inc  b                     ; B = B+1, option Kempston
rra                        ; Rotate A right, 2 pressed?
jr   nc, printFirstScreen_end ; No carry, click and skip
inc  b                     ; B = B+1, option Sinclair 1
rra                        ; Rotate A right, 3 pressed?
jr   nc, printFirstScreen_end ; No carry, click and skip
inc  b                     ; Increments B, Sinclair 2
rra                        ; Rotate A right, 4 pressed?
jr   nc, printFirstScreen_end ; No carry, click and skip
rra                        ; Rotate A right, 5 pressed?
jr   c, printFirstScreen_op ; Carry, NOT pressed, loop
ld   a, (hardness)         ; A = hardness
inc  a                     ; A = A+1
cp   $06                   ; A = 6?
jr   nz, printFirstScreen_opCont ; A < 6, skip
ld   a, $01                ; A = 1

printFirstScreen_opCont:
ld   (hardness), a         ; Upgrades difficulty
call PrintHardness         ; Paint it
jr   printFirstScreen_op   ; Loop to press 1 to 4

printFirstScreen_end:
ld   a, b                  ; A = selected option
ld   (controls), a         ; Load into memory
call FadeScreen            ; Fade screen

ret

We compile, load the emulator and test the difficulty setting.

It doesn’t work, at least not the way we want it to. It is very fast and extremely difficult to select the desired difficulty.

We are going to change the part of the routine that evaluates which keys are pressed, relying on the ROM routines that control that there is a pause between each key detection to avoid repetition.

Let’s switch to interrupt mode 1 so that all system variables are automatically updated.

We go to const.am and add two counters pointing to two system variables that we need to know which key was last pressed using the ROM routines.

ASM
;--------------------------------------------------------------------
; Address where the keypad status flags are located when 
; The interrupts are active in mode 1.
;
; Bit 3 = 1 input in L mode, 0 input in K mode.
; Bit 5 = 1 a key has been pressed, 0 has not been pressed.
; Bit 6 = 1 numeric character, 0 alphanumeric.
;--------------------------------------------------------------------
FLAGS_KEY: equ $5c3b

;--------------------------------------------------------------------
; Memory address where the last pressed key is located.
; when mode 1 interrupts are active.
;--------------------------------------------------------------------
LAST_KEY: equ $5c08

If we read the comments we will know what is what.

We go back to print.asm, find printFirstScreen_op and delete the lines just below LD B, $01 to line JR C, printFirstScreen_op, including this last line. Just above printFirstScreen_op we add the following lines:

ASM
di
im   1
ei

ld   hl, FLAGS_KEY
set  03, (hl)

Disable interrupts, DI, switch mode to one, IM 1, re-enable interrupts, EI, load the address of the keyboard flags in HL, LD HL, FLAGS_KEY, and set the input mode to L, SET $03, (HL).

Just below printFirstScreen_op we implement the keyboard reading using ROM:

ASM
bit  $05, (hl)
jr   z, printFirstScreen_op
res  05, (hl)

We check if a key has been pressed, BIT $05, (HL), if it has not we return to the loop, JR Z, printFirstScreen_op, and if it has we set bit 5 to zero for future checks, RES $05, (HL).

The next line is LD B, $01. We continue to implement below it.

ASM
ld   c, '0' + $01
ld   a, (LAST_KEY)
cp   c
jr   z, printFirstScreen_end

We load in C the ASCII code of the one, LD C, ‘0’ + $01, in A the last key pressed, LD A, (LAST_KEY), we check if it is the one, CP C, and if so we jump, JR Z, printFirstScreen_end.

ASM
inc  b
inc  c
cp   c
jr   z, printFirstScreen_end

Increment B (Kempston option), INC B, increment C (key two), INC C, check if pressed, CP C, and jump, JR Z, printFirstScreen_end, if so.

We do the same to check if three or four (Sinclair 1 and 2) has been pressed:

ASM
inc  b
inc  c
cp   c
jr   z, printFirstScreen_end
inc  b
inc  c
cp   c
jr   z, printFirstScreen_end

All that remains is to check that five (difficulty) has been pressed:

ASM
inc  c
cp   c
jr   nz, printFirstScreen_op

Increment C (key 5), INC C, see if it is pressed, CP C, and skip if not, JR NZ, printFirstScreen_op.

All that remains is to find printFirstScreen_end and add the following lines before RET:

ASM
di
im   2
ei

We deactivate the interrupts, DI, switch to mode two, IM 2, and activate them, EI.

The final aspect of the routine is as follows:

ASM
; -------------------------------------------------------------------
; Presentation screen, selection of controls and difficulty.
;
; Alters the value of the AF, BC and HL registers.
; -------------------------------------------------------------------
PrintFirstScreen:
call CLS                   ; Clear screen
ld   hl, title             ; HL = title definition
call PrintString           ; Paint title
ld   hl, firstScreen       ; HL = screen definition
call PrintString           ; Paint screen
call PrintHardness         ; Paint difficulty

di                         ; Disables interruptions
im   1                     ; Switches to mode 1
ei                         ; Activates interruptions

ld   hl, FLAGS_KEY         ; HL = keyboard flags address
set  $03, (hl)             ; L-mode input
printFirstScreen_op:
bit  $05, (hl)             ; Any key pressed?
jr   z, printFirstScreen_op ; No, returns to loop
res  $05, (hl)             ; Bit 5 = 5, required for future inspections
ld   b, $01                ; B = 1, option keys
ld   c, '0' + $01          ; C = ASCII code 1
ld   a, (LAST_KEY)         ; A = last key pressed
cp   c                     ; 1 pressed?
jr   z, printFirstScreen_end ; Yes, jump
inc  b                     ; B = B+1, option Kempston
inc  c                     ; C = C+1, key 2
cp   c                     ; 2 pressed?
jr   z, printFirstScreen_end ; Yes, jump
inc  b                     ; B = B+1, option Sinclair 1
inc  c                     ; C = C+1, key 3
cp   c                     ; 3 pressed?
jr   z, printFirstScreen_end ; Yes, jump
inc  b                     ; B = B+1, option Sinclair 2
inc  c                     ; C = C+1, key 4
cp   c                     ; 4 pressed?
jr   z, printFirstScreen_end ; Yes, jump
inc  c                     ; C = C+1, key 5
cp   c                     ; 5 pressed?
jr   nz, printFirstScreen_op ; No, loop
ld   a, (hardness)         ; A = hardness
inc  a                     ; A = A+1
cp   $06                   ; ¿A = 6?
jr   nz, printFirstScreen_opCont ; No, skip
ld   a, $01                ; A = 1
printFirstScreen_opCont:
ld   (hardness), a         ; Upgrades difficulty
call PrintHardness         ; Paint it
jr   printFirstScreen_op   ; Loop until key is pressed 1 to 4

printFirstScreen_end:
ld   a, b                  ; A = selected option
ld   (controls), a         ; Load into memory
call FadeScreen            ; Fade screen

di                         ; Disables interruptions
im   2                     ; Switches to mode 2
ei                         ; Activates interruptions

ret

We compile, load the game into the emulator and see that we can select the difficulty we want.

Now that we have selected the difficulty, we need to change the behaviour of the game depending on the selected difficulty: the enemies may or may not reach the line where our ship is, and there are one to five enemy fires at the same time. We control this with the constants ENEMY_TOP_B and FIRES, so these values must be variable.

Open var.asm, locate the hardness tag, and add these lines just above it:

ASM
enemiesTopB:
db ENEMY_TOP_B
firesTop:
db FIRES

We have our variables, now we need to use them. Go to game.asm, find the tag moveEnemies_Y_down and the line SUB ENEMY_TOP_B. We replace this line with the following, reading the comments we know what it does:

ASM
push hl                    ; Preserves HL
ld   hl, enemiesTopB       ; HL = top at the bottom
sub  (hl)                  ; Subtract it 
pop  hl                    ; Retrieve HL

Continuing with game.asm, locate enableEnemiesFire_loop and the line CP FIRES. We will replace this line with the following lines:

ASM
push hl                    ; Preserves HL
ld   hl, firesTop          ; HL = maximum number of shots
ld   c, (hl)               ; C = maximum number of shots
pop  hl                    ; Retrieve HL
cp   c                     ; Compares maximum shots with active

This controls how far the enemy ships can reach from below, and how many shots can be fired at once.

We need a routine that changes the enemiesTop and firesTop values depending on the selected difficulty.

We continue in game.asm, locate Sleep and implement it just above it:

ASM
SetHardness:
ld   hl, enemiesTopB
ld   (hl), ENEMY_TOP_B
ld   a, (hardness)
cp   $03
jr   nc, setHardness_Fire
inc  (hl)

Aim HL at the enemy position stop below, LD HL, enemyTopB, update to default top, LD (HL), ENEMY_TOP_B, load difficulty in A, LD A, (hardness), see if it is three, CP $03, if no carry is greater than or equal and jump, JR NC, setHardness_Fire. If there is carry, the hardness is less than three, we increase the top of the enemy position below by one line, INC (HL). Remember that we are working with inverted coordinates.

ASM
setHardness_Fire:
ld   hl, firesTop
ld   (hl), $01
cp   $01
ret  z
cp   $03
ret  z
ld   (hl), FIRES

ret

We aim HL at the maximum number of simultaneous shots, LD HL firesTop, set it to one, LD (HL), $01, check if we are on difficulty one, CP $01, quit if so, RET Z, check if it is three, CP $03, quit if so. If not, we load the default maximum number of shots, LD (HL), FIRES, and exit, RET.

The final aspect of the routine is as follows:

ASM
; -------------------------------------------------------------------
; Assign difficulty
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
SetHardness:
ld   hl, enemiesTopB       ; HL = top bottom enemies
ld   (hl), ENEMY_TOP_B     ; Update with default buffer
ld   a, (hardness)         ; A = hardness
cp   $03                   ; Compares it with 3
jr   nc, setHardness_Fire  ; No carry A => 3, skip
inc  (hl)                  ; Up one stop line below enemies
setHardness_Fire:
ld   hl, firesTop          ; HL = maximum fires
ld   (hl), $01             ; Sets it to 1 
cp   $01                   ; Difficulty 1?
ret  z                     ; Yes, exits
cp   $03                   ; Difficulty 3?
ret  z                     ; Yes, exits
ld   (hl), FIRES           ; Load default maximum shots

ret

Now it’s time to test if the level selection behaves as we want it to. We go to main.asm, find Main_start and the line CALL PrintFirstScreen. Immediately after this line we add the call to the difficulty setting:

ASM
call SetHardness           ; Assigns the difficulty

Now there is only one of the aspects we pointed out at the beginning; if the selected difficulty level is four, we start each level with five lives.

We continue in main.asm, look for the Main_restart tag and the line JR Z, Win. Just below this line we will implement the last aspect of difficulty:

ASM
ld   a, (hardness)
cp   $04
jr   nz, main_restartCont
ld   a, $05
ld   (livesCounter), a
main_restartCont:

We load the difficulty into A, LD A, (hardness), check if it is four, CP $04, and skip if not, JR NZ, main_restartCont.

If we don’t jump, we load five into A, LD A, $05, and update the number of lives in memory, LD (livesCounter), A, to start each level with five.

The last aspect of the routine is as follows:

ASM
Main_restart:
ld   a, (levelCounter)     ; A = level
cp   $1e                   ; A = 31? (we have 30)
jr   z, Win                ; Yes, jump, VICTORY!

ld   a, (hardness)         ; A = hardness
cp   $04                   ; Is it 4?
jr   nz, main_restartCont  ; No, skip
ld   a, $05                ; A = 5
ld   (livesCounter), a     ; Lives = 5

main_restartCont:
call FadeScreen            ; Fade screen
call ChangeLevel           ; Change level
call PrintFrame            ; Paint frame
call PrintInfoGame         ; Paint info titles
call PrintShip             ; Paint ship
call PrintInfoValue        ; Paint info values
call PrintEnemies          ; Paint enemies
call ResetEnemiesFire      ; Resets enemy shots
; Delay
call Sleep                 ; Produces delay
jr   Main_loop             ; Main loop

We compile, load the emulator and check that the different difficulty levels behave as we have defined.

Muting

The implementation of turning the music on and off is simple. We go to main.asm and add a new comment to the flags tag:

ASM
; Bit 5 -> mute 0 = No, 1 = Yes

In bit five of the flags we will indicate whether the mute is active or not.

Now we need to implement the enabling or disabling of this bit. Locate Main_loop and add the new implementation just below it:

ASM
rst  $38
ld   hl, FLAGS_KEY
set  03, (hl)
bit  $05, (hl)
jr   z, main_loopCheck

We update the system variables, RST $38, point HL to the keyboard flags, LD HL, FLAGS_KEY, put the input in L mode, SET $03, (HL), check if a key has been pressed, BIT $05, (HL), and if not, we jump.

ASM
res  05, (hl)
ld   a, (LAST_KEY)
cp   'M'
jr   z, main_loopMute
cp   'm'
jr   nz, main_loopCheck

We set bit five to zero for future inspection, RES $05, (HL), load the key pressed in A, LD A, (LAST_KEY), see if M was pressed, CP ‘M’, skip if so, JR Z, main_loopMute, see if m was pressed, CP ‘m’, and skip if not, JR NZ, main_loopCheck.

ASM
main_loopMute:
ld   a, (flags)
xor  $20
ld   (flags), a

main_loopCheck:

If m has been pressed, either uppercase or lowercase, we load the flags in A, LD A, (flags), invert bit five, XOR $20, and update the value in memory, LD (flags), A. Finally, we include the tag we jumped to that did not exist, main_loopCheck.

This is a good time to remember how XOR works at bit level:

0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0

If the two bits are the same, the result is 0, if they are different, the result is 1. With XOR $20, if bit 5 is set to 1, the result is 0, if it is set to 1, the result is 1. The rest of the bits are unaffected.

The final appearance of the beginning of the Main_loop routine is as follows:

ASM
; Main loop
Main_loop:
rst  $38                   ; Update system variables
ld   hl, FLAGS_KEY         ; HL = keypad flags address
set  $03, (hl)             ; L-mode input
bit  $05, (hl)             ; Key pressed?
jr   z, main_loopCheck     ; No, skip
res  $05, (hl)             ; Bit 5 = 0, for future inspections
ld   a, (LAST_KEY)         ; A = last key pressed
cp   'M'                   ; M pressed?
jr   z, main_loopMute      ; Yes, skip
cp   'm'                   ; m pressed?
jr   nz, main_loopCheck    ; No, skip
main_loopMute:
ld   a, (flags)            ; A = flags
xor  $20                   ; Reverse bit 5 (mute)
ld   (flags), a            ; Update in memory

main_loopCheck:
call CheckCtrl             ; Check keystroke controls
call MoveFire              ; Move fire

Finally, we locate GameOver, we see that the top line is this:

ASM
jr   Main_loop             ; Main loop

We replace JR with JP because after adding several lines, JR would give us an out-of-range jump error.

Now that we’ve enabled or disabled the mute bit by pressing the M key, let’s take it into account when we play or don’t play the music. We go to Int.asm, find the Isr_sound tag and add the following lines just below it:

ASM
bit  $05, (hl)             ; Bit 5 active (mute)?
jr   nz, Isr_end           ; Yes, jump

When we reach Isr_soung HL points to flags of main.asm. We evaluate if bit five (mute) is set, BIT $05, (HL), and jump if it is, JR NZ, Isr_end.

It’s time to check that we have implemented the mute correctly. We compile, load the emulator, start the game and check that when we press M (without pressing any other key at the same time) the music is muted, when we press it again it starts playing again. The sound effects still play.

Loading screen

We come to the last point in the development of Space Battle, the loading screen.

Since the beginning of the tutorial we have been dragging an aspect that will cause an error when including the loading screen, we saw it in ZX-Pong, we fixed it, but again I have fallen into it; we have to change the start address of our game.

We go to main.asm and change the start address, which is now ORG $5DAD, to ORG $5DFD.

We go to int.asm and change FLAGS: EQU $5DAD and leave it as FLAGS: EQU $5DFD. We also change MUSIC: EQU $5DAE and leave it as MUSIC: EQU $5DFE.

If we now compile and load, we may get an error, because we have not changed the addresses in the loader. In addition to the addresses, we will add a POKE, which we already used in ZX-Pong, and the loading of the loading screen.

The final appearance of the loader should look like this:

VB
10 CLEAR 24059
20 POKE 23739,111: LOAD ""SCREEN$
30 LOAD ""CODE: LOAD ""CODE 32348
40 RANDOMIZE USR 24060

The loading screen, which you should download and leave in the Step14 folder, should look like this:

Download the loading screen here.

We now have the modified loader and the loading screen in the directory. All that remains is to include the loading screen in SpaceBattle.tap.

If we are working on Linux, we edit make and leave it as it is:

ShellScript
pasmo --name Martian --tap main.asm martian.tap martian.log
pasmo --name Int --tap int.asm int.tap int.log
cat loader.tap MarcianoScr.tap martian.tap int.tap > SpaceBattle.tap

If we are working on Windows, we edit make.bat and leave it as it is:

ShellScript
pasmo --name Martian --tap main.asm martian.tap martian.log
pasmo --name Int --tap int.asm int.tap int.log
copy /b loader.tap+MarcianoScr.tap+martian.tap+int.tap SpaceBattle.tap

We have added the loading screen to SpaceBattle.tap.

Run make or make.bat, load the emulator and we have our game, unless you want to change something…

ZX Spectrum Assembly, Space Battle

In the next chapter of ZX Spectrum Assembly, we will go deeper into the use of ZEsarUX.

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