Finishing the ZX Spectrum Shooting Gallery | Bumbershoot Software
This week I’ll be finishing up the port of my Shooting Gallery Rosetta Stone program to the ZX Spectrum. After last week, the only thing left was shots and the logic surrounding them.
This is a feature that needs a lot of work at all levels, from design through implementation through consideration of the top-level program organization.
Designing the Data
There’s a lot of things we need to do with the shots, so we’ll need to define the space to handle it for us. One of them is pretty trivial; we’ll want a convenient place to store the highest Y coordinate (that is, the point lowest on the screen) that any live shots are. We measure when to allow the next shot by making sure this value is below some threshold.
max_shot_y # 1
The individual shots themselves have to track their location and also whether they hit something during the render phase. We know that we’re going to be completely separating rendering from game logic, so while the pixel-intersection operations happen during rendering, everything else happens during frame logic. That gives us three bytes worth of data to hold: the two coordinates and a collision value.
On the TMS9918-and-Z80 systems, I used the hardware collision detection register and then used my CPU-side mirror of sprite VRAM as an array of 4-byte structures. This time around I decided to flip the script a little, working a bit more like I would on the 6502 and used a "structure of arrays" approach instead:
num_shots # 1<br>shot_y # 16<br>shot_x # 16<br>shot_collide # 16
Without hardware sprites, I decided that this time I would keep the array continuous as well; hence we’ll need a length variable num_shots to go with it. My older code just processed all possible shots at all times; this one will need to compact the array the way the Amiga code did.
The last bit of information we need is the data we pass on to rendering. We had three steps in our plan two weeks ago:
Erase out the bottom two pixels of every shot. We missed a spot here: in this phase we also need to erase any shot that participated in a collision last frame entirely.
Check the pixels that we’re about to extend our active shots to; if there’s a pixel drawn there then the shot we’re about to draw is colliding with a target.
Draw all the remaining pixels of all our shots; it’ll be easier to just redraw them all out then try to figure out if a moving target has swept away its old pixels.
Erasing is easy; all we need is the memory address to start clearing from. Since all erasing happens before any drawing we don’t even need to do any bit-level silliness; we can just wipe out the whole byte. I decide that this will be an array named shot_tails that gives us the place to start our erasing. Much like the targets last week, every point we ever erase from will be on an even pixel row, which makes erasing its successor basically free.
I also decide, at this point, that I’ll handle the deletion of entire shots (each six pixels tall) by just putting three entries in this table. That means I’ll want to have more than 16 entries (my maximum allocation for shots) in this table; in principle only one shot should be getting fully erased each frame, so doubling my array to 32 entries seems sufficient. The size of this array might vary from frame to frame, thanks to these collisions. I decide that I’ll be using null pointers as a terminator for this list instead of having an explicit size counter.
Drawing shots is a little more exciting. In addition to the screen memory byte to start at, we’ll need a bitmask to identify how to composit the shot into the screen. Since that will be happening during collision detection, we will also need some way to propagate collision results back to the shot_collide array. In the end, I make this another null-terminated list, this time of four-byte structures: the two-byte address, a one-byte bitmask for drawing the shot, and an index callback. With 16 shots in the array and the potential need for a terminator, we’ll ultimately need 17×4=68 bytes.
shot_tails # 64<br>shot_heads # 68
(Why can’t I just use the indices of the arrays for the head? Shots leaving the screen, as we’ll see, are still being actively updated but won’t be redrawn.)
Autonomous Movement
Much like the targets, updating the "automatic" part of the shots is very easy; we just subtract 2 from each active Y coordinate if it isn’t already at zero.
ld hl,shot_y<br>ld a,(num_shots)<br>or a<br>ret z ; Return immediately if no shots<br>ld b,a<br>1 ld a,(hl) ; Check current Y coord<br>or a ; Already zero?<br>jr z,2F ; If so, on to next shot<br>sub 2 ; If not, subtract 2<br>ld (hl),a<br>2 inc hl<br>djnz 1B
Retiring Shots
I’ll need to keep the shot array compacted as I go, and that means removing shots that have left play. I need some way of determining which shots count as dead, and we have an obvious candidate for that from our previous code: we treat a Y coordinate of 0 as being a shot that needs to be...