- Circular Motion In z80 Assembly
- 08 May 2014 09:32:15 pm
- Last edited by MateoConLechuga on 15 May 2014 10:37:46 pm; edited 7 times in total
After looking around on multitudes of sites, it seems as though there are hardly any topics regarding circular motion. Hopefully, this will attempt to elaborate a little on this idea. It might seem long at first, but it really isn’t all that bad.
Let's say that you have an object in one of your programs, and you want to move it in a circle. Now, depending on your program, this section could be set up however you may like, it is simply up to you. For this example, we will be using an object defined by 7 bytes: One for type, such as clockwise vs. counterclockwise, two bytes for current location, two more for center of the circle, one for the radius, and one for the counting the amount of rotation the object has gone through, ending up with something like this:
Code:
This may not make much sense why those numbers are the way they are, but it will become clearer as we progress.
Now, to set up the movement routines. If you are a little unsure on how sine and cosine behave in each quadrant, I would recommend that you looks here: Sine and Cosine Behavior
Now, the question is, how do we use these sine and cosine values? Well, instead of using the TI-OS routines to get these values, we are going to use a LUT. We will split up each quadrant into 21 different parts, for a total of 84 parts per circle. For those of you who feel like a more appropriate number of parts is more reasonable, feel free to split it up however you like. And instead of trying to use these values as a fixed point number, which could be slow if you are programming an intensive side-scrolling engine, we are going to convert them using the formula int(128sin(θ)), where θ takes on the the values between 0 and 90 for each quadrant. To get the values we want, we can use some quick BASIC code, starting θ at 1 in order to skip the omnipresent 0:
Code:
We then chop off the first and last elements of the list, because we do not want them to be used twice. Because cosine is closely related to sine, we can just reverse the order to get the cosine list. So, once we plug these two lists into our code, it should look like this:
Code:
You may have noticed that some elements are a little off; this is just done after the main code is written; don't worry about it right now.
Now, having refreshed yourself with sine and cosine, lets take a look at some code:
Code:
Okay, now, this might be somewhat overwhelming. So let's go through this. In Rotate1, the object begins on the right end of the x-axis, because the counter location in ObjectData is set to 21. Then, as the first byte in ObjectData is 0, it will rotate counterclockwise, and increments the counter. Then, it checks to see how much it should move the object based on its origin and radius. For further reference, check the code. To do this, it shifts HL right by 7 bits, thus undoing our previous multiplication by 128, and then proceeds to add this to the origin values, storing the result in a different part of ObjectData, so that it is easier to draw this information later on.
Why did we change some values earlier? So that the motion would look smoother.
Hopefully this has been some help, I've also included an example program using this technique. If at first you do not understand, check out this program, and take a look at the commented lines. Good luck!
Example Program: Circle Demo
Any comments/suggestions?
Let's say that you have an object in one of your programs, and you want to move it in a circle. Now, depending on your program, this section could be set up however you may like, it is simply up to you. For this example, we will be using an object defined by 7 bytes: One for type, such as clockwise vs. counterclockwise, two bytes for current location, two more for center of the circle, one for the radius, and one for the counting the amount of rotation the object has gone through, ending up with something like this:
Code:
ObjectData:
.db 0,0,0,32,47,10,21
(Type),(CurrentY),(CurrentX),(CenterY),(CenterX),(Radius),(Counter)
This may not make much sense why those numbers are the way they are, but it will become clearer as we progress.
Now, to set up the movement routines. If you are a little unsure on how sine and cosine behave in each quadrant, I would recommend that you looks here: Sine and Cosine Behavior
Now, the question is, how do we use these sine and cosine values? Well, instead of using the TI-OS routines to get these values, we are going to use a LUT. We will split up each quadrant into 21 different parts, for a total of 84 parts per circle. For those of you who feel like a more appropriate number of parts is more reasonable, feel free to split it up however you like. And instead of trying to use these values as a fixed point number, which could be slow if you are programming an intensive side-scrolling engine, we are going to convert them using the formula int(128sin(θ)), where θ takes on the the values between 0 and 90 for each quadrant. To get the values we want, we can use some quick BASIC code, starting θ at 1 in order to skip the omnipresent 0:
Code:
For(θ,1,90,4 ; Can Change Step To However Much You Want To Advance By Each Time
int(128sin(θ→L₁((θ+3)/4
End
We then chop off the first and last elements of the list, because we do not want them to be used twice. Because cosine is closely related to sine, we can just reverse the order to get the cosine list. So, once we plug these two lists into our code, it should look like this:
Code:
SineTable:
.db 11,16,28,37,46,53,61,69,76,84,90,96,102,107,111,116,119,122,124,126,126 ; Sine Table
CosineTable:
.db 126,126,124,122,119,116,111,107,102,96,90,84,76,69,61,53,46,37,28,16,11 ; Cosine Table
You may have noticed that some elements are a little off; this is just done after the main code is written; don't worry about it right now.
Now, having refreshed yourself with sine and cosine, lets take a look at some code:
Code:
MoveObjectClockwise:
ld a,(ix+6) ; Load Counter Into Register A
dec a ; Once It Gets To 0, Reset To 84
jr nz,NoResetCounter
ld a,84 ; 84 = 21 Parts Per Quadrant*4
jr NoResetCounter
MoveObjectCounterClockwise:
ld a,(ix+6) ; Load Counter Into Register A
inc a
cp 85 ; Once It Passes 84
jr nz,NoResetCounter
ld a,1 ; Start Back At 1
NoResetCounter:
ld h,$00
ld (ix+6),a
cp 22
jp c,Rotate1 ; In First Quadrant
cp 22
jp c,Rotate2 ; In Second Quadrant
cp 64
jp c,Rotate3 ; In Third Quadrant
; **NOTE** Take A Look At Rotate1
Rotate4: ; In Forth Quadrant
sub 63
ld l,a ; Load Counter Into Register L
push hl
ld de,CosineTable
call Multiply
ld a,(ix+4)
sub l
ld (ix+2),a
pop hl
ld de,SineTable
call Multiply
ld a,(ix+3)
add a,l
ld (ix+1),a
ret
Rotate1:
ld l,a ; Load Counter Into Register L
push hl
ld de,SineTable ; Since Were In Quadrant One, Load Sine Table
call Multiply ; Multiply and Divide To Get Current Y Value To Add
ld a,(ix+4) ; Load Y Value Of The Circle's Center
add a,l ; Add Y Value To A, Because In Quadrant One
ld (ix+2),a ; Store Back Into Object's Y Posistion
pop hl
ld de,CosineTable ; Since Were In Quadrant One, Load Cosine Table
call Multiply ; Multiply and Divide To Get Current X Value To Add
ld a,(ix+3) ; Load Center Of Circle X
add a,l ; Add X Value To A, Because In Quadrant One
ld (ix+1),a ; Store Back Into Object's X Posistion
ret ; Return
Rotate2:
sub 21 ; Now In Quadrant Two; Have To Find Offest In Table
ld l,a ; Load Counter Into Register L
push hl
ld de,CosineTable ; In Quad Two, Simply Switch Sine And Cosine
call Multiply
ld a,(ix+4)
add a,l ; Add Y Value, Because In Quadrant Two
ld (ix+2),a
pop hl
ld de,SineTable ; In Quadrant Two, Simply Switch Sine And Cosine
call Multiply
ld a,(ix+3)
sub l ; Subtract X Value, Because In Quadrant Two
ld (ix+1),a
ret
Rotate3:
sub 42 ; Now In Quadrant Three; Have To Find Offest In Table
ld l,a ; Load Counter Into Register L
push hl
ld de,SineTable
call Multiply
ld a,(ix+4)
sub l ; Subtract Y Value, Because In Quadrant Three
ld (ix+2),a
pop hl
ld de,CosineTable
call Multiply
ld a,(ix+3)
sub l ; Subtract X Value, Because In Quadrant Three
ld (ix+1),a
ret
Multiply:
add hl,de
ld e,(hl)
ld h,(ix+5) ; Load Radius, So We Know What To Multiply By
ld d,0
ld l,d
ld b,8
MultiplyLoop:
add hl,hl ; Some Bit Shifting
jp nc,Divide
add hl,de
Divide:
djnz MultiplyLoop
xor a ; Shift HL Right 7 Bits, Thus Dividing By 128
add hl,hl
rla
ld l,h
ld h,a ; Register L Now Holds The Amount To Be Added Or Subtracted
ret
SineTable:
.db 11,16,28,37,46,53,61,69,76,84,90,96,102,107,111,116,119,122,124,126,126 ; Sine Table
CosineTable:
.db 126,126,124,122,119,116,111,107,102,96,90,84,76,69,61,53,46,37,28,16,11 ; Cosine Table
ObjectData:
.db 0,0,0,31,48,8,21 ; (Type),(Current Y = Doesn't Matter),(Current X = Doesn't Matter),(Center Y),(Center X),(Radius),(Counter Location)
; (Types: 1 = Clockwise, 0 = Counterclockwise)
; (Location Types: 21 = Right, 42 = Top, 63 = Left, 84 = Bottom)
Okay, now, this might be somewhat overwhelming. So let's go through this. In Rotate1, the object begins on the right end of the x-axis, because the counter location in ObjectData is set to 21. Then, as the first byte in ObjectData is 0, it will rotate counterclockwise, and increments the counter. Then, it checks to see how much it should move the object based on its origin and radius. For further reference, check the code. To do this, it shifts HL right by 7 bits, thus undoing our previous multiplication by 128, and then proceeds to add this to the origin values, storing the result in a different part of ObjectData, so that it is easier to draw this information later on.
Why did we change some values earlier? So that the motion would look smoother.
Hopefully this has been some help, I've also included an example program using this technique. If at first you do not understand, check out this program, and take a look at the commented lines. Good luck!
Example Program: Circle Demo
Any comments/suggestions?