Rotation reversing for a single frame using Quaternion.LookRotation

I’m using the following code for my player controller:

            public class PlayerController : MonoBehaviour
            {
             //Components
             [SerializeField] Camera gameCamera;
             [SerializeField] Rigidbody playerbody;
        
             //Physics Constants
             private float acc = 0.0703125f;
             private float frc = 0.0703125f;
             private float top = 9f;
        
        
              //Inputs
             public Vector3 moveInput;
             public Vector3 localizedInput;
            
              //Coroutines
             Coroutine turnCoRt;
        
        
             //On Move is called when activity from the Move InputAction is detected.
             public void OnMove(InputValue value)
             {
                 Vector2 rawInput = value.Get<Vector2>();
                 moveInput = new Vector3(rawInput.x, 0, rawInput.y);
        
                 if (moveInput != Vector3.zero)
                 {
                     //Get direction of input relative to the camera direction.
                     localizedInput = GetLocalizedInput();
                     if (turnCoRt != null) StopCoroutine(turnCoRt);
                     turnCoRt = StartCoroutine(Turn());
                 }
                 else
                 {
                     //Get direction of input relative to the camera direction.
                     localizedInput = Vector3.zero;
                     if (turnCoRt != null) StopCoroutine(turnCoRt);
                 }
             }
        
        
             //Update is called once per frame.
             private void Update()
             {
                 localizedInput = GetLocalizedInput();
             }
        
             // FixedUpdate is called once per fixed timestep frame.
             void FixedUpdate()
             {
                 if (moveInput != Vector3.zero)
                 {
                    Accelerate(acc);
                 }
                 else
                 {
                    Decelerate(frc);
                 }
              }
        
         void Accelerate(float acc)
             {
                 if (playerbody.velocity.magnitude < top)
                 {
                     if (playerbody.velocity.magnitude + acc < top)
                     {
                         playerbody.AddForce(acc * localizedInput, ForceMode.VelocityChange);
                     }
                     else
                     {
                         playerbody.AddForce((top - playerbody.velocity.magnitude) * localizedInput, ForceMode.VelocityChange);
                     }
                 }
             }
        
             void Decelerate(float dec)
             {
                 if (playerbody.velocity.magnitude > 0)
                 {
                     if (dec < playerbody.velocity.magnitude)
                     {
                         playerbody.AddForce(dec * -playerbody.velocity.normalized, ForceMode.VelocityChange);
                     }
                     else
                     {
                         playerbody.velocity = Vector3.zero;
                     }
                 }
             }
        
             Vector3 GetLocalizedInput ()
             {
                 Vector3 camVector = Vector3.ProjectOnPlane(gameCamera.transform.forward, transform.up).normalized;
                
                Quaternion camRotation = Quaternion.FromToRotation(Vector3.forward, camVector);
               
                Vector3 result = camRotation * moveInput;
               
                return result;
             }
        
             IEnumerator Turn ()
             {
                 for (float r = 0; r < 1; r += Time.deltaTime)
                 {
                     if (r > 1) r = 1;
        
                     transform.rotation = Quaternion.Slerp(Quaternion.LookRotation (transform.forward, transform.up), Quaternion.LookRotation (localizedInput, transform.up), r);

                     playerbody.velocity = transform.forward * playerbody.velocity.magnitude;
                     
                     yield return null;
                 }
        
                 for (; ; )
                 {
                     transform.rotation = Quaternion.LookRotation(localizedInput, transform.up);

                     playerbody.velocity = transform.forward * playerbody.velocity.magnitude;

                     yield return null;
                 }
             }
         }

This gives the desired effect most of the time but occasionally, when holding left or right to turn, the character will be rotated 180 degrees around the Y axis for a single frame before returning to the intended rotation on the next. Can anybody figure out what’s causing it?

Not completely sure what’s causing the issue, but there’s something really strange about the way you’re using coroutines here and it might be related.

The way your logic works around line 31 and 38, your coroutine is always going to be stopped after exactly one frame. That means your coroutine will only ever run up until the first yield statement. In fact I’m confused as to how you’re getting any rotation on your character at all given that this means you’ll be calling Slerp only with a third parameter of 0 every time… Am I missing something there?

Also the if statement on line 107 can never be true because the for loop conditional precludes it.

I think I figured it out. The Quaternion.FromToRotation(Vector3.forward, camVector) on line 96 seems to have been causing problems when camVector happened to align with Vector3.forward. Replacing it with Quaternion.LookRotation(camVector, transform.up) seems to have resolved the issue.

@PraetorBlue On the subject of the coroutine: I’m using the New Input System so OnMove() is only called when the status of “Move” (WASD or Left Analog Stick) changes. The way I have it set up essentially restarts the coroutine whenever you input a new direction so you don’t end up with multiple instances fighting each other.

As for line 107, my understanding is that each iteration of the for loop checks if r < 1 and, if it is, adds Time.deltaTime. Since it’s possible that Time.deltaTime is greater than 1 - r it is therefore possible that the final value of r could exceed 1, in which case the if statement will set the value of r to 1. If I’m misunderstanding though please let me know.

1 Like

For loops work like this:

  • Check the condition (break if false)
  • Run the body
  • Increment
  • Repeat.

Condition checking happens first. Increment happens last. So it’s not possible for r to be > 1 inside the loop.

1 Like

When you’re Slerping or Lerping, it’s often critical to reach exactly 1.0, and as Praetor points out above, a for() loop isn’t suited to this.

Here is how I do almost ALL my slerps and lerps:

See how the fraction will always reach (at least) 1.0 before the loop exits? This lets you be confident of the final state.