Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question IEnumerator with transform.Rotate is slighty off

Discussion in 'Scripting' started by iocron, Jan 27, 2023.

  1. iocron

    iocron

    Joined:
    Nov 18, 2016
    Posts:
    4
    I am trying to rotate a object (with the help of a custom Easing Curve + IEnumerator), but the rotation is slightly off. A example of rotating 90deg results in a strange rotation of about ~100deg. I've spend two hours already, but I just can't figure out where the exact error is and how to solve it :/

    Code (CSharp):
    1. IEnumerator RotateCustom(GameObject target, float rotation=90f, float duration=1f){
    2.     float time = 0;
    3.  
    4.     while (time < duration)
    5.     {
    6.         float t = time / duration;
    7.         float progress = t * t * t * (t * (t * 6f - 15f) + 10f); // EaseSmoothStep
    8.  
    9.         target.transform.Rotate(new Vector3(0f, rotation / 360f * progress, 0f));
    10.         time += Time.deltaTime;
    11.    
    12.         yield return null;
    13.     }
    14. }
     
  2. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    Rotate works in local space by default. Perhaps that's throwing you off, if the object is parented to something else that's also been rotated?

    If you're using this on an object that has a parent, try moving the object to the scene root (so that it has no parent) and seeing if the behavior is different.
     
  3. TzuriTeshuba

    TzuriTeshuba

    Joined:
    Aug 6, 2019
    Posts:
    185
    I might be missing something, but i dont follow the math and why the sum of the applied rotations should equal the desired amount. The logic may be sound, but Im missing it.

    I run the following code in an online c# editor and the total rotation comes out to like 6.4 so im not sure how you got 100 degrees.
    Code (CSharp):
    1.     public static void Main()
    2.     {
    3.         float rotation=90f;
    4.         float duration=1f;
    5.         float dt = 0.02f;
    6.         float totalRot = 0f;
    7.         for(float time=0; time <= duration; time = time + dt){
    8.             float t = time / duration;
    9.             float progress = t * t * t * (t * (t * 6f - 15f) + 10f);
    10.             float frameRot = rotation / 360f * progress;
    11.             totalRot = totalRot + frameRot;
    12.         }
    13.      
    14.         Console.WriteLine(totalRot);
    15.  
    16.     }
    also, it doesnt seem like the amount you rotate in a frame is really dependent on Time.deltaTime, but rather just how much time has passed since starting the sequence. is your motion stuttery/not smooth?
     
    Last edited: Jan 27, 2023
  4. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,890
    You're doing
    transform.Rotate
    which is an additive operation, whilst you iterate from 0 to 1 on t. This makes no sense. You should be using something like
    transform.rotation = Quaternion.Slerp(startingRotation, endRotation, progress);
     
  5. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    I originally thought that was the case, actually -- I guess I should've just gone and checked, hah...

    But I would expect it to go *crazy* in that case -- repeatedly rotating by 5, then 10, then 15, then 20, then 25, [...], then 90 degrees would cause way more than 100 degrees of total rotation!
     
  6. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,890
    I suspect that is indeed the case but due to the cyclical nature of rotations they're just ending up with a net rotation of ~100 degrees at the end by chance.
     
  7. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,919
    Uhm, how do you get those numbers? Your rotation value is set to 90. You divide that by 360. So at max your rotation per frame is 0.25 in that case. Though since t is less than 1 you actually rotate by less each frame.

    The whole rotation is also framerate depended. Only linear progress can be "fixed" by deltaTime. So your "time" does increase properly. However your rotation actually follows a quadratic curve. You essentially integrate deltaTime which is more or less a constant which gives you a linear progress. Then you integrate that time which gives you a quadratic behaviour. Though, your smoothing function would throw it off even more :)

    Like @PraetorBlue said, the correct solution would be to use Quaternion.Slerp between the start and end rotation. Something like this:

    Code (CSharp):
    1. // untested
    2.     IEnumerator RotateCustom(GameObject target, float rotation=90f, float duration=1f)
    3.     {
    4.         var start = target.rotation;
    5.         var end = Quaternion.Euler(0f, rotation, 0f) * start;
    6.         float factor = 1f / duration;
    7.         for (float t = 0f; t < 1f; t += Time.deltaTime * factor)
    8.         {
    9.             float progress = t * t * t * (t * (t * 6f - 15f) + 10f); // Ease out
    10.             target.transform.rotation = Quaternion.Slerp(start, end, progress);
    11.             yield return null;
    12.         }
    13.         target.transform.rotation = end;
    14.     }
     
  8. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    Ah, I missed the division by 360f. That makes a lot more sense now.
     
  9. iocron

    iocron

    Joined:
    Nov 18, 2016
    Posts:
    4
    Thank you for all the answers, I've tried all suggested answers, but unfortunately this does not solve my issue, because I made the mistake of simplifying my original code too much in my first post.

    So I will try to explain the related problem. I want to rotate a object (a rectangle, e.g. a door) around a specific pivot point/position. But when I try to change target.transform.position accordingly to the target.transform.rotation animation, then it seems like the movement is off / not in sync with the rotation, even if I use Slerp for both rotation and position. I've also tried another version with RotateAround(), and it rotates correctly around a specific position, but then I have problems applying custom lerp easings. I've uploaded a gif with the animation (see attachment). The code looks like this:

    Code (CSharp):
    1.  
    2. IEnumerator LerpRotateDoor(GameObject target, /*Quaternion startRotation, Quaternion endRotation,*/ float duration=1f)
    3. {
    4.     // Test Values for a simple 90deg rotation around a specific pivot position
    5.     var startRotation = target.transform.rotation;
    6.     var endRotation = Quaternion.Euler(0f, -90f, 0f) * startRotation;
    7.     var startPosition = target.transform.position;
    8.     var endPosition = startPosition + new Vector3(target.transform.localScale.x / 2, 0, -(target.transform.localScale.x / 2));
    9.     float factor = 1f / duration;
    10.  
    11.     for (float t = 0f; t < 1f; t += Time.deltaTime * factor)
    12.     {
    13.         float progress = t * t * t * (t * (t * 6f - 15f) + 10f); // Ease out
    14.         target.transform.rotation = Quaternion.Slerp(startRotation, endRotation, progress);
    15.         target.transform.position = Vector3.Slerp(startPosition, endPosition, progress);
    16.         yield return null;
    17.     }
    18.  
    19.     target.transform.rotation = endRotation;
    20.     target.transform.position = endPosition;
    21. }
    22.  
     

    Attached Files:

    Last edited: Feb 17, 2023
  10. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,919
    No, it's completely in sync, however what you're doing makes things just much more complicated than it needs to be. The issue here is that you actually want to rotate around a point at the edge of your door where you define your "hinge". However instead you rotate the door around its center and manually shift the door around. This doesn't make much sense. Since you "move" / lerp the center linearly to the target position, the door would not describe an "arc".

    You can simplify your whole logic by setting up some additional gameobjects. That way you can "configure" the door in the editor and the code becomes much simpler. A rotating door only rotates on a single axis. So all you have to do is place your "axis" at the right position and simply rotate that axis. I would highly recommend using two empty gameobjects. One represent the fix part of the hinge, the other nested object would represent the moving / rotating part of the hinge. Below that rotating object you can add your actual door object as a child. You can offset the door mesh inside the hinge object so the position of the hinge is where it should be. To open / close the door you only need to rotate the inner hinge object around a single axis. No manual position change / manipulation required. You also don't need to use slerp as you can simply assign the localRotation and use
    Quaternion.Euler(0,angle,0)
    to specify the relative rotation within the outer "fixed hinge" parent object.

    Of course to move the door where it needs to be, you just move the outer hinge object where it should be. So the same script can be used for any kind of rotating door. Since you can re-orient the outer hinge any way you like, you can even change the rotation axis. You probably want to give the script an target angle how far the door should rotate, so it could be changed depending on the usage.
     
  11. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,919
    Here's an example script that provides 3 methods to open / close / toggle the door:

    Code (CSharp):
    1. using System.Collections;
    2. using UnityEngine;
    3.  
    4. public class RotatingDoor : MonoBehaviour
    5. {
    6.     public Transform child;
    7.     public float ClosedAngle = 0;
    8.     public float OpenAngle = 90;
    9.     public float duration = 1f;
    10.     private bool m_Moving = false;
    11.     private bool m_Open = false;
    12.     private float m_CurrentAngle = 0;
    13.  
    14.     void Awake()
    15.     {
    16.         SetAngle(ClosedAngle);
    17.     }
    18.     void SetAngle(float aAngle)
    19.     {
    20.         m_CurrentAngle = aAngle;
    21.         child.localEulerAngles = new Vector3(0, m_CurrentAngle, 0);
    22.     }
    23.     IEnumerator MoveDoor(bool aOpen)
    24.     {
    25.         if (m_Open == aOpen)
    26.             yield break;
    27.         m_Moving = true;
    28.         float startAngle = m_CurrentAngle;
    29.         float targetAngle = aOpen ? OpenAngle : ClosedAngle;
    30.         float factor = 1f / duration;
    31.  
    32.         for (float t = 0f; t < 1f; t += Time.deltaTime * factor)
    33.         {
    34.             float progress = t * t * t * (t * (t * 6f - 15f) + 10f);
    35.             SetAngle(Mathf.Lerp(startAngle, targetAngle, progress));
    36.             yield return null;
    37.         }
    38.         m_Open = aOpen;
    39.         SetAngle(targetAngle);
    40.         m_Moving = false;
    41.     }
    42.     public void OpenDoor()
    43.     {
    44.         if (!m_Moving && !m_Open)
    45.             StartCoroutine(MoveDoor(true));
    46.     }
    47.     public void OpenClose()
    48.     {
    49.         if (!m_Moving && m_Open)
    50.             StartCoroutine(MoveDoor(false));
    51.     }
    52.     public void ToggleDoor()
    53.     {
    54.         if (!m_Moving)
    55.             StartCoroutine(MoveDoor(!m_Open));
    56.     }
    57. }
    58.  
    Note that we can't really interrupt the animation. So we can only close the door once it's fully open and vice versa. If you want to be able to switch direction in between, it would be better to not use a coroutine at all. Instead something like this would make the most sense:

    Code (CSharp):
    1. using System.Collections;
    2. using UnityEngine;
    3.  
    4. public class RotatingDoor : MonoBehaviour
    5. {
    6.     public Transform child;
    7.     public float ClosedAngle = 0;
    8.     public float OpenedAngle = 90;
    9.     public float duration = 1f;
    10.     private float m_CurrentPos = 0;
    11.     private float m_Direction = -1;
    12.  
    13.     void Awake()
    14.     {
    15.         SetPos(0);
    16.     }
    17.     void SetPos(float aCurrentVal)
    18.     {
    19.         float t = m_CurrentPos = Mathf.Clamp01(aCurrentVal);
    20.         t = t * t * t * (t * (t * 6f - 15f) + 10f);
    21.         child.localEulerAngles = new Vector3(0, Mathf.Lerp(ClosedAngle, OpenedAngle, t), 0);
    22.     }
    23.     private void Update()
    24.     {
    25.         SetPos(m_CurrentPos + Time.deltaTime * m_Direction / duration);
    26.         if (m_CurrentPos == 1 || m_CurrentPos == 0)
    27.         {
    28.             enabled = false;
    29.         }
    30.     }
    31.     public void OpenDoor()
    32.     {
    33.         enabled = true;
    34.         m_Direction = 1;
    35.     }
    36.     public void OpenClose()
    37.     {
    38.         enabled = true;
    39.         m_Direction = -1;
    40.     }
    41.     public void ToggleDoor()
    42.     {
    43.         enabled = true;
    44.         m_Direction = -m_Direction;
    45.     }
    46. }
    Here we simply either increase or decrease the lerp variable accordingly to the given direction. The script will disable itself once it reaches one of the two ends. So the Update only runs while the door is rotating. We can simply change the direction at any time which would reverse the rotation from its current position.

    Just in case it's not clear, "child" would be the child object that should actually rotate. So you need to assign that in the inspector. That child would rotate inside the parent object around the parents up axis. You can now parent the actual door to that child object and position the door in such a way it rotates the way you want. The closed angle should usually be 0. That makes it easier to position the door. Since the angles are not constraint and we simply lerp between them, we could even model something like a valve where we could set a closed angle of 0 and an open angle of 1800 (5 full rotations). Set the duration to 3 seconds and you get a neat rotating animation. Such generic scripts can be used for all sorts of things.

    Though for more complex animations, it would be the best to actually create an animation. That's why Unity has an animation system :)