How To Write ZX Spectrum Games – Chapter 8
Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.
Converting Pixel Positions to Screen Addresses
UDGs and character graphics are all very fine and dandy, but the better games usually use sprites and there are no handy ROM routines to help us here because Sir Clive didn’t design the Spectrum as a games machine. Sooner or later a games programmer has to confront the thorny issue of the Spectrum’s awkward screen layout. It’s a tricky business converting x and y pixel coordinates into a screen address but there are a couple of methods we might employ to do this.
Using a Screen Address Look-up Table
The first method is to use a pre-calculated address table containing the screen address for each of the Spectrum’s 192 lines such as this, or a similar variation:
xor a ; clear carry flag and accumulator. ld d,a ; empty de high byte. ld a,(xcoord) ; x position. rla ; shift left to multiply by 2. ld e,a ; place this in low byte of de pair. rl d ; shift top bit into de high byte. ld hl,addtab ; table of screen addresses. add hl,de ; point to table entry. ld e,(hl) ; low byte of screen address. inc hl ; point to high byte. ld d,(hl) ; high byte of screen address. ld a,(ycoord) ; horizontal position. rra ; divide by two. rra ; and again for four. rra ; shift again to divide by eight. and 31 ; mask away rubbish shifted into rightmost bits. add a,e ; add to address for start of line. ld e,a ; new value of e register. ret ; return with screen address in de. . . addtab defw 16384 defw 16640 defw 16896 . .
On the plus side this is very fast, but it does mean having to store each of the 192 line addresses in a table, taking up 384 bytes which might be better employed elsewhere.
Calculating Screen Addresses
The second method involves calculating the address ourselves and doesn’t require an address look-up table. In doing this we need to consider three things: Which third of the screen the point is in, the character line to which it is closest, and the pixel line upon which it falls within that cell. Judicious use of the and operand will help us to decide all three. It is a complicated business however, so please bear with me as I endeavour to explain how it works.
We can establish which of the three screen segments a point is situated in by taking the vertical coordinate and masking away the six least significant bits to leave a value of 0, 64 or 128 each of the segments being 64 pixels apart. As the high bytes of the 3 screen segment addresses are 64, 72 and 80 – a difference of 8 going from one segment to another – we take this masked value and divide it by 8 to give us a value of 0, 8 or 16. We then add 64 to give us the high byte of the screen segment.
Each segment is divided into 8 character cell positions which are 32 bytes apart, so to find that aspect of our address we take the vertical coordinate and mask away the two most significant bits we used to determine the segment along with the three least significant bits which determine the pixel position. The instruction and 56 will do nicely. This gives us the character cell position as a multiple of 8, and as the character lines are 32 bytes apart we multiply this by 4 and place our number in the low byte of the screen address.
Finally, character cells are further divided into pixel lines 256 bytes apart, so we again take our vertical coordinate, mask away everything except the bits which determine the line using and 7, and add the result to the high byte. That will give us our vertical screen address. From there we take our horizontal coordinate, divide it by 8 and add it to our address.
Here is a routine which returns a screen address for (xcoord, ycoord) in the de register pair. It could easily be modified to return the address in the hl or bc registers if desired.
scadd ld a,(xcoord) ; fetch vertical coordinate. ld e,a ; store that in e. ; Find line within cell. and 7 ; line 0-7 within character square. add a,64 ; 64 * 256 = 16384 = start of screen display. ld d,a ; line * 256. ; Find which third of the screen we're in. ld a,e ; restore the vertical. and 192 ; segment 0, 1 or 2 multiplied by 64. rrca ; divide this by 8. rrca rrca ; segment 0-2 multiplied by 8. add a,d ; add to d give segment start address. ld d,a ; Find character cell within segment. ld a,e ; 8 character squares per segment. rlca ; divide x by 8 and multiply by 32, rlca ; net calculation: multiply by 4. and 224 ; mask off bits we don't want. ld e,a ; vertical coordinate calculation done. ; Add the horizontal element. ld a,(ycoord) ; y coordinate. rrca ; only need to divide by 8. rrca rrca and 31 ; squares 0 - 31 across screen. add a,e ; add to total so far. ld e,a ; de = address of screen. ret
Once the address has been established we need to consider how our graphics are shifted into position. The three lowest bit positions of the horizontal coordinate indicate how many pixel shifts are needed. A slow way to plot a pixel would be to call the scadd routine above, perform an and 7 on the horizontal coordinate, then right shift a pixel from zero to seven times depending on the result before dumping it to the screen.
A shifter sprite routine works in the same way. The graphic image is taken from memory one line at a time, shifted into position and then placed on the screen before moving to the next line down and repeating the process. We could write a sprite routine which calculated the screen address for every line drawn, and indeed the first sprite routine I ever wrote worked in such a way. Fortunately it is much simpler to determine whether we’re moving within a character cell, crossing character cell boundaries, or crossing a segment boundary with a couple of and instructions and to increment or decrement the screen address accordingly. Put simply, and 63 will return zero if the new vertical position is crossing a segment, and 7 will return zero if it is crossing a character cell boundary and anything else means the new line is within the same character cell as the previous line.
This is a shifter sprite routine which makes use of the earlier scadd routine. To use it simply set up the coordinates in dispx and dispy, point the bc register pair at the sprite graphic, and call sprite.
sprit7 xor 7 ; complement last 3 bits. inc a ; add one for luck! sprit3 rl d ; rotate left... rl c ; ...into middle byte... rl e ; ...and finally into left character cell. dec a ; count shifts we've done. jr nz,sprit3 ; return until all shifts complete. ; Line of sprite image is now in e + c + d, we need it in form c + d + e. ld a,e ; left edge of image is currently in e. ld e,d ; put right edge there instead. ld d,c ; middle bit goes in d. ld c,a ; and the left edge back into c. jr sprit0 ; we've done the switch so transfer to screen. sprite ld a,(dispx) ; draws sprite (hl). ld (tmp1),a ; store vertical. call scadd ; calculate screen address. ld a,16 ; height of sprite in pixels. sprit1 ex af,af' ; store loop counter. push de ; store screen address. ld c,(hl) ; first sprite graphic. inc hl ; increment pointer to sprite data. ld d,(hl) ; next bit of sprite image. inc hl ; point to next row of sprite data. ld (tmp0),hl ; store in tmp0 for later. ld e,0 ; blank right byte for now. ld a,b ; b holds y position. and 7 ; how are we straddling character cells? jr z,sprit0 ; we're not straddling them, don't bother shifting. cp 5 ; 5 or more right shifts needed? jr nc,sprit7 ; yes, shift from left as it's quicker. and a ; oops, carry flag is set so clear it. sprit2 rr c ; rotate left byte right... rr d ; ...through middle byte... rr e ; ...into right byte. dec a ; one less shift to do. jr nz,sprit2 ; return until all shifts complete. sprit0 pop hl ; pop screen address from stack. ld a,(hl) ; what's there already. xor c ; merge in image data. ld (hl),a ; place onto screen. inc l ; next character cell to right please. ld a,(hl) ; what's there already. xor d ; merge with middle bit of image. ld (hl),a ; put back onto screen. inc hl ; next bit of screen area. ld a,(hl) ; what's already there. xor e ; right edge of sprite image data. ld (hl),a ; plonk it on screen. ld a,(tmp1) ; temporary vertical coordinate. inc a ; next line down. ld (tmp1),a ; store new position. and 63 ; are we moving to next third of screen? jr z,sprit4 ; yes so find next segment. and 7 ; moving into character cell below? jr z,sprit5 ; yes, find next row. dec hl ; left 2 bytes. dec l ; not straddling 256-byte boundary here. inc h ; next row of this character cell. sprit6 ex de,hl ; screen address in de. ld hl,(tmp0) ; restore graphic address. ex af,af' ; restore loop counter. dec a ; decrement it. jp nz,sprit1 ; not reached bottom of sprite yet to repeat. ret ; job done. sprit4 ld de,30 ; next segment is 30 bytes on. add hl,de ; add to screen address. jp sprit6 ; repeat. sprit5 ld de,63774 ; minus 1762. add hl,de ; subtract 1762 from physical screen address. jp sprit6 ; rejoin loop.
As you can see, this routine utilises the xor instruction to merge the sprite onto the screen, which works in the same way that PRINT OVER 1 does in Sinclair BASIC. The sprite is merged with any graphics already present on screen which can look messy. To delete a sprite we just display it again and the image magically vanishes.
If we wanted to draw a sprite on top of something that is already on the screen we would need some extra routines, similar to the one above. One would be required to store the graphics on screen in a buffer so that that portion of the screen could be re-drawn when the sprite is deleted. The next routine would apply a sprite mask to remove the pixels around and behind the sprite using and or or, then the sprite could finally be applied over the top. Another routine would be needed to restore the relevant portion of screen to its former state should the sprite be deleted. However, this would take a lot of CPU time to achieve so my advice would be not to bother unless your game uses something called double buffering – otherwise known as the back screen technique, or you’re using a pre-shifted sprites, which we shall discuss shortly.
Another method you may wish to consider involves making sprites appear to pass behind background objects, a trick you may have seen in Haunted House or Egghead in Space. While this method is handy for reducing colour clash it requires a sizeable chunk of memory. In both games a 6K dummy mask screen was located at address 24576, and each byte of sprite data was anded with the data on the dummy screen before being xored onto the physical screen located at address 16384. Because the physical screen and the dummy mask screen were exactly 8K apart it was possible to flip between them by toggling bit 5 of the h register. To do this for the sprite routine above our sprit0 routine might look like this:
sprit0 pop hl ; pop screen address from stack. set 5,h ; address of dummy screen. ld a,(hl) ; what's there already. and c ; mask away parts behind the object. res 5,h ; address of physical screen. xor (hl) ; merge in image data. ld (hl),a ; place onto screen. inc l ; next character cell to right please. set 5,h ; address of dummy screen. ld a,(hl) ; what's there already. and d ; mask with middle bit of image. res 5,h ; address of physical screen. xor (hl) ; merge in image data. ld (hl),a ; put back onto screen. inc hl ; next bit of screen area. set 5,h ; address of dummy screen. ld a,(hl) ; what's already there. and e ; mask right edge of sprite image data. res 5,h ; address of physical screen. xor (hl) ; merge in image data. ld (hl),a ; plonk it on screen. ld a,(tmp1) ; temporary vertical coordinate.
A shifter sprite routine has one major drawback: its lack of speed. Shifting all that graphic data into position takes time, and if your game needs a lot of sprites bouncing around the screen, you should consider using pre-shifted sprites instead. This requires eight separate copies of the sprite image, one in each of the shifted pixel positions. It is then simply a matter of calculating which sprite image to use based on the horizontal alignment of the sprite, calculating the screen address, and copying the sprite image to the screen. While this method is much faster it is fantastically expensive in memory terms. A shifter sprite routine requires 32 bytes for an unmasked 16×16 pixel sprite, a pre-shifted sprite requires 256 bytes for the same image. Writing a Spectrum game is a compromise between speed and available memory. In general I prefer to move my sprites 2 pixels per frame meaning the odd pixel alignments are not required. Even so, my pre-shifted sprites still require 128 bytes of precious RAM.
You may not necessarily want the same sprite image in each pre-shifted position. For example, by changing the position of a sprite’s legs in each of the pre-shifted positions a sprite can be animated to appear as if it is walking from left to right as it moves across the screen. Remember to match the character’s legs to the number of pixels it is moved each frame. If you are moving a sprite 2 pixels each frame it is important to make the legs move 2 pixels between frames. Less than this will make the sprite appear as if it is skating on ice, any more and it will appear to be struggling for grip. I’ll let you into a little secret here: believe it or not, this can actually affect the way a game feels so getting your animation right is important.