1
\$\begingroup\$

I'm working on a 2D top-down game, and am working on the player controls.

I have the following code currently controlling a player sprite:

void Update () {
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");
        float headingAngle = Mathf.Atan2(moveHorizontal, moveVertical);
        Debug.Log("Heading angle: " + (headingAngle * Mathf.Rad2Deg).ToString() + " degrees.");
        Quaternion newHeading = Quaternion.Euler(0f, 0f, headingAngle * Mathf.Rad2Deg);     

        if (transform.rotation != newHeading && (moveHorizontal != 0f || moveVertical != 0f))
        {         
            Debug.Log("Turning. Rotation: " + transform.rotation + " | New Heading: " + newHeading);
            transform.rotation = Quaternion.Slerp(transform.rotation, newHeading, turnSpeed * Time.deltaTime);
        }
        else
        {
            Debug.Log("Heading in the right direction, moving...");
            transform.position += new Vector3(moveHorizontal * moveSpeed * Time.deltaTime, moveVertical * moveSpeed * Time.deltaTime, 0);                     
        }     
    }

This code sort of works, but occasionally the player will stop moving, even though it's rotated in the direction that the joystick is pointing. When that happens, the transform.rotation quaternion will be something like '(0, 0, -7, -7)', but the newHeading quaternion will be '(0, 0, 7, 7)', like they've flipped around. When that happens, the transform.rotation != newHeading in the if condition is always true, so the else condition is never reached.

If I keep moving the joystick to different locations, eventually the player will start moving again.

By the way, I have a sprite attached as a child to a game object, and the sprite is rotated 90 degrees in the z-axis, if that makes any difference.

Any help would be appreciated!

\$\endgroup\$
  • \$\begingroup\$ Atan2 takes arguments in the y,x order, so vertical first \$\endgroup\$ – Bálint Jun 23 '17 at 14:28
  • \$\begingroup\$ I've changed the order of the atan2 args to no effect. \$\endgroup\$ – Nikolas Adair Jun 23 '17 at 22:41
1
\$\begingroup\$

Remember, the basic == / != operators, unless explicitly overridden to check for something else, are looking for bitwise equality. They answer the question "are these two items literally the same, down to the very last decimal place?"

This is important to keep in mind, because very often we don't want bitwise equality. Instead, we want to ask "are these two items effectively the same for gameplay purposes?" The 20th binary digit after the decimal is rarely visible to players, so checking for a match bit-for-bit is usually much more exacting than we want, finding mismatches when as players and designers we expect to see a match.

This is why you'll often see advice to never compare two floating point numbers for exact equality - use range based comparisons (< <= >= >) or epsilons like Unity's Mathf.Approximately() method instead, to match floats that have a meaningful value rather than an exact bit pattern.

This gets compounded in the case of quaternions by the fact that quaternions form a double cover over the set of 3D rotations. That means any single rotation in 3D can be expressed equivalently as two different quaternions, q and -1*q, like your example of (0, 0, 0.707, 0.707) and (0, 0, -0.707, -0.707). You can verify that assigning your transform's rotation to either of these quaternions yields the same result, but we can see at a glance that comparing them for numeric equality won't find them to be the same, or even similar.

Instead of checking if two quaternions are bit-for-bit identical, what we really want to ask is "do these two rotations face the same way?" To do that, we can measure the similarity between the two quaternions using Quaternion.Angle() to determine the angle of rotation between them. If the angle is very very small, then you can be confident they're sufficiently equivalent.

(You can also use a dot product for this, to get the cosine of the angle instead, but using the Angle convenience methods gives us a nice, linear, easy-to-interpret result that's often worth the extra trig)


One last thing before I finish this answer: your rotation update contains some common errors.

transform.rotation = Quaternion.Slerp(transform.rotation, newHeading, turnSpeed * Time.deltaTime);

When you interpolate between your current value and a target using a small step, the result is not a linear approach at a consistent speed, but an exponential ease-out. (Fast at the start, and slowing down as your value gets closer and closer to its target, approaching it asymptotically)

  • This is fine and desirable at times, but because it's a non-linear rate of change, multiplying by deltaTime does not correctly adjust it for a variable framerate. You need to use an exponential weight, as described in this answer. You also probably don't need to use the more expensive Slerp, which ensures a more even rate of change. Since you're already using a non-linear easing, you probably wouldn't notice a change from switching to the simpler Lerp.

  • Alternatively, if you want a constant rate of change, use the RotateTowards() helper method - this handles the necessary math to rotate at a constant speed without overshooting.

\$\endgroup\$
  • \$\begingroup\$ This is extremely helpful! It really baffled me that the condition didn't appear to be functioning, so knowing how the Quaternions are best compared is invaluable. The information about the interpolation methods is apropos as well, because the exponential ease-out that you've described is the next issue I needed to tackle! Thanks a ton for the help! \$\endgroup\$ – Nikolas Adair Jun 24 '17 at 19:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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