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

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! Smile

Example Program: Circle Demo

Any comments/suggestions?
Nice tutorial Smile I wonder how fast it is compared the "naive" implementation, the one using a 256-bytes LUT and having the same code for each angle-offset.
matrefeytontias wrote:
Nice tutorial Smile I wonder how fast it is compared the "naive" implementation, the one using a 256-bytes LUT and having the same code for each angle-offset.


Could be quite faster, as this method can also support as many parts of the circle as you would like, and completely avoids using any fixed or floating point math. Also, it is quite simple to change the radius, direction, location and other things, even while it is rotating. This is basically what was used in this program: The World's Hardest Game
Btw, you don't need an "or a" after "dec a". "dec a" affects the z flag. You can also use "sub" instead of "cp" to make the checks, then you can "inc a" instead "sub XX". And going on that, i'm pretty sure using a push/pop would actually be faster (and smaller). You could also use two "ex af,af'"s which would definitely be faster than reloading the value from ix and subtracting an immediate value. I also think it'd be slightly faster if you used jr instead of jp in NoResetCounter, since jr is faster when the condition is false. For the first quadrant, it'll be two clocks slower, for the rest it'll be faster. And smaller.

I appreciate you writing this up, it's been years since i've touched any trig. I've been a fan of using small path tables which i believe is faster but definitely not as flexible or accurate as this. So thanks Smile
Can't believe I hadn't noticed this until now - nice work Mateo! This is a great tutorial on circle work in ASM Smile
chickendude wrote:
Btw, you don't need an "or a" after "dec a". "dec a" affects the z flag. You can also use "sub" instead of "cp" to make the checks, then you can "inc a" instead "sub XX". And going on that, i'm pretty sure using a push/pop would actually be faster (and smaller). You could also use two "ex af,af'"s which would definitely be faster than reloading the value from ix and subtracting an immediate value. I also think it'd be slightly faster if you used jr instead of jp in NoResetCounter, since jr is faster when the condition is false. For the first quadrant, it'll be two clocks slower, for the rest it'll be faster. And smaller.

I appreciate you writing this up, it's been years since i've touched any trig. I've been a fan of using small path tables which i believe is faster but definitely not as flexible or accurate as this. So thanks Smile


Thanks for the tips! I'll add them in there! Smile
As I was thinking about what Chickendude said in my question thread, I looked to see if I could find anything helpful here.
And wow, this is awesome Very Happy
add wrote:
As I was thinking about what Chickendude said in my question thread, I looked to see if I could find anything helpful here.
And wow, this is awesome Very Happy

Cool! Smile Any ideas for use of this? You can make spirals, loops, zigzags, and all sorts of interesting things. Even rectangular motion, if you really wanted too. This could make circular games a little easier to program! Wink

EDIT: As a matter of fact, I have learned some more, so I am going to edit this a little and make it more optimized and faster. Smile

Basically it just makes it so only one LUT is required. +10 brownie points if you can optimize further! Smile


Code:
MoveObjectClockwise:
   ld   a,(ix+6)         ; Load Counter Into Register A
   dec   a            ; Once It Gets To 0, Reset To 84
   jp   nz,NoResetCounter
   ld   a,84            ; 84 = 21 Parts Per Quadrant*4
   jp   NoResetCounter

MoveObjectCounterClockwise:
   ld   a,(ix+6)            ; Load Counter Into Register A
   inc   a
   cp   85            ; Once It Passes 84
   jp   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   43
   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   
   ex   af,af'
   call   MultiplyCosine
   ld   a,(ix+4)
   sub   l            
   ld   (ix+2),a
   ex   af,af'
   ld   e,a            ; Load Counter Into Register L   
   call   MultiplySine
   ld   a,(ix+3)
   add   a,l
   ld   (ix+1),a
   ret

Rotate1:
   ld   e,a            ; Load Counter Into Register E
   ex   af,af'
   call   MultiplySine         ; 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
   ex   af,af'            ; Load Counter into A
   ld   l,a            ; Load Counter Into Register L
   call   MultiplyCosine         ; 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            
   ex   af,af'
   call   MultiplyCosine
   ld   a,(ix+4)
   add   a,l            ; Add Y Value, Because In Quadrant Two
   ld   (ix+2),a
   ex   af,af'            ; Now In Quadrant Two; Have To Find Offest In Table
   ld   e,a            ; Load Counter Into Register E
   call   MultiplySine
   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   e,a            ; Load Counter Into Register E
   ex   af,af'
   call   MultiplySine
   ld   a,(ix+4)
   sub   l            ; Subtract Y Value, Because In Quadrant Three         
   ld   (ix+2),a
   ex   af,af'            ; Now In Quadrant Three; Have To Find Offest In Table
   ld   l,a            ; Load Counter Into Register L   
   call   MultiplyCosine
   ld   a,(ix+3)
   sub   l            ; Subtract X Value, Because In Quadrant Three
   ld   (ix+1),a
   ret

MultiplySine:
   ld   hl,AngleTable+21
   xor   a
   ld   d,a
   sbc   hl,de
   jr   Backwards
MultiplyCosine:
   ld   de,AngleTable
   add   hl,de
Backwards:
   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

AngleTable:
 .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
  
Register to Join the Conversation
Have your own thoughts to add to this or any other topic? Want to ask a question, offer a suggestion, share your own programs and projects, upload a file to the file archives, get help with calculator and computer programming, or simply chat with like-minded coders and tech and calculator enthusiasts via the site-wide AJAX SAX widget? Registration for a free Cemetech account only takes a minute.

» Go to Registration page
Page 1 of 1
» All times are UTC - 5 Hours
 
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum

 

Advertisement