The math for moving an object around in a circle is quite straightforward, as we can use a couple of functions SIN and COS, which calculate the value of a position on a sine wave for a given angle, returning values between 0 and 1, here is the output of sin for angles 0-45 degrees!
sin(0°) = 0 sin(16°) = 0.275637 sin(32°) = 0.529919 sin(1°) = 0.017452 sin(17°) = 0.292372 sin(33°) = 0.544639 sin(2°) = 0.034899 sin(18°) = 0.309017 sin(34°) = 0.559193 sin(3°) = 0.052336 sin(19°) = 0.325568 sin(35°) = 0.573576 sin(4°) = 0.069756 sin(20°) = 0.34202 sin(36°) = 0.587785 sin(5°) = 0.087156 sin(21°) = 0.358368 sin(37°) = 0.601815 sin(6°) = 0.104528 sin(22°) = 0.374607 sin(38°) = 0.615661 sin(7°) = 0.121869 sin(23°) = 0.390731 sin(39°) = 0.62932 sin(8°) = 0.139173 sin(24°) = 0.406737 sin(40°) = 0.642788 sin(9°) = 0.156434 sin(25°) = 0.422618 sin(41°) = 0.656059 sin(10°) = 0.173648 sin(26°) = 0.438371 sin(42°) = 0.669131 sin(11°) = 0.190809 sin(27°) = 0.45399 sin(43°) = 0.681998 sin(12°) = 0.207912 sin(28°) = 0.469472 sin(44°) = 0.694658 sin(13°) = 0.224951 sin(29°) = 0.48481 sin(45°) = 0.707107
Cos will output a similar set of numbers as sin but at 90 degrees offset, therefore using sine and cos we could write something like this to get the position on a circle.
x = sin(angle)
y = cos(angle)
However it will be a very small circle as all the values are floating point values between 0 and 1, so on a game screen we would not see any movement due to the pixel resolution. Luckily there is a simple fix, we just need to multiply the values by the radius of our circle.
x = sin(angle) * radius
y = cos(angle) * radius
So now if we draw a sprite at the x and y position, and then add say 10 to the angle then draw another sprite and so on, we will see a circle forming!
for angle= 0 to 360 step 10
x = sin(angle) * radius
y = cos(angle) * radius
drawSprite(x,y)
loop
This would be great if we were writing in a higher language and did not care about speed, however floating point in z80 on the Spectrum next is slow and would not be suitable for a game engine, therefore we have to come up with a faster solution.
Tables, Tables and Tables
Pre Calculating the sine and cosines and storing them in a table before we run our code would speed things up greatly, however representing a floating point number in z80 is over complex as doing floating point math in z80 is hard and there is a much better solution.
Enter Fixed Point
A fixed-point number representation is a data type for a number that has a fixed number of digits before the point and a fixed number of digits after the point, which sounds simple but how do we do that in z80?
Remember the numbers were between 0 and 1 which means they only need a few bits before the point and we can use the rest of the bits as after the point for the fractions, so we could represent our sine and cosine values as a 2.6 fixed point number having 2 bits before the point and 6 fractional bits after the point.
Here are Sin and Cos tables in a 2.6 fixed point and some sin and cos functions in z80.
sinTable: .db $00,$02,$03,$05,$06,$08,$09,$0b,$0c,$0e .db $10,$11,$13,$14,$16,$17,$18,$1a,$1b,$1d .db $1e,$20,$21,$22,$24,$25,$26,$27,$29,$2a .db $2b,$2c,$2d,$2e,$2f,$30,$31,$32,$33,$34 .db $35,$36,$37,$38,$38,$39,$3a,$3b,$3b,$3c .db $3c,$3d,$3d,$3e,$3e,$3e,$3f,$3f,$3f,$40 .db $40,$40,$40,$40,$40 cosTable: .db $40,$40,$40,$40,$40,$40,$3f,$3f,$3f,$3e .db $3e,$3e,$3d,$3d,$3c,$3c,$3b,$3b,$3a,$39 .db $38,$38,$37,$36,$35,$34,$33,$32,$31,$30 .db $2f,$2e,$2d,$2c,$2b,$2a,$29,$27,$26,$25 .db $24,$22,$21,$20,$1e,$1d,$1b,$1a,$18,$17 .db $16,$14,$13,$11,$10,$0e,$0c,$0b,$09,$08 .db $06,$05,$03,$02,$02 //---------------------------------------------------------------------- // sin function // // in a degrees // // out e contains the sine 2.6 format // // dirty hl,de //---------------------------------------------------------------------- sin: ld h,0 // clear the high part of hl ld l,a // move the angle into the low part of hl ld de,sinTable // get the base of the sin table add hl,de // add the angle to the base to get the index into the sin Table ld e,(hl) // get the SIN ret // e contains the sine in 2.6 format //---------------------------------------------------------------------- // cosine function // in a degrees // // out e contains the cosine 2.6 format // // dirty hl,de //---------------------------------------------------------------------- cos: ld h,0 // clear the high part of hl ld l,a // move the angle into the low part of hl ld de,cosTable // get the base of the cos table add hl,de // add the angle to the base to get the index into the cos Table ld e,(hl) // get the COS ret // e contains the sine in 2.6 format
Notice the tables are very small (64 bytes each) and therefore not every angle in the 360 degree circle can be represented, which means I have limited my angles to 256 angles so the angle can fit into 8 bits also note that I have divided my circle into 4 quadrants hence 64 bytes for each of the tables.
360/256 gives you an angle step of 1.4 degrees give or take
To get the x and y coordinates in z80 is just a matter of working out which quadrant you are in, read the tables, and bit shifting them down 6 bits after the radius calculations are done.
Here is the code to do the magic, it could probably be a little faster, but hey it works!
// ---------------------------------------------------------------------- // position on the edge of a circle // input radius - store the radius in this variable before calling // input a,angle // input bc y center of the circle // input de x center of the circle // output bc x position on the circle // output de y position on the circle // // dirty everything //---------------------------------------------------------------------- radius: db 0 // store the radius here before calling posOnCircle: cp 64 // ae we less than 64 jp m,A_0_64 // yes but for some reason its not right so we will need another check cp 128 // are we between 64 and 128 jp m,A_64_128 // yup cp 192 // are we between 128 and 192 jp m,A_128_192 // yarp so do that code ret a_192_256: push bc // save the x center sub 192 // take off 192 making it 0-64 ld b,a // now reverse the angle ld a,64 // making it sub b // 64-0 push de // save the y center call getSinCos // get the cos and sin for this angle pop hl // get the y center sub hl,bc // take y center from the cos result ld bc,hl // and stick it in y return pos pop hl // now get the x center add hl,de // add the sin result ld de,hl // stick it in the x return pos ret // and return A_128_192: sub 128 push bc push de call getSinCos // get the cos and sin for this angle pop hl // get the y center sub hl,bc // take y center from the cos result ld bc,hl // and stick it in y return pos pop hl // now get the x center sub hl,de // take off the sin result ld de,hl // stick it in the x return pos ret A_64_128: push bc sub 64 ld b,a ld a,64 sub b push de call getSinCos // get the cos and sin for this angle pop hl // get the y center add hl,bc // add y center to the cos result ld bc,hl // and stick it in y return pos pop hl // now get the x center sub hl,de // take off the sin result ld de,hl // stick it in the x return pos ret A_0_64: bit 7,a // test to see if its negative by bit testing bit 7 jp nz,a_192_256 push bc push de call getSinCos // get the cos and sin for this angle pop hl // get the y center add hl,bc // add y center to the cos result ld bc,hl // and stick it in y return pos pop hl // now get the x center add hl,de // add the sin result ld de,hl // stick it in the x return pos ret //---------------------------------------------------------------------- // calculate the x y pos //---------------------------------------------------------------------- getSinCos: call sin // get the sin(angle) in e ld hl,radius ld d,(hl) // radius mul d,e // multiply sin by the radius ld b,6 // 2.6 fixed point bsrf de,b // do down shift the result push de // save x pos call cos // get the cos(angle) in e ld hl,radius ld d,(hl) // radius mul d,e // multiply sin by the radius ld b,6 // 2.6 fixed point bsrf de,b // do down shift the result pop bc // get the x pos ret
For other types of games you could easily modify the above code, to have a different radius for the x and the y which would give you ellipses instead of circles.
I have ripped this out of my game code for my shoot em up i am writing, as you can see I am increasing the radius of the sprites in the circles!
Enjoy!
Thanks Patricia, this is another excellent gift to the community. Look forward to getting my head around it (just got VS CODE environment working and am now building a linked list system – thank to your previous posts!).
As the cos and sin differ by 90 degrees you need a sine table of 0 to 360+90 – where 90 to 36+90 is the entries from the sin table.
As you are using speccy Next instructions you sin and cos can be written
sin: ld hl,sine_table
add hl,a
ld (hl),a
ret
and the same for cos
cos: ld hl,cos_table
add hl,a
ld (hl),a
ret
If you stick to 256 byte alignment for the tables
sin: ld h,sin_table>>8
ld l,a
ld (hl),a
ret
I found a bug in the comments detailing the calling convention, please see the comment at the top of posOnCircle its been updated.
input radius – store the radius in this variable before calling
Not h as stated previously!