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