Going in Circles

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!



3 thoughts on “Going in Circles”

  1. 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!).

  2. 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

  3. 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!

Leave a Reply

Your email address will not be published.