This brings me to the main event. No self-respecting cracktro is complete without a starfield!
My first, naive approach to the classic starfield effect was to scatter random stars across the screen, then move down each scan-line applying the same rl trick I’d used for the scroll-text. I figured I could get parallax by shifting each row a different amount: One pixel for the first row, two for the second, three for the third, then back to one again, all the way down the screen to give the impression of several “fields” of stars moving at different speeds.
It turned out there were two major problems with that approach. Firstly, shifting an entire row smeared whatever else was on it along with the stars - which I’d expected, and had filed away as a problem for future me. The bigger issue was speed: by the time I’d finished the first three rows I’d already performed hundreds of shift operations, and it was nowhere near fast enough to fit inside a single frame. Everything crawled across the screen, and the music playback slowed down so much that you could actually hear the arpeggio ornaments warbling away as separate notes.
Clearly I was going to have to keep track of the individual stars instead. I found this routine, which was more-or-less what I had in mind and started me off with the idea of a structure to hold the state of each star. It also uses a screen address lookup much like the one from “Break Into Program”.
That got things moving, but the co-ordinate lookup and tracking proved expensive to do (it has to run for every star, every frame as well as keep track of the old position for erasing) and I’d been ignoring the bigger question how to display stuff on top of the starfield. My original idea was simply to draw the stars and then paint text over them each frame, which blew the frame budget just as comprehensively as my first attempt.
If I wanted something more interesting than an empty starfield, I’d have to keep track of which cells were occupied by text and avoid drawing stars in them in the first place, rather than over-painting them every time. After lots of scribbling in my notepad and visualising rows of bits, I eventually solved both challenges by “cheating” fairly liberally.
So now, I don’t generate a random starfield at all. Instead, I built a table of pre-calculated Y and X position pairs:
stdata:
defb 109, 25, 88,182, 42, 96, 155,226
defb 59,254, 137,169, 160, 84, 125,193
; ... and so on, for 32 stars ...
This saves faffing about with an RNG, and once I have a layout that looks good I know it’ll always look good. I can also “fudge” things and ensure no two stars share the same y co-ordinate which sidesteps a whole class of problems like stars sharing a scanline, and having to deal with them overtaking each other.
The “random” layout is generated by a simple Ruby script:
#!/usr/bin/env ruby
PAIRS = 32
PER_LINE = 4 # pairs per line
Y_RANGE = 40..175 # exclude rows taken up by my logo & bars
X_RANGE = 0..255
raise "y range too small" if Y_RANGE.size < PAIRS
# no duplicate y values
ys = Y_RANGE.to_a.sample(PAIRS)
pairs = ys.map { |y| [y, rand(X_RANGE)] }
pairs.each_slice(PER_LINE) do |row|
body = row.map { |y, x| format('%3d,%3d,', y, x) }.join(' ')
puts "defb #{body.chomp(',')}"
end
When the routine initialises, I convert those co-ordinate pairs into a table that tracks the state of each star. So rather than storing a position and translating it to a screen address every frame, each of my star records now looks like this (offsets from the base address):
STSCR EQU 0 ; screen address of the byte the star is in, e.g. $4123
STOCC EQU 2 ; a pointer to the "occupancy check" table cell
STMASK EQU 4 ; the star's bit pattern, e.g. %00010000
STSTEP EQU 5 ; "speed" - num. of pixels this star travels per frame
The key thing is that there are no X/Y co-ordinates at all. For each star I store the screen byte it sits in and the bit mask within that byte, so it’s already in the form the screen writes need. That means no address conversion per frame, just simple bit shifting and rotation.
Moving a star is then dead simple. I rotate its mask left with rlca, and when the bit rotates out of bit 7, it lands back in bit 0 with the carry flag set. That’s the signal I need to move the star one character cell to the left, which is one of the “cheap moves” from earlier.
Because I’m tracking which cell each star is in, the collision check comes almost for free. That’s what the second pointer is for: STOCC (Star Occupancy) moves in step with STSCR through a table holding one byte per character cell. The “typewriter” routine sets a byte to 1 for every cell containing a text character or the cursor, so checking whether a star is hidden behind text is a single or instruction. If the cell is occupied, I skip drawing the star but carry on rotating and stepping it along until it reaches a cell where it can be displayed:
ld l,(ix+STOCC)
ld h,(ix+STOCC+1)
ld a,(hl) ; A = the occupancy check for this cell
or a ; test A against zero
jr nz,stmove ; it's occupied, so leave the screen alone
Parallax is still handled by moving each star a different number of pixels per frame, but the cost is a lot lower this time as I’m only touching the cells the stars actually occupy, rather than every byte of every row.