Note: This article was originally written by Jonathan Cauldwell and is reproduced here with permission.
So far, we have moved sprites up, down, left and right by whole pixels. However, many games require more sophisticated sprite manipulation. Platform games require gravity, top-down racing games use rotational movement and others use inertia.
Jump and Inertia Tables
The simplest way of achieving gravity or inertia is to have a table of values. For example, the Egghead games make use of a jump table and maintain a pointer to the current position. Such a table might look like the one below.
; Jump table. ; Values >128 are going up,
With the pointer stored in jptr, we might do something like this:
ld hl,(jptr) ; fetch jump pointer. ld a,(hl) ; next value. cp 128 ; reached end of table? jr nz,skip ; no, we're okay. dec hl ; back to maximum velocity. ld a,(hl) ; fetch max speed. skip inc hl ; move pointer along. ld (jptr),hl ; set next pointer position. ld hl,verpos ; player's vertical position. add a,(hl) ; add relevant amount. ld (hl),a ; set player's new position.
To initiate a jump, we would set jptr to jtabu. To start falling, we would set it to jtabd.
Okay, so it’s a bit simplistic. In practice, we would usually use the value from the jump table as a loop counter, moving the player up or down one pixel at a time, checking for collisions with platforms, walls, deadly items etc as we go. We might also use the end marker (128) to signify that the player had fallen too far, and set a flag so that the next time the player hits something solid, he loses a life. That said, you get the picture.
If we want more sophisticated gravity, inertia, or rotational movement we need fractional coordinates. Up until now, with the Spectrum’s resolution at 256×192 pixels, we have only needed to use one byte per coordinate. If instead we use a two-byte register pair, the high byte for the integer and low byte for the fraction, we open up a whole new world of possibilities. This gives us 8 binary decimal places, allowing very precise and subtle movements. With a coordinate in the HL pair, we can set up the displacement in DE, and add the two together. When plotting our sprites, we simply use the high bytes as our x and y coordinates for our screen address calculation, and discard the low bytes which hold the fractions. The effect of adding a fraction to a coordinate will not be visible every frame, but even the smallest fraction, 1/256, will slowly move a sprite over time.
Now we can take a look at gravity. This is a constant force, in practice it accelerates an object towards the ground at 9.8m/s^2. To simulate it in a Spectrum game, we set up our vertical coordinate as a 16-bit word. We then set up a second 16-bit word for our momentum. Each frame, we add a tiny fraction to the momentum, then add the momentum to the vertical position. For example:
ld hl,(vermom) ; momentum. ld de,2 ; tiny fraction, 1/128. add hl,de ; increase momentum. ld (vermom),hl ; store momentum. ld de,(verpos) ; vertical position. add hl,de ; add momentum. ld (verpos),hl ; store new position. ret verpos defw 0 ; vertical position. vermom defw 0 ; vertical momentum.
Then, to plot our sprites, we simply take the high byte of our vertical position, verpos+1, to give us the number of pixels from the top of the screen. Different values of DE will vary the strength of the gravity, indeed we can even swap the direction by subtracting DE from HL, or by adding a negative distance (65536-distance). We can apply the same to the y coordinate too, and have the sprite subject to momentum in all directions. This is how we would go about writing a Thrust-style game.
The other thing we might need for a Thrust game, top-down racers, or anything where circles or basic trigonometry is involved is a sine/cosine table. Mathematics isn’t everybody’s cup of tea, and if your trigonometry is a little rusty I suggest you read up on sines and cosines before continuing with the remainder of this chapter.
In mathematics, we can find the x and y distance from the centre of a circle given the radius and the angle by using sines and cosines. However, whereas in maths a circle is made up of either 360 degrees or 2 PI radians, it is more convenient for the Spectrum programmer to represent his angle as, say, an 8-bit value from 0 to 255, or even use fewer bits, depending on the number of positions the player sprite can take. He can then use this value to look up his 16-bit fractional values for the sine and cosine in a table. Assuming we have an 8-bit angle set up in the accumulator, and we wish to find the sine, we simply access the table in a manner similar to this:
ld de,2 ; tiny fraction - 1/128. ld l,a ; angle in low byte. ld h,0 ; zero displacement high byte. add hl,hl ; double displacement as entries are 16-bit. ld de,sintab ; address of sine table. add hl,de ; add displacement for this angle. ld e,(hl) ; fraction of sine. inc hl ; point to second half. ld d,(hl) ; integer part. ret ; return with sine in de.
Sinclair BASIC actually provides us with the values we require, with its SIN and COS functions. Using this, we can POKE the values returned into RAM and either save to tape, or save out the binary using an emulator such as SPIN. Alternatively, you may prefer to use another programming language on the PC to generate a table of formatted sine values. to import into your source file, or include as a binary. For a sine table with 256 equally-spaced angles, we would need a total of 512 bytes, but we would need to be careful to convert the number returned by SIN into one our game will recognise. Multiplying the sine by 256 will give us our positive values, but where SIN returns a negative result, we might need to multiply the ABS value of the sine by 256, then either subtract that from 65536 or set bit d7 of the high byte to indicate that the number must be subtracted rather than added to our coordinate. With a sine table constructed in this manner, we don’t need a separate table for cosines, as we just add or subtract 64, or a quarter-turn, to the angle before looking up the value in our table. To move a sprite at an angle of A, we add the sine of A to one coordinate, and the cosine of A to the other coordinate. By changing whether we add or subtract a quarter turn to obtain the cosine, and which plane uses sines and which uses cosines, we can start our circle at any of the 4 main compass points, and make it go in a clockwise or anti-clockwise direction.
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.
This tutorial is by Derek M. Smith © 2005.
Sprite graphics are the backbone of arcade games. Put simply, a sprite is a moveable screen object, such as a spaceship, alien, or anything else you can imagine. Some computers come with built in sprites, such as the Commodore and Atari ranges. These machines could generate sprites without the need for complex programming, and on top of this, they used an additional processor, which left the main CPU free to get on with the rest of the program. The Spectrum sadly was never adorned with such luxuries and so programmers had to write their own sprite routines from scratch (a daunting task for the amateur).
Sloppy programming in this area could really spoil a game, either making it too slow, or through flickering graphics.
A simple sprite routine is often one of the first things a new programmer, after having mastered the basics of machine code, will have a go at.
At first it seems straight forward, but soon all sorts of hurdles appear. The first usually being the Spectrum’s unusual screen arrangement. More experienced programmers who have mastered the basic sprite handling routine will then seek ways to optimise it.
This tutorial is aimed at both beginners and the more experienced programmers. Beginners will learn the principles behind sprite programming and the experienced machine coders will learn to find ways of improving their routines. For simplicity, this tutorial assumes that the reader has a decent grasp of assembly language.
ZX Spectrum screen arrangement
“The display file stores the television picture. It is rather curiously laid out…” -Spectrum Manual ch24 p164
On the face of it the Spectrum’s screen arrangement is pretty strange. Just watch the screen of a game loading, and you’ll understand what I mean. Why does it skip lines like that? Sure it may be interesting to watch the picture being gradually built up, but it can be a real pain when it comes to writing a sprite routine (at least until you understand the principles behind its layout).
First off, the Spectrum screen has a resolution of 256 pixels across by 192 down, not including the border around it. It can display 8 colours (including black and white) with two levels of intensity (brightness).
The Memory Map for the screen starts at address 16384 and is 6912 bytes long.
It is split in to two halves with the first 6144 bytes containing the bit-map (or pixel map, if you wish) and the remaining 768 bytes containing the attribute-map.
Consider the bit-map first:-
Each line of 256 pixels is stored as 32 bytes: 32 x 8 = 256. So far so good.
Now you would think that each line would follow on from the one before in the pixel map, and most people (myself included) write their first sprite routine thinking this to be the case – only to find that when they execute the routine, with a shudder of anticipation, their sprite is spread all over the screen. At this point some give up (or decide to write adventure games instead) baffled by the Spectrum’s idiosyncrasies.
Let me say now that when you do grasp the screen layout and the techniques used for addressing it, I think you will be glad that the designers done it that way.
Type in the following and run it:
10 FOR S = 16384 TO 22527 20 POKE S, 255 30 NEXT S
This short BASIC program fills the screen. Although it is moving through memory sequentially, POKING 255 into each memory location, the screen fills up in a rather more esoteric manner. Run the program a few times and watch the pattern it traces. You’ll notice two things: One is the way it skips lines, another is that the screen is divided into three parts.
I said earlier that each line is stored as 32 bytes, with the first line beginning at 16384. Where does the second line start? Most of us assume it would start at 16384+32. In fact it starts at 16384+256 and 16384+32 takes us down to line 8. Remember that the screen is used to display text (characters) as well as graphics. So adding 32 actually takes us down one character row (8 screen lines).
In machine code 16bit addresses, such as 16384, are stored in high byte / low byte format. The high byte is equal to ADDRESS / 256 and the low byte is the remainder from the division, so 16384 in high/low byte format would be 64, 0 (16384 / 256 = 64 with no remainder). This means that when we increase the high byte of a 16 bit address by one, it is equivalent to adding 256. So when it comes to screen addressing all we need to do to move down a line is increase the high byte of the address. This can be done quite easily and quickly in machine code:
ld hl,16384 ;load the hl register pair with the address of the start of the display file inc h ;increment the high byte (4 tstates)
This is in fact much quicker than adding 32 where we would have to do the following:
ld hl,16384 ld a,l ;load the accumulator with the l register as we cannot add ;directly to the l register add a,32 ;add 32 ld l,a ;load the result back into the l register (total time 15 tstates)
The above works within a group of 8 lines, ie. 1 char line.
Now, as mentioned before, the other peculiar thing about the screen layout is that it is divided in to three parts – top, middle and bottom. Each third of the screen has 64 lines (or 8 character rows) and takes up 2048 bytes of memory. All that has been said so far applies only so long as we don’t cross from one third into another.
The whole matter becomes at good deal clearer if we look at the screen address in binary.
High Byte | Low Byte 0 1 0 T T L L L Cr Cr Cr Cc Cc Cc Cc Cc
I have used some abbreviations to make things a bit clearer:
T – these two bits refer to which third of the screen is being addressed: 00 – Top, 01 – Middle, 10 – Bottom
L – these three bits indicate which line is being addressed: from 0 – 7, or 000 – 111 in binary
Cr – these three bits indicate which character row is being addressed: from 0 – 7
Cc – these five bits refer to which character column is being addressed: from 0 – 31
The top three bits ( 010 ) of the high byte don’t change.
Calculating the screen address
The first task in putting a sprite on the screen is to translate the X,Y coords into a screen address.
There are two ways of doing this. One is to set up a look-up table which contains 192 addresses corresponding to each screen line. The other way is a bit more interesting and involves distilling the appropriate line, column and row bits that make up the address from the X and Y coords. Lets examine this way first.
Taking the Y coord first: This will be in the range 0 – 191, with 0 corresponding to the top of the screen. The lowest three bits indicate which line (within a character row) 0 – 7 we are dealing with. This is the same as the high byte of the screen address. The top two bits refer to which third of the screen we are dealing with:
Y Coord T T - - - L L L
Let us assume that the B & C registers contain our X & Y coords. First we need to isolate the lowest three bits of the Y coord (C reg.), as follows:
ld a,c and %00000111 ;% indicates that the number following it is in binary format
We will use the HL register pair for the screen address. So next we transfer these three bits to the high byte (H reg.)
In order to get the top two bits into the correct position we must shift them right three times, as follows:
ld a,c rra ;rotate right accumulator rra rra
This shifts all the bits to the right three times, with the highest bit being reset after each shift. All that’s left to do at this stage is to clear the bits we don’t need, as follows:
This must now be ORd with the H register.
Then the top three bits, which remain constant, are set as appropriate:
or %01000000 ld h,a ;load the result back into the H reg.
So now we have the high byte of the screen address.
The low byte of the address is composed of the following bits of both the X and Y coords:
X Coord | Y Coord Cc Cc Cc Cc Cc - - - - - Cr Cr Cr - - - Low Byte Cr Cr Cr Cc Cc Cc Cc Cc
The character column bits need to be shifted to the right three times.
ld a,b ;B reg. holds the X coord rra rra rra and %00011111 ; ensure the top 3 bits are clear ld l,a
Then the character row bits must be shifted left twice so that they correspond to the highest three bits of the low byte of the address (see earlier).
ld a,c ;C reg. holds the Y coord rla ;rotate left accumulator rla and %11100000 ;isolate the character row bits or l ;OR the result with the low byte ld l,a ;and place in L register
Right, that’s the difficult part done. We now have the screen address in HL. All that is left is to get the pixel position (0 – 7 from left to right) from the X coord (lowest three bits).
ld a,b and %00000111
So the complete routine is as follows:
On Entry: B reg = X coord, C reg = Y coord On Exit: HL = screen address, A = pixel position ; Calculate the high byte of the screen addressand store in H reg. ld a,c and %00000111 ld h,a ld a,c rra rra rra and %00011000 or h or %01000000 ld h,a ; Calculate the low byte of the screen address and store in L reg. ld a,b rra rra rra and %00011111 ld l,a ld a,c rla rla and %11100000 or l ld l,a ; Calculate pixel postion and store in A reg. ld a,b and %00000111
Using a Look-up Table
The other method of calculating the screen address is to use a table of pre-calculated addresses, and then use the Y coord to pick out the right one. This is a good deal quicker than the above method, albeit at the cost of 384 bytes of memory for the table. The look-up table contains the address of each line in the display file, 192 lines.
Routine to generate a Screen Address Table:
scradtab equ 64000 gentab ld de,16384 ld hl,scradtab ld b,192 lineloop ld (hl),e inc l ld (hl),d inc hl inc d ld a,d and 7 jr nz,nextline ld a,e add a,32 ld e,a jr c,nextline ld a,d sub 8 ld d,a nextline djnz lineloop ret
Assuming again that on entry the B and C registers contain our X & Y coords.
ld de,scradtab ;address of look-up table ld l,c ;Y coord in L register ld h,0 add hl,hl ;multiply by two, as each address is 2 bytes add hl,de ;add to the start address of the table. HL is now at the ;appropriate point in the table ld a,(hl) ;get low byte of screen address; store in A temporarily ;so as not to corrupt HL inc l ld h,(hl) ;get high byte ld l,a ;HL now contains the address of the start of the line ld a,b ;calculate character column 0 - 31 from X coord and %11111000 ;isolate appropriate bits rrca ;rotate right circular accumulator (faster than srl a) rrca rrca ;shifting three times is the same as dividing by eight add a,l ;add to low byte of address ld l,a ;put result back in L reg.
On exit HL will contain the screen address.
An even quicker way is to arrange the lookup table so that it is aligned with a 256 byte page boundary. This method separates the low byte and the high byte of the screen addresses, so in effect there are two tables one containing the low bytes of the address of each screen line and one containing the high bytes. Each must be aligned with a page boundary, ie. the start address of the table must be cleanly divisible by 256.
The above routine for generating the screen address table needs the following changes made to it:
lineloop ld (hl),e inc h ld (hl),d dec h inc l
The table would be filled as follows:
64000 - 64191 0....... 32....... 64....... 96....... 128....... Low bytes of screen addresses 64256 - 64447 64,65,66....71, 64,65,66....71, 64,65,66....71.... High bytes of screen addresses
Finding the start address of a particular line is then done as follows:
ld h,scradtab/256 ; high byte of start of table ld l,c ; C reg. contains the Y coord ld a,(hl) ; already we have found the low byte, so store in A temporarily inc h ; increasing high byte moves forward 256 bytes, and to the ld h,(hl) ; corresponding high byte of the screen address ld l,a ; HL now contains the screen address for the start of the line
All that is then needed is to add on the column position, which is calculated in the same way as the previous routine.
The main factors which need consideration when writing a sprite routine are:
1. Whether or not to use masked sprites
2. Whether or not to use pre-shifted sprites
3. What sizes of sprite to use and whether to use a different routine tailored for each size or a generic
routine which will produce various sizes of sprite
Writing a generic sprite routine which can handle many sizes and types of sprite, is generally shunned by most programmers, because the complexity of the routine has a negative impact on performance. It is perferable to write several routines tailored to specific sizes of sprite.
As per usual there is a trade-off between memory and speed, with the fastest routines using the most memory.
Masked sprites on average will take about 30% more time to process.
Pre-shifted sprites, which are sprites stored in 2, 4 or 8 different pixel positions, are by far the fastest type, but consume lots of memory.
An alternative to using pre-shifted sprites favored by some programmers, is to have a look-up table containing each bit pattern from 0 – 255 shifted into eight positions. In practice this takes up 4K of memory but this is usually less than is required when pre-shifting a large number of sprites. More about this later.
A Worked Example
It’s now time to take a look at how to code a basic sprite routine. The example below is for an 8 x 8 non-masked sprite. The sprite data (a small arrow pointer) is located at USR “A” – 65368. The routine shifts the data in real time. The sprite is XORed with the contents of the screen. You could also OR the sprite with the screen contents (see below for explanation of OR, XOR etc.) Real time rotation of sprite data is easiest acheived with small sprites (16 pixels or less wide), as the data can be stored in registers while being rotated.
The steps in the algorithm to draw the sprite could be stated as follows:
1. Retrieve X and Y coords.
2. Calculate Screen Address based on X,Y coords, using a look-up table
3. Calculate Bit Position from X coord (X coord AND 7 = bit position)
4. Retrieve a line of the sprite graphics data
5. Check if Bit Position is zero
6. If so there is no need to shift the sprite data, so skip the code which shifts the data (jump to step 8 )
7. Shift the sprite data according to its bit position
8. Put the line of sprite data on the screen
9. Adjust screen address for next line
10. Perform steps 4-9 until all lines have been drawn
ORG 50000 SPRITE DI ;Disable Interupts. Not strictly necessary in this ;example as we are not redirecting the Stack. LD BC,(XPOS) ; First off, get the X & Y coords ; and place them in B & C registers LD H,SCRADTAB/256 ; this next section calculates the LD L,B ; screen address using a lookup table LD A,(HL) ; as explained earlier in the tutorial INC H LD H,(HL) LD L,A LD A,C AND 248 RRCA RRCA RRCA ADD A,L LD L,A LD (SCRADD),HL ; store screen address for later ; as HL is needed again LD A,C ; find which pixel position the sprite AND 7 ; will be at and store it. We need this to LD (BITPOS),A ; know how many times to shift the sprite data LD HL,SPRGFX ; start address for the sprite graphic LD C,8 ; The sprite is 8 lines tall LINELOOP LD E,(HL) ; load the E reg with the first line of INC L ; sprite data, and move forward PUSH HL ; preserve this address for later LD A,(BITPOS) ; Retrieve the pixel position OR A ; Quick way of testing if A is zero - Note: A is unaffected JR Z,SKIPROTATE ; If zero then no shifting is needed LD B,A ; loop counter for number of times to ; rotate (shift) sprite data XOR A ; Quick way setting A to zero and ; clearing the Carry Flag. The Carry Flag must be reset ; as the rotate instructions will shift its contents ; into the sprite data. ROTATELOOP RR E ; An extra register is needed for the RRA ; shifted sprite data. After the RR E is executed ; rightmost bit of sprite data is shifted out to the Carry Flag. ; It is then shifted into the A reg by RRA. So no data is lost. DJNZ ROTATELOOP ; Loop back until shifting is complete SKIPROTATE LD D,A ; Store in D register, as A will be ; needed for another purpose. E & D regs now contain the ; shifted sprite data. LD HL,(SCRADD) ; Get back the screen address LD A,(HL) ; Actually put a line of the sprite on the screen XOR E LD (HL),A INC L LD A,(HL) XOR D LD (HL),A DEC L ; move back and down one line INC H LD A,H ; It is not necessary to recalculate AND 7 ; the screen address for each line of JR NZ,A1 ; the sprite. All that is needed is to LD A,L ; check if a char. or segment boundary ADD A,32 ; has been crossed and adjust address LD L,A ; accordingly. JR C,A1 LD A,H SUB 8 LD H,A A1 LD (SCRADD),HL ; store it again POP HL ; retrieve the address of the next line ; of sprite data DEC C JP NZ,LINELOOP ; loop back until all lines are drawn. EI ; Enable Interupts again RET XPOS DEFB 0 YPOS DEFB 0 SCRADD DEFW 0 BITPOS DEFB 0 SCRADTAB EQU 64000
On Exit : A, BC, DE, HL corrupt
As can be seen from the above routine it is not necessary to re-calculate the screen address for every line of a sprite. A couple of checks can be made to test if the next line to be drawn is in a new character row or screen third and adjust the address accordingly:
LD A,H ;HL contains screen address AND 7 JR NZ, REST_OF_PROGRAM LD A,L ADD A,32 LD L,A JR C, REST_OF PROGRAM LD A,H SUB 8 LD H,A Rest Of Program continues here...
Routine to generate a table of screen addresses
GENTAB LD DE,16384 LD HL,SCRADTAB LD B,192 LINELOOP LD (HL),E INC H LD (HL),D DEC H INC L INC D LD A,D AND 7 JR NZ,NEXTLINE LD A,E ADD A,32 LD E,A JR C,NEXTLINE LD A,D SUB 8 LD D,A NEXTLINE DJNZ LINELOOP RET
Sprite Graphics Data
ORG 65368 SPRGFX DEFB %00000000 DEFB %01111000 DEFB %01110000 DEFB %01111000 DEFB %01011100 DEFB %00001110 DEFB %00000100 DEFB %00000000
OR, XOR, AND
00111100 OR 00011000 = 00111100 00111100 XOR 00011000 = 00100100 00111100 AND 00011000 = 00011000
Masking is a technique used to blank out the area behind a sprite before drawing the sprite to the display. It’s slower than planar (non-masked) sprites, and uses twice as much memory but the final result is more pleasing to the eye. If your sprites are going to be moving over a patterned background its the best technique to use.
The way to draw a masked sprite is to AND the mask with background and then OR the actual sprite with the result. When you AND two bitmaps, bits in the BACKGROUND are reset by zeros in the MASK, and left as they are by ones. That’s probably not very easy to grasp so look at the following examples:
background mask 00111100 and 11111111 = 00111100 ; each bit in the data byte remains the same after the mask is applied 00111100 and 00000000 = 00000000 ; all the bits in the data byte are reset by the mask 11111111 and 00111100 = 00111100 ; the middle four bits remain unchanged and the rest are reset
The previous example showed how to code a very simple sprite routine. Lets now look at a more complex example. This routine will draw a 16 x 16 Masked Sprite, storing the background underneath in a buffer.
This is probably the largest size that can be conveniently shifted in real-time. Any larger and you will want to use a different technique.
The following routine uses a couple of non-standard programming techniques. These tricks help to speed up execution but make the code a bit more difficult to follow. One technique involves using the Stack to retrieve the sprite data, the other is self-modifying code, where the program pokes values into instructions that will be executed later on in the routine.
ORG 50000,50000 SPRITE DI ; Disable Interupts. LD (SPTEMP),SP ; Store Stack Pointer as it will be redirected ; by the routine LD BC,(XPOS) LD A,C AND 7 LD (BITPOS),A LD H,SCRADTAB/256 ; Calculate screen address as before LD L,B LD A,(HL) INC H LD H,(HL) LD L,A LD A,C AND 248 RRCA RRCA RRCA ADD A,L LD L,A LD (SCRADD),HL LD IX,BKGRNDBUF ; IX is used to point to a temporary buffer where the contents ; of the screen under the sprite are stored. LD SP,SPRGFX ; start address for the sprite graphic LD A,16 ; The sprite is 16 lines tall LD (LINECOUNT),A LINELOOP POP DE ; Using the stack to retrieve a line of sprite graphics POP HL ; E and D will contain the MASK as it is stored first ; L and H hold the actual sprite image LD A,255 ; When shifting, the A reg will be the extra register that the MASK ; is shifted into. It is loaded with 255 as that means all the bits are ; initially transparent. SCF ; The carry flag is set by this instruction so that bits shifted into the ; left side of the mask are set (transparent) EX AF,AF' ; We also need an extra register for the sprite image, so we will use ; the alternate or shadow A register. LD A,(BITPOS) ; Retrieve the pixel position OR A ; Quick way of testing if A is zero JR Z,SKIPROTATE ; If zero then no shifting is needed LD B,A ; loop counter for number of times to shift XOR A ; clear carry flag and set A reg. to zero, ; as the rotate instructions will shift its contents (carry flag's) ; into the sprite image data. This is the opposite to the MASK. ROTATELOOP EX AF,AF' ; Shift the Sprite Mask data RR E RR D RRA EX AF,AF' ; Shift the Sprite Image data RR L RR H RRA DJNZ ROTATELOOP ; Loop back until shifting is complete SKIPROTATE LD (DATA+1),A ; This is the piece of self-modifying code mentioned earlier. EX AF,AF' ; As the A reg will be needed and there are no other registers LD (MASK+1),A ; free, we will poke their data (the rightmost bytes of the sprite mask ; and image) into instructions further on in the routine. LD BC,(SCRADD) ; Get back the screen address LD A,(BC) ; Actually put a line of sprite data on the screen LD (IX),A ; Store what is under the sprite so that it can be erased ; without corrupting the background AND E ; This instruction applies the MASK OR L ; The sprite image is then ORed with the background LD (BC),A ; and the result copied back to the screen INC C INC IXL LD A,(BC) ; Put the second byte on the screen LD (IX),A ; As we are dealing with a 16 x 16 sprite AND D ; each line will be three bytes wide when shifted OR H LD (BC),A INC C INC IXL LD A,(BC) ; Put the third byte on the screen LD (IX),A MASK AND 255 ; These are the two instructions that were modified DATA OR 0 ; earlier. The data stored in the A reg. and its shadow ; were poked into the AND & OR instructions. LD (BC),A INC IXL DEC C DEC C ; move back and down one line INC B LD A,B ; It is not necessary to recalculate AND 7 ; the screen address for each line of JR NZ,A1 ; the sprite. All that is needed is to LD A,C ; check if a char. or segment (screen third) boundary ADD A,32 ; has been crossed and adjust the address LD C,A ; accordingly. JR C,A1 LD A,B SUB 8 LD B,A A1 LD (SCRADD),BC ; store it again LD HL,LINECOUNT DEC (HL) JP NZ,LINELOOP ; loop back until all lines are drawn. LD SP,(SPTEMP) ; Restore the Stack Pointer EI ; Enable Interupts again RET
In order to clear the sprite we need to copy back to the screen the Buffer holding the contents
of the screen under the sprite. The following routine will do this.
CLEARSPRITE DI LD BC,(OLDXPOS) LD H,SCRADTAB/256 LD L,B LD A,(HL) INC H LD H,(HL) LD L,A LD A,C AND 248 RRCA RRCA RRCA ADD A,L LD L,A LD DE, BKGRNDBUF LD C,16 CLOOP LD A,(DE) LD (HL),A INC L INC E LD A,(DE) LD (HL),A INC L INC E LD A,(DE) LD (HL),A INC E DEC L DEC L INC H LD A,H AND 7 JR NZ,A2 LD A,L ADD A,32 LD L,A JR C,A2 LD A,H SUB 8 LD H,A A2 DEC C JP NZ,CLOOP EI RET XPOS DEFB 0 YPOS DEFB 0 OLDXPOS DEFB 0 OLDYPOS DEFB 0 LINECOUNT DEFB 0 BITPOS DEFB 0 SCRADD DEFW 0 SPTEMP DEFW 0 SCRADTAB EQU 64000 BKGRNDBUF EQU 64512
; ASM source file created by SevenuP v1.12 ; SevenuP (C) Copyright 2002-2004 by Jaime Tejedor G¢mez, aka Metalbrain ;GRAPHIC DATA: ;Pixel Size: ( 16, 16) ;Char Size: ( 2, 2) ;Sort Priorities: X char, Char line, Y char, Mask ;Attributes: No attributes ;Mask: Yes, mask before SPRGFX DEFB 248, 31, 0, 0 DEFB 224, 7, 3,192 DEFB 192, 3, 15,240 DEFB 128, 1, 25,248 DEFB 128, 1, 51,252 DEFB 0, 0, 39,252 DEFB 0, 0,111,250 DEFB 0, 0,127,252 DEFB 0, 0,127,250 DEFB 0, 0,127,252 DEFB 0, 0, 63,248 DEFB 128, 1, 63,244 DEFB 128, 1, 31,232 DEFB 192, 3, 13, 80 DEFB 224, 7, 2,128 DEFB 248, 31, 0, 0
Addendum: There appears to be a minor bug in the original code by Derek Smith. Commentator uglifruit posted a fix in the comments section, which has now been integrated into the above code.