ZalaXa 12 — Towards a Scrolling Starfield

The technology in Namco’s arcade version of Galaga (1981) is from the same era as the ZX Spectrum, but is actually a little beefier. The main board has three Z80 processors, clocked at 3Mhz—each one just under the speed the Spectrum’s single processor runs! Sadly, I think that rules out being able to code an authentic arcade version of Galaga in this tutorial series. In particular, if you recall, NIRVANA+’s multicolour raster-chasing technique uses about 78% of the available processor T-states we normally have when writing Spectrum programs. That said, this should still be a fun exercise in pushing the machine to its limits 🙂

Arcade Galaga has a rather nice winking multicolour starfield scrolling down the screen during gameplay. This isn’t really necessary for the game, and won’t demonstrate any NIRVANA+ techniques, but I am quite attached to the effect, so I thought I’d take a short detour and try and get this working on the start menu.

I did some rough calculations, and decided we need about 100 stars, give or take. My first thought was to write a short BASIC program to prototype this, using RND and PLOT in a loop, but I decided to do it in machine code, which I find a bit easier.

My first attempt used Patrick Rak’s 8-bit Complementary-Multiply-With-Carry (CMWC) random number generator, to generate a table of random bytes. This proved the concept nicely, but then I remembered people often use the ZX Spectrum ROM as a source of weakly-random numbers. Surely this would be good enough for my purposes, as it’s only a few stars. The trick would be to pick a short sequence of numbers from somewhere in the ROM that looks natural enough. We have 16KB to play with—more on the later models, but let’s try to limit ourselves to something that will work and give the same results on all Spectrum models.

Thinking further, I realised the vertical scrolling will be the trickiest part of the problem. The Spectrum screen is laid out in vertical thirds, and having the pixels jump between third-boundaries will complicate things. For our purposes, though, we should be able to duplicate the same stars across all three. If each third wraps around, then individual stars will seem to scroll continuously into the next third at the same time as wrapping round. Einfach genial!!

Better still, we might be able to mitigate any obvious repetitive patterns, by using different attribute colours for each third. The entire starfield will appear to be shimmering because of the colour effects, and that should provide some distraction.

I came up with a routine that does this, noting the “seed” ROM address on the debug screen for reference. It waits till you press a key, then adds one byte to the seed address and does it all over again. Like this:

Excellent! There’s a bit of flicker in the middle third, but that’s just a side effect of the way I coded the keypress and screen re-clear loop.

It turns out that most of the seed values are not very random at all. But, persevering, I found a few seed values that gave fairly uniform results, without any ugly double pixels or tight bunchings. This was the most promising one, $03F3:

Not perfect, but good enough.

I’ll develop this a bit further in the next tutorial, but let’s finish up by looking at my code:

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
; stars.asm
 
SetupStars              proc
                        ld a, DimWhiteBlackP            ; Custom colour for ClsAttr.
                        call ClsAttr.WithCustomColour   ; Alternate entry point without setting colour.
RandomLoop:
                        call ClsPixels                  ; Clear all the pixels.
Seed equ $+1:           ld hl, $0000                    ; Starting point of ROM "psuedo-random" table (2 bytes per star). Try $03F3!
                        zeusdatabreakpoint 1, "zeusprinthex(1, hl)", $ ; Log current value of ROM table to debug window.
                        ld a, NumberOfStars             ; Loop through this many times, once for each star.
DrawLoop:
                        ex af, af'                      ; Save number of stars (loop counter).
                        ld e, (hl)                      ; Read 2 bytes,
                        inc hl                          ;   for this star,
                        ld a, (hl)                      ;   into de.
                        and %00000111                   ; Constrain de between 0..2047
                        ld d, a                         ;   (size of top screen third).
                        ld a, e                         ; Mask out a bit number (0..7) from the X coordinate,
                        or %11000111                    ;      turn it into a "set n, a" instruction ($CB 11nnn111),
                        ld (SetBit), a                  ; SMC> then write this into the pixel-drawing code.
                        ex de, hl                       ; Sawp the reading addr into de, and the writing addr into hl.
                        ld b, high(PixelAddress)        ; c stays 0, so this is a fast way of doing ld bc, $4000.
                        add hl, bc                      ; Calculate a pixel address in the top screen third.
                        xor a                           ; Draw a single pixel star,
SetBit equ $+1:         set SMC, a                      ; <SMC  by setting that pseudo-random
                        ld (hl), a                      ;       bit (0..7) from earlier.
                        ld b, 8                         ; Fast way of doing ld bc, $0800 (size of a screen third).
                        add hl, bc                      ; hl is now a pixel address in the middle screen third.
                        ld (hl), a                      ; Draw the same single pixel star here, too.
                        add hl, bc                      ; hl is now a pixel address in the bottom screen third.
                        ld (hl), a                      ; Draw the same single pixel star here, too.
                        ex de, hl                       ; Swap the writing addr back into de, and reading addr into hl.
                        ex af, af'                      ; Retrieve the number of stars (loop counter),
                        dec a                           ;   decrease it,
                        jp nz, DrawLoop                 ;   and do all over again if there are any stars left.
 
                        call WaitForAnyKeyPress         ; Spin until any key is pressed
 
                        ld hl, (Seed)                   ; Increase starting point of ROM "psuedo-random" table
                        inc hl                          ;      by one byte,
                        ld (Seed), hl                   ; SMC> save it into the routine,
                        jp RandomLoop                   ;      and rerun the routine again.
 
NumberOfStars           equ 32                          ; Constant declared locally to keep it handy.
pend

Once again, apologies for the syntax highlighter I’m using. My pair of ex af, af' opcodes apparently turned a whole bunch of the midsection into one long string. Fortunately the Z80 is not fooled by such shenanigans…

Anyway, this rather dense chunk of code clears the screen at the beginning, then on line 10 sets the ROM address seed value ($0000 initially).

Line 11 is a sweet Zeus feature that can break, or print values, based on expressions you write. Here, I’m writing an expression in slot 1, and setting it to be active on line $—the next opcode, which in this case is ld a, NumberOfStars. It doesn’t matter too much where you set it—in this case, the important thing is that it’s after hl has been set, but before it changes again.

The expression itself is zeusprinthex(1, hl). The first 1 means “always evaluate the expression1“. Everything else inside the brackets is a comma-separated list of things to print—in this case, just hl. Because I used zeusprinthex, the arguments are printed as hex values—as you might expect, zeusprint(1, hl) would print the decimal value of hl.

Referring back to the video, this is indeed what happens.

The next section (lines 12-19) sets up a loop counter, stores it away in the a' alternative register, then reads a couple of bytes from the ROM. These bytes, taken together, are effectively a random number between 0 and 65535. The size of our screen thirds is 2KB, or 2048 bytes. It turns out we can zeroise the leftmost five bytes, to turn it into a random number between 0 and 2047. Once again, powers of two are the bomb!

10
11
12
13
14
15
16
17
18
19
; stars.asm
 
                        ld a, NumberOfStars             ; Loop through this many times, once for each star.
DrawLoop:
                        ex af, af'                      ; Save number of stars (loop counter).
                        ld e, (hl)                      ; Read 2 bytes,
                        inc hl                          ;   for this star,
                        ld a, (hl)                      ;   into de.
                        and %00000111                   ; Constrain de between 0..2047
                        ld d, a                         ;   (size of top screen third).

The following section (lines 20-22) Grabs three of the bits from the part of the screen address that governs the X coordinate (meaning it won’t change as it vertically scrolls), and picks one of the 8 pixels within the pixel address to plonk a star into.

18
19
20
21
22
; stars.asm
 
                        ld a, e                         ; Mask out a bit number (0..7) from the X coordinate,
                        or %11000111                    ;      turn it into a "set n, a" instruction ($CB 11nnn111),
                        ld (SetBit), a                  ; SMC> then write this into the pixel-drawing code.

This is really fancy stuff. The Z80 processor is wired up internally in a very logical and consistent way, which means there are many relationships between similar opcodes. The opcodes for setting a bit all follow this rule, or formula: SET b, r is a two-byte instruction, the first of which is always $CB. The second is $C0+(8*b)+r, where b is a bit number between 0 and 7, and r is a register chosen from one of these values:

Register    Value
A           7
B           0
C           1
D	    2
E	    3
H	    4
L	    5

We’re using register a, which is %00000111, to set the star pixel. $C0 also happens to be $11000000. Adding these two together gives us a mask of %11000111. We take the three bits we chopped out of the X coordinate, which are in the exact position to fill the three zero bits in our mask, then or them together in line 22. The result is an opcode that’s always one of the following:

set 0, a
set 1, a
set 2, a
set 3, a
set 4, a
set 5, a
set 6, a
set 7, a

Having done that, line 22 writes it into the correct place in the program (line 27), ready to be run shortly. This is similar to how an assembler works, but we used the technique in our own program.

The code between lines 23 and 35 calculates a pixel byte based on our X and Y coordinates, executes our hand-assembled set instruction, and writes the star byte to the display file. It then does it two more times to the other two screen thirds, taking advantage of the fact that the screen thirds are all separated by exactly $0800 bytes (2048 in decimal).

The rest of it is just boring glue code that waits for a key to be pressed, moves to the next ROM seed value, clears the screen again, and reruns the whole process.

In the next tutorial I’ll explore ways to vertically scroll the stars. Cheers!

  1. Instead of a literal 1, it could itself be another expression that evaluates to either 0 (don’t print) or 1 (do print).

Leave a Reply

Your email address will not be published. Required fields are marked *