Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I'm new to XNA and this is my first actual project, so forgive my noobness.

I'm using

 jointAngle = System.Math.Atan2(RightStick.Y, RightStick.X);

in order to set an angle of a joint (farseer) so that the attached body points in the direction pushed with the right joystick. This all works great and i get values from negative pi to pi which correlate with the pushed direction. The problem arises when i go from 360 degrees to 361. Instead of the continued rotation beyond 360 degrees that one might expect, it jumps back to 0 degrees (or -3.14 radians), which causes a jerking motion as the joint suddenly shifts 359 degrees back instead of 1 forward.

Is there a simple way to account for this? I came up with a few solutions, but they're all pretty messy at best and completely unreliable at worst.

Please help!

share|improve this question
    
An instantaneous 361 degree turn is visually the same as a 1 degree turn. So I would find it reasonable for the function to wrap. How are you applying the rotations? Blending between them in some way? –  Byte56 Dec 31 '12 at 18:32
    
Having it be visually the same is the idea, that is not the reality though. The rotations are applied as a 'target angle' for the joint connecting the objects. As such, when it goes over 360 degrees, it snaps back 359 degrees to one instead of proceeding forward one. Because it is a joint that is calculated in realtime by the physics engine and not instant, there's a very noticeable snap back that occurs. –  Maxx Dec 31 '12 at 20:53
add comment

1 Answer

up vote 8 down vote accepted

atan2 is a mathematical function; it is stateless. There is nothing in it to “know” that you want an angle which is close to the angle from the previous frame, as opposed to an angle which is simply sufficient to identify the direction.

You must write your own logic to handle this case. There are several possible behaviors you could implement; here's the ones I've thought of. Note that I am not familiar with C#, XNA, or Farseer, so I have only pseudocode here, but all of these should work as long as you can retrieve the joint's current actual angle as well as the target angle.

  1. The joint should spin the smallest amount to match the direction of the joystick. This means that if the user spins the stick faster than the joint can keep up, it will start turning the other direction to make a shorter motion. This can be implemented as:

    let currentStickDirection = atan2(RightStick.Y, RightStick.X)
    let currentJointDirection = currentJointAngle mod 2π
    let difference = ((currentStickDirection - currentJointDirection + π) mod 2π) - π
    jointTargetAngle = currentJointAngle + difference
    

    The addition and subtraction of π (a half-turn) combined with modulus puts the difference-between-angles in the range −π…+π, which is the “smallest amount to match” property.

    Note that if you are implementing, for example, a turret, then this might be undesirable as it makes it harder to strafe a particular arc as the player needs to make sure not to lead too much.

  2. The joint should follow the motions of the stick. That is, if the user quickly makes multiple revolutions of the stick, the joint will follow and make multiple revolutions even if the user's movement is long over.

    To implement this, the logic is the same as above except that instead of using the joint's current actual angle, we use the joint's target angle (which can be simply a variable/field in your program, unrelated to the physics engine) as the state input.

    let currentStickDirection = atan2(RightStick.Y, RightStick.X)
    let currentJointDirection = jointTargetAngle mod 2π
    let difference = ((currentStickDirection - currentJointDirection + π) mod 2π) - π
    jointTargetAngle += difference
    
  3. Same as option 2, but the joint will never make an unnecessary full revolution. This differs from option 1 in that the joint will always turn in the same direction as the input does.

    To implement this, use the code from option 2, but after all of the above we also remove extra ±2πs from the target angle:

     let revolutions = (jointTargetAngle - currentJointAngle) / 2π
     if (revolutions >= 1)
         jointTargetAngle -= floor(revolutions) * 2π
     else if (revolutions <= -1)
         jointTargetAngle -= ceil(revolutions) * 2π
    

Note that if the stick is near its center, then small motions, possibly including noise/vibration rather than user motion, will cause large changes in the computed angle. If you haven't already, you will probably want to add a “dead zone” around the center which disables the angle computation. This can be a simple distance condition, like pow(RightStick.Y, 2) + pow(RightStick.X, 2) < pow(deadZoneRadius, 2).

Another interesting option would be to reduce the “motor power” of the joint (I don't know what exact options Farseer provides in this area) depending on the computed distance, so that small pushes on the stick can be used to slowly swing the joint (note that then the imprecision of direction matters less) and moving it to the limit does a fast swing. This would naturally reduce the effect of noise about the center, but unless you are using option 1 above (which does not depend on the previous target angle) and your joystick has perfect mechanical return-to-center, you will still want to have a deadzone.


Commentary: The two key principles here are:

  1. Work with differences between angles, not absolute angles.
  2. Make sure that (where appropriate for the application) you are treating any particular angle θ as equivalent to θ + 2π. This is the reason for all of the uses of modulo; the π offsets are used to control where we flip from positive to negative numbers, which affects the overall behavior of the algorithm, but not in a way which is dependent on the input range.
share|improve this answer
    
+1! A very thorough answer to the question and proposed solutions to the actual problem. –  chaosTechnician Dec 31 '12 at 22:23
    
@chaosTechnician And yet I managed to forget something. I've added a discussion of dead zones. –  Kevin Reid Dec 31 '12 at 23:08
    
This looks right on the money. Thank you so much for taking the time to answer my (probably poorly stated) question. –  Maxx Jan 1 '13 at 0:33
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.