ZalaXa 13 — You Gotta Roll With It

In the last tutorial I looked at creating a randomized starfield for the main menu. Since then I’ve been playing around with techniques to scroll it vertically, like the starfield on the Galaga arcade machine.

I found a bug in the code I wrote. Instead of using a pair of random(ish) numbers for the X and Y coordinates of each star, I was actually using the Y coordinate of the previous star for the X coordinate of the next one. This was causing unnecessary vertically-repeating patterns.

After fiddling around with this for a while, I decided to use a third randomish number to represent the star position with the character. This leaves me with some spare bits I can use for attribute colours without introducing more repeating patterns.

I also added a seed randomizer in my test routine—instead of advancing the ROM seed sequence by one every time you press a key, I completely randomized it to any address between $0000 and $3FFF. I used Patrik Rak’s Complementary Multiply With Carry pseudo-random number generator again for this.

Same deal as last time, except this time it scrolls vertically:

Most of the code is pretty similar to last time. The section that draws the stars is duplicated. In the first iteration, the part that creates the set n, a opcode now creates a res n, a instruction instead, which undraws the previous star.

; stars.asm
                        ld a, (hl)                      ; Read the byte represnting the star pixel,
                        or %10000111                    ;      turn it into a "res n, a" instruction ($80 10nnn111) by setting
                        and %10111111                   ;      and clearing bits,
                        ld (ResetBit), a                ; SMC> then write this into the pixel-drawing code.

In the second iteration, we keep the set n, a instruction as it was in part 12. But before we draw the star pixel, we manipulate the Y coordinate by including the ScrollStar macro:

; macros.asm
ScrollStar              macro()
                        ld a, d                         ; Retrieve the byte containing bits 0..2 of the Y coordinate.
                        and %00000111                   ; Calculate Y mod 8 (0..7).
                        cp %00000111                    ; If 7 then
                        jp z, CharScroll                ;   do a character scroll.
                        ld a, d                         ; Otherwise do
                        inc a                           ;   a pixel
                        ld d, a                         ;   scroll.
                        inc (hl)                        ; Save px-scrolled offset back to StarField.Table.
                        jp Continue
                        ld a, e                         ; Retrieve the byte containing bits 3..4 of the Y coordinate.
                        add 32                          ; Increment those two bits by one
                        ld e, a                         ;   and save back the byte. This moves into the next char row.
                        dec l                           ; Now retrieve the byte
                        ld (hl), a                      ;   containing bits 0..2 of the Y coordinate,
                        inc l                           ;   and
                        xor a                           ;   reset it to zero, ensuring we start at the top of that char row.
                        ld d, a                         ; Save that back to the byte, and also
                        ld (hl), a                      ;   save the px-scrolled offset back to StarField.Table.

Doing this as a macro isn’t really necessary at all, but when reading the code, it does emphasise that the pixel-setting half is almost the same as the pixel-clearing half. It achieves this by abstracting the extra code into a different place—without any of the call/ret/jp overhead a separate routine would entail.

The stuff in the macro decides whether we’re scrolling within a character boundary, or scrolling between character boundaries. Then it sets the appropriate bits of the Y coordinate accordingly. The specific maths here is needed because of how the Y coordinate is distributed within the screen address:

Image from the L Break Into Program blog, with thanks to Dean Belfield.

In the Spectrum’s distinctive display file, the bits labelled Y0, Y1 and Y2 govern the position (0..7) within a character row. We set these bits in the first half of the macro.

The bits labelled Y3, Y4 and Y5 govern the position of the character row (0..7) within the current screen third. We set these bits in the second half of the macro.

The bits labelled Y6 and Y7 govern which screen third the character row appears in. Because we’re repeating the same stars in all three thirds, we set these bits in the SetupStars routine:

; stars.asm
                        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.

Actually, adding $0800 to the screen address using the add hl, bc instruction is an inefficient way of doing this. All we really need is to add 8 to h. Or even better, flip one or two bits of h. I might optimise this part another time.

I made a list of 32 of my favourite starfields using this new version of the randomizer test routine. I particularly like how it tends towards drifts of stars that look like constellations.

This is my favourite starfield (seed address $26B1), complete with the scroll effect:

Next time, I’ll explore setting attribute colours to make the stars twinkle. Cheers!

One thought on “ZalaXa 13 — You Gotta Roll With It

Leave a Reply

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