Home > Net >  Quaternion loop not synced with time in Unity C#
Quaternion loop not synced with time in Unity C#

Time:07-14

I've been having trouble with Quaternion lerps. I'm simply looking to rotate my character 90 degrees based on their current rotation. The script below executes that almost perfectly, except for the fact that my character rotates a full 90 degrees long before rotationTime reaches the max value of 1. For some reason, the value of rotationTime is not properly synced with the progress of the lerp, and I can't seem to figure out why. What am I missing?

public class Movement : MonoBehaviour
{
    bool Rotating = false;
    Quaternion targetRotation;
    public float rotationTime = 0f;
    public float speed = 0.1F;
    private Rigidbody rb;

    private void Awake(){
        rb = GetComponent<Rigidbody>();
    }
    
    public void Turn(InputAction.CallbackContext context){ //executes when 'E' is pressed
        if (context.started && Rotating == false) {
            targetRotation = Quaternion.Euler(0,transform.eulerAngles.y   90f,0);
            rotationTime = 0;
            Rotating = true;
        }            
    }
    
    void Update() {
        if (Rotating == true) {
            rotationTime = rotationTime   Time.deltaTime * speed;
            transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation,rotationTime);
        };
        if (rotationTime > 1) {
            Rotating = false;        
        }
    }       
}

CodePudding user response:

I suspect that the main issue here is that you are using transform.rotation in your Quaternion.Lerp. You change it every frame, so every frame the start rotation will be closer to the target rotation. You should add some kind of _initRotation variable, and set it to transform.rotation in your Turn method. I mean something like this:

public void Turn(InputAction.CallbackContext context)
{ 
    if (context.started && Rotating == false) 
    {
        targetRotation = Quaternion.Euler(0,transform.eulerAngles.y   90f,0);
        _initRotation = transform.rotation;
        rotationTime = 0;
        Rotating = true;
    }
}

...

void Update()
{
...
    transform.rotation = Quaternion.Lerp(_initRotation, targetRotation,rotationTime);
...
}

Also, you have a logical issue with the lerp function. It does not affect the result in your particular case, but it can cause problems later.

You increment your rotation time by Time.deltaTime * speed every frame, it is not correct as it is not time passed from the start of the rotation.

According to the Quaternion.Lerp documentation, t value is always clamped to [0, 1]. So it is more convenient to use normalized time value instead of abstract speed value (right now it has no physical sense, it is just a multiplier).

It would be much clearer to use something like this:

void Update()
{
...
    rotationTime  = Time.deltaTime;
    transform.rotation = Quaternion.Lerp(transform.rotation,  targetRotation, rotationTime / fullRotationTime);
...
}

CodePudding user response:

Generally, I almost always work with carriers, and I recommend that you do the same.

public class Movement : MonoBehaviour
{
    bool Rotating = false;
    Vector3 targetRotation;
    public float rotationTime = 0f;
    public float speed = 0.1F;
    private Rigidbody rb;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
    }
    public void Turn(InputAction.CallbackContext context)
    { //executes when 'E' is pressed
        if (context.started && Rotating == false)
        {
            targetRotation = new Vector3(0, transform.eulerAngles.y   90f, 0);
            rotationTime = 0;
            Rotating = true;

        }

    }
    void Update()
    {
        if (Rotating == true)
        {
            rotationTime = rotationTime   Time.deltaTime * speed;
            transform.eulerAngles = Vector3.Lerp(transform.eulerAngles, targetRotation, rotationTime);
        };
        if (rotationTime > 1)
        {
            Rotating = false;

        }
    }
}

Also consider using coroutines for more readable code and to improve performance.

Good work!

CodePudding user response:

  • As it was mentioned already by this answer one of your main issues is that every time you use a new transform.rotation as interpolation start point

    => Your rotation starts fast and then gets slower and slower the closer you reach the target value.

There other issues here though:

  • You are using the transform.rotation.eulerAngles! From the API:

    When using the .eulerAngles property to set a rotation, it is important to understand that although you are providing X, Y, and Z rotation values to describe your rotation, those values are not stored in the rotation. Instead, the X, Y & Z values are converted to the Quaternion's internal format.

    When you read the .eulerAngles property, Unity converts the Quaternion's internal representation of the rotation to Euler angles. Because, there is more than one way to represent any given rotation using Euler angles, the values you read back out may be quite different from the values you assigned. This can cause confusion if you are trying to gradually increment the values to produce animation.

    To avoid these kinds of problems, the recommended way to work with rotations is to avoid relying on consistent results when reading .eulerAngles particularly when attempting to gradually increment a rotation to produce animation. For better ways to achieve this, see the Quaternion * operator.

  • And then in general, since I see there is a Rigibody involved you shouldn't set or read anything directly via the Transform component at all but rather only go through that rigidbody! Otherwise you might break/fight against the physics resulting in strange behavior and breaking collision detection.

In your case in my eyes it is way easier to control the entire thing in a Corouine which avoids the need for all the class fields and imho is way easier to understand, control and maintain:

public class Movement : MonoBehaviour
{
    public float speed = 0.1F;
    [SerializeField] private Rigidbody rb;

    private bool isRotating = false;
    
    private void Awake()
    {
        if(!rb) rb = GetComponent<Rigidbody>();
    }

    public void Turn(InputAction.CallbackContext context)
    {
        //executes when 'E' is pressed
        if (context.started && !isRotating)
        {
            StartCoroutine(RotateRoutine());
        }
    }

    private IEnumerator RotateRoutine()
    { 
        // little safety check first
        if(Mathf.Approximately(speed, 0f)
        {
            yield break;
        }

        // Just to be really sure you avoid any concurrent routines
        if (isRotating)
        {
            yield break;
        }
        
        // Lock so no other routine ca be started
        isRotating = true;

        // wait until we are in the next physics frame
        yield return new WaitForFixedUpdate();
        
        // store the initial rotation -> go throug  the rigibody not the transform
        var start = rb.rotation;
        var end = start * Quaternion.Euler(0,  90f, 0);

        var duration = 1 / speed;

        for (var rotationTime = 0f; rotationTime < duration; rotationTime  = Time.deltaTime)
        {
            // this would be a linear growing factor from 0 to 1
            var factor = rotationTime / duration;
            // optionally you could add ease in and out at the ends
            // basically you can add whatever curve function you like to grow from 0 to 1 within the given rotationTime 
            factor = Mathf.SmoothStep(0, 1, rotationTime);
             
            // interpolate from start to end while the factor grows from 0 to 1
            var rotation = Quaternion.Slerp(start,end, factor);

            // again for rigibdoy rather do this instead of going through transform
            rb.MoveRotation();

            // again wait for the next physics update
            yield return new WaitForFixedUpdate();
        }

        // Just to be sure to end on clean values
        rb.MoveRotation(end);

        // release the lock for the next routine to start
        isRotating = false;
    }
}
  • Related