0

I have a coroutine that is initiated when the user enters a trigger and then presses a specific key. The rotating as a whole works, but towards the end of the rotation it begins to rotate much, much slower. I have had this issue when using Vector3.Lerp but I fixed that by using speed * Time.deltaTime and another variable to make it smooth. I tried to use a step as per the Quaternion.RotateTowards unity docs but it would just not rotate at all.

void Update()
{
    if (Input.GetKey(KeyCode.Z))    
    {
        Debug.Log("Raycast.");
        if (Physics.Raycast(transform.position, Vector3.down, 1.5f))
        {
            StartCoroutine(Turn(cubeObject));
        }
    }
}

IEnumerator Turn(Transform cubeObject)
{
    Quaternion targetRotation = Quaternion.identity;
    do
    {
        Debug.Log("Begin rotating.");
        Vector3 targetDirection = cubeObject.position - transform.position;
        targetRotation = Quaternion.LookRotation(targetDirection);
        Quaternion nextRotation = Quaternion.RotateTowards(
                transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
        transform.localEulerAngles = new Vector3(0, nextRotation.eulerAngles.y, 0);
        yield return null;

    } while (Quaternion.Angle(transform.rotation, targetRotation) == 0f);     
}

And if I change the very last value on the last line (Quaternion.Angle) to anything greater than 0, the rotation never ends. I will be locked in rotation until I exit runtime. I'm only trying to rotate fully towards the target just once when the appropriate key is pressed.

derHugo
  • 83,094
  • 9
  • 75
  • 115
dangoqut132
  • 17
  • 10
  • 1
    Are you not starting a new coroutine every frame while z is pressed? But each trying to take where it is now to a new rotation? – BugFinder Jul 13 '23 at 12:48
  • If I take out the input entirely so that the trigger alone activates the coroutine, the coroutine will run perfectly (no slow down at all), however the debugging inside the IEnumerator will continuously run until I exit runtime. I am trying to make it rotate to the target rotation, and then stop completely (I can begin to move freely after). – dangoqut132 Jul 13 '23 at 12:56
  • @dangoqut132 you should still use `GetKeyDown` and check if a coroutine is already running .. you are currently spawning a lot of concurrent routines every frame so the longer you press the key the faster the rotation will happen – derHugo Jul 13 '23 at 13:01
  • In general instead of going through the euler angles I would rather make sure that the `targetDiretcion` only rotates aroun the `Y` axis (`targetDirection.y = 0;`) and then rather directly use the `transform.rotation = nextRotation;` ... you are also mixing world space direction and `rotation` with `localEulerAngles` which might create hickups – derHugo Jul 13 '23 at 13:03
  • slerp in general starts fast and ends slow the closer you are to the target – DevAm00 Jul 13 '23 at 13:06
  • @derHugo thanks for the reply. I had to change the original code to eulerAngles because as I was rotating inside the trigger, my character controller would flip and rotate on the x or z axis and then my character would be horizontal rather than vertical. Also GetKeyDown works fine, but it still has the slow down effect towards the end of the turn. I will try and use the above though. – dangoqut132 Jul 13 '23 at 13:07
  • I think in general it might result in this slow down effect if there is a delta in the Y axis ... because you never rotate towards this Y offset the angle never matches (in general you shouldn't use == for float at all) - the closet you get is a rotation that basically points below/above your actual target .. so this offset effect at the beginning doesn't really matter but has more and more impact the closer you come to the desired rotation ... I hope that is somehow understandable :D – derHugo Jul 13 '23 at 13:16
  • So since you actually do `RotateTowards` the **actual** target rotation offset on Y this rotation happens on all the Axis... you however only apply the rotation on Y and throw away the other components => the closer you get to the final rotation the Y is already "correct", now it tries to rotate on X/Z .. but you ignore those => barely a rotation happening in those frames – derHugo Jul 13 '23 at 13:18
  • @derHugo thanks for the extended replies. I understand what you mean in your last comment about it trying to rotate on X/Z after Y, but I'm still a bit stumped atm and even though I have implemented your first response by taking out EulerAngles, the original problem still persists. Even after rotation, the coroutine continues to run and I've tried using a bool to StopCoroutine when false but that doesn't work because I believe the bool is never turning false. – dangoqut132 Jul 13 '23 at 13:29

1 Answers1

1

As said there is a combination of a couple of issues here

  • First you potentially start a lot of concurrent ("simultaneously" happening) Coroutines! You should only start ONE and either make sure you don't start a second one or interrupt an already running one:

     private Coroutine currentRoutine;
    
     void Update()
     {
         // Use GetkeyDown to check of the first frame the key went down
         // rather than a continuous holding down of the button
         if (Input.GetKeyDown(KeyCode.Z))    
         {
             Debug.Log("Raycast.");
             if (Physics.Raycast(transform.position, Vector3.down, 1.5f))
             {
                 if (currentRoutine != null) StopCoroutine(currentRoutine);
                 currentRoutine = StartCoroutine(Turn(cubeObject));
             }
         }
     }
    
     IEnumerator Turn(Transform cubeObject)
     {
         ...
    
         currentRoutine = null;
     }
    
  • Then you are mixing the global world space direction targetDirection and also world space transform.rotation with a local space transform.localEulerAngles. You want to either do all calculation in local or global space - don't mix!

  • Then I think the slowing down can be explained by the fact that your targetDirection might have an offset in the Y axis.

    You do RotateTowards which rotates into the target rotation on all three axes - but then throw away X/Z and only apply the Y component.

    => The closer your rotation comes to the final targetRotation (potentially never fully reaching it btw) the more you see the effect since the Y component of the rotation is already almost "correct" so it now tries to rotate more and more on the X/Z components - the ones you ignore.

    In order t solve this (and in general) I would recommend to stick to Quaternion as much as possible and rather already fix the target direction

     targetDirction.y = 0;
     targetRotation = Quaternion.LookRotation(targetDirection);
     transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
    
     yield return null;
    
  • And finally never use == for comparing float! You could rather use Quaternion == which uses a reduced precision of 1e-5

     while(transform.rotation != targetRotation);
    
     // and then make sure to end up with clean values
     transform.rotation = targetRotation;
    

    or alternatively if you want to stick to the angle an be more precise use Mathf.Approximately

     while(!Mathf.Apprximately(Quaternion.Angle(transform.rotation, targetRotation), 0f));
    

    which basically equals doing

     while(Quaternion.Angle(transform.rotation, targetRotation) > Mathf.Epsilon);
    
BugFinder
  • 17,474
  • 4
  • 36
  • 51
derHugo
  • 83,094
  • 9
  • 75
  • 115
  • This actually worked perfectly I believe. I just had to add a bool for the TriggerEnter & Exit and that was it. I'm going to use all of this information in the future for more learning, but it's a little disappointing that I was not even close to the right path when trying to come up with my own solution. But thanks again, I really appreciate the time and effort you put in for multiple answers here. – dangoqut132 Jul 13 '23 at 13:38
  • Hey I know this is a late reply but I thought I already had it figured out. Is there a way to call another method once the coroutine is done? So for example once the coroutine has finished and I have rotated, a bool immediately becomes true, then inside that bool (which will be in the Update method), a new method is called etc. – dangoqut132 Jul 13 '23 at 15:43
  • 1
    @dangoqut132 of course .. simply put this after the `while` loop ... you can alo just do it directly from the routine instead of polling a bool flag in `Update` .. o you could also pass in a Callback `Action` to be invoked at the bottom of the routine – derHugo Jul 13 '23 at 15:46
  • Yeah sorry I figured it out immediately as I posted that comment. Thanks again – dangoqut132 Jul 13 '23 at 15:55
  • I didn't realise that there now is a massive issue because I did no testing after I adjusted the code, so as a band aid fix I've been trying to do something simple. Is there a way to "glue" my player to the cube the second I press the appropriate input key (the one above)? I thought the trigger was supposed to "glue" the player but doesn't seem to be the case – dangoqut132 Jul 13 '23 at 18:41
  • Assuming your player is a `Rigidbody` you could make the other object one as well an use [Joints](https://docs.unity3d.com/Manual/Joints.html) .. in your case for gluing the object probably a [`FixedJoint`](https://docs.unity3d.com/Manual/class-FixedJoint.html) or if you want a bit of elasticity a [`SpringJoint`](https://docs.unity3d.com/Manual/class-SpringJoint.html) -> create it (`AddComponent`) the moment you want to glue them together, remove it (`Destroy`) the moment you want to unglue them again – derHugo Jul 14 '23 at 06:33
  • Is there any way to do this with the in-built character controller? The closest I've gotten so far is to have a secondary controller, and then disable the "main movement" controller, and then enable the "ladder" controller. I've been looking into transform typeOf component – dangoqut132 Jul 14 '23 at 10:42