How To Write ZX Spectrum Games – Chapter 14
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.