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.
Simple Background Collision Detection
Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.
Anyone who ever spent time programming in Sinclair BASIC may well remember the ATTR function. This was a way to detect the colour attributes of any particular character cell on the screen, and though tricky for the BASIC programmer to grasp, could be very handy for simple collision detection. The method was so useful in fact that it its machine language equivalent was employed by a number of commercial games, and it is of great use to the novice Spectrum programmer.
There are two ways to find the colour attribute settings for a particular character cell on the Spectrum. A quick look through the Spectrum’s ROM disassembly reveals a routine at address 9603 which will do the job for us, or we can calculate the memory address ourselves.
The simplest way to find an attribute value is to use a couple of ROM routines:
ld bc,(ballx) ; put x and y in bc register pair. call 9603 ; call ROM to put attribute (c,b) on stack. call 11733 ; put attributes in accumulator.
However, it is much faster to do the calculation ourselves. It is also useful to calculate an attribute’s address, and not just its value, in case we want to write to it as well.
Calculating Attribute Addresses
Unlike the Spectrum’s awkward pixel layout, colour cells, located at addresses 22528 to 23295 inclusive, are arranged sequentially in RAM as one would expect. In other words, the screen’s top 32 attribute cells are located at addresses 22528 to 22559 going left to right, the second row of colour cells from 22560 to 22591 and so on. To find the address of a colour cell at print position (x,y) we therefore need only to multiply x by 32, add y, then add 22528 to the result. By then examining the contents of this address we can find out the colours displayed at a particular position, and act accordingly. The following example calculates the address of an attribute at character position (b,c) and returns it in the HL register pair.
; Calculate address of attribute for character at (b, c). atadd ld a,b ; x position. rrca ; multiply by 32. rrca rrca ld l,a ; store away in l. and 3 ; mask bits for high byte. add a,88 ; 88*256=22528, start of attributes. ld h,a ; high byte done. ld a,l ; get x*32 again. and 224 ; mask low byte. ld l,a ; put in l. ld a,c ; get y displacement. add a,l ; add to low byte. ld l,a ; hl=address of attributes. ld a,(hl) ; return attribute in a. ret
Interrogating the contents of the byte at hl will give the attribute’s value, while writing to the memory location at hl will change the colour of the square.
To make sense of the result we have to know that each attribute is made up of 8 bits which are arranged in this manner:
d0-d2 ink colour 0-7, 0=black, 1=blue, 2=red, 3=magenta, 4=green, 5=cyan, 6=yellow, 7=white d3-d5 paper colour 0-7, 0=black, 1=blue, 2=red, 3=magenta, 4=green, 5=cyan, 6=yellow, 7=white d6 bright, 0=dull, 1=bright d7 flash, 0=stable, 1=flashing
The test for green paper for example, might involve:
and 56 ; mask away all but paper bits. cp 32 ; is it green(4) * 8? jr z,green ; yes, do green thing.
while checking for yellow ink could be done like this:
and 7 ; only want bits pertaining to ink. cp 6 ; is it yellow (6)? jr z,yellow ; yes, do yellow wotsit.
Applying what we Have Learned to the Game
We can now add an attribute collision check to our Centipede game. As before, the new sections are underlined.
; We want a black screen. ld a,71 ; white ink (7) on black paper (0), ; bright (64). ld (23693),a ; set our screen colours. xor a ; quick way to load accumulator with zero. call 8859 ; set permanent border colours. ; Set up the graphics. ld hl,blocks ; address of user-defined graphics data. ld (23675),hl ; make UDGs point to it. ; Okay, let's start the game. call 3503 ; ROM routine - clears screen, opens chan 2. ; Initialise coordinates. ld hl,21+15*256 ; load hl pair with starting coords. ld (plx),hl ; set player coords. call basexy ; set the x and y positions of the player. call splayr ; show player base symbol. ; Now we want to fill the play area with mushrooms. ld a,68 ; green ink (4) on black paper (0), ; bright (64). ld (23695),a ; set our temporary colours. ld b,50 ; start with a few. mushlp ld a,22 ; control code for AT character. rst 16 call random ; get a 'random' number. and 15 ; want vertical in range 0 to 15. rst 16 call random ; want another pseudo-random number. and 31 ; want horizontal in range 0 to 31. rst 16 ld a,145 ; UDG 'B' is the mushroom graphic. rst 16 ; put mushroom on screen. djnz mushlp ; loop back until all mushrooms displayed. ; This is the main loop. mloop equ $ ; Delete the player. call basexy ; set the x and y positions of the player. call wspace ; display space over player. ; Now we've deleted the player we can move him before redisplaying him ; at his new coordinates. ld bc,63486 ; keyboard row 1-5/joystick port 2. in a,(c) ; see what keys are pressed. rra ; outermost bit = key 1. push af ; remember the value. call nc,mpl ; it's being pressed, move left. pop af ; restore accumulator. rra ; next bit along (value 2) = key 2. push af ; remember the value. call nc,mpr ; being pressed, so move right. pop af ; restore accumulator. rra ; next bit (value 4) = key 3. push af ; remember the value. call nc,mpd ; being pressed, so move down. pop af ; restore accumulator. rra ; next bit (value 8) reads key 4. call nc,mpu ; it's being pressed, move up. ; Now he's moved we can redisplay the player. call basexy ; set the x and y positions of the player. call splayr ; show player. halt ; delay. ; Jump back to beginning of main loop. jp mloop ; Move player left. mpl ld hl,ply ; remember, y is the horizontal coord! ld a,(hl) ; what's the current value? and a ; is it zero? ret z ; yes - we can't go any further left. ; now check that there isn't a mushroom in the way. ld bc,(plx) ; current coords. dec b ; look 1 square to the left. call atadd ; get address of attribute at this position. cp 68 ; mushrooms are bright (64) + green (4). ret z ; there's a mushroom - we can't move there. dec (hl) ; subtract 1 from y coordinate. ret ; Move player right. mpr ld hl,ply ; remember, y is the horizontal coord! ld a,(hl) ; what's the current value? cp 31 ; is it at the right edge (31)? ret z ; yes - we can't go any further left. ; now check that there isn't a mushroom in the way. ld bc,(plx) ; current coords. inc b ; look 1 square to the right. call atadd ; get address of attribute at this position. cp 68 ; mushrooms are bright (64) + green (4). ret z ; there's a mushroom - we can't move there. inc (hl) ; add 1 to y coordinate. ret ; Move player up. mpu ld hl,plx ; remember, x is the vertical coord! ld a,(hl) ; what's the current value? cp 4 ; is it at upper limit (4)? ret z ; yes - we can go no further then. ; now check that there isn't a mushroom in the way. ld bc,(plx) ; current coords. dec c ; look 1 square up. call atadd ; get address of attribute at this position. cp 68 ; mushrooms are bright (64) + green (4). ret z ; there's a mushroom - we can't move there. dec (hl) ; subtract 1 from x coordinate. ret ; Move player down. mpd ld hl,plx ; remember, x is the vertical coord! ld a,(hl) ; what's the current value? cp 21 ; is it already at the bottom (21)? ret z ; yes - we can't go down any more. ; now check that there isn't a mushroom in the way. ld bc,(plx) ; current coords. inc c ; look 1 square down. call atadd ; get address of attribute at this position. cp 68 ; mushrooms are bright (64) + green (4). ret z ; there's a mushroom - we can't move there. inc (hl) ; add 1 to x coordinate. ret ; Set up the x and y coordinates for the player's gunbase position, ; this routine is called prior to display and deletion of gunbase. basexy ld a,22 ; AT code. rst 16 ld a,(plx) ; player vertical coord. rst 16 ; set vertical position of player. ld a,(ply) ; player's horizontal position. rst 16 ; set the horizontal coord. ret ; Show player at current print position. splayr ld a,69 ; cyan ink (5) on black paper (0), ; bright (64). ld (23695),a ; set our temporary screen colours. ld a,144 ; ASCII code for User Defined Graphic 'A'. rst 16 ; draw player. ret wspace ld a,71 ; white ink (7) on black paper (0), ; bright (64). ld (23695),a ; set our temporary screen colours. ld a,32 ; SPACE character. rst 16 ; display space. ret ; Simple pseudo-random number generator. ; Steps a pointer through the ROM (held in seed), returning ; the contents of the byte at that location. random ld hl,(seed) ; Pointer ld a,h and 31 ; keep it within first 8k of ROM. ld h,a ld a,(hl) ; Get "random" number from location. inc hl ; Increment pointer. ld (seed),hl ret seed defw 0 ; Calculate address of attribute for character at (dispx, dispy). atadd ld a,c ; vertical coordinate. rrca ; multiply by 32. rrca ; Shifting right with carry 3 times is rrca ; quicker than shifting left 5 times. ld e,a and 3 add a,88 ; 88x256=address of attributes. ld d,a ld a,e and 224 ld e,a ld a,b ; horizontal position. add a,e ld e,a ; de=address of attributes. ld a,(de) ; return with attribute in accumulator. ret plx defb 0 ; player's x coordinate. ply defb 0 ; player's y coordinate. ; UDG graphics. blocks defb 16,16,56,56,124,124,254,254 ; player base. defb 24,126,255,255,60,60,60,60 ; mushroom.