Rotating a gameobject question

I feel like this should be Unity 101, but I am completely stuck.

I have a gameobject that I want to rotate -90 degrees on the x axis if I click the left arrow, 90 degrees on the x axis if I click the right arrow, 90 degrees on the z axis if I click the up arrow and -90 degrees on the z axis if I click the down arrow. The object should take 1 second to do the rotation. To clarify a little further, from the perspective of a static camera, the object should appear to rotate in the same direction each time a given button is clicked.

When my object starts at an initial 0 rotation I can easily code this to work. However, once the object is at 90 degrees on the x axis, the object appears to rotate on the y axis instead of the z axis if I click up or down. I also notice this even in the unity editor if I manually slide the rotation values.

Any insight into how I can make the object always rotate in my desired location in world space regardless of the objects current rotation? I clearly don’t have a full grasp of Quaternions and rotations.

Here’s a quick script thanks to chatGPT that demonstrates what I’m talking about.

using System.Collections;
using UnityEngine;

public class CubeController : MonoBehaviour
{
    public float rotationTime = 1f;
    public KeyCode leftKey = KeyCode.LeftArrow;
    public KeyCode rightKey = KeyCode.RightArrow;
    public KeyCode upKey = KeyCode.UpArrow;
    public KeyCode downKey = KeyCode.DownArrow;

    private bool isRotating = false;

    void Update()
    {
        if (!isRotating && Input.GetKeyDown(leftKey))
        {
            StartCoroutine(RotateObjectLocalX(-90f));
        }
        else if (!isRotating && Input.GetKeyDown(rightKey))
        {
            StartCoroutine(RotateObjectLocalX(90f));
        }
        else if (!isRotating && Input.GetKeyDown(upKey))
        {
            StartCoroutine(RotateObjectLocalZ(90f));
        }
        else if (!isRotating && Input.GetKeyDown(downKey))
        {
            StartCoroutine(RotateObjectLocalZ(-90f));
        }
    }

    IEnumerator RotateObjectLocalX(float angle)
    {
        isRotating = true;

        Quaternion targetRotation = transform.localRotation * Quaternion.Euler(angle, 0, 0);
        Quaternion initialRotation = transform.localRotation;
        float elapsedTime = 0f;

        while (elapsedTime < rotationTime)
        {
            transform.localRotation = Quaternion.Slerp(initialRotation, targetRotation, elapsedTime / rotationTime);
            elapsedTime += Time.deltaTime;
            yield return null;
        }

        isRotating = false;
    }

    IEnumerator RotateObjectLocalZ(float angle)
    {
        isRotating = true;

        Quaternion targetRotation = transform.localRotation * Quaternion.Euler(0, 0, angle);
        Quaternion initialRotation = transform.localRotation;
        float elapsedTime = 0f;

        while (elapsedTime < rotationTime)
        {
            transform.localRotation = Quaternion.Slerp(initialRotation, targetRotation, elapsedTime / rotationTime);
            elapsedTime += Time.deltaTime;
            yield return null;
        }

        isRotating = false;
    }
}

Quaternion.RotateTowards should be enough

1 Like

Rotate your object on world axis, such as Vector.right or Vector3.up.

You should also generate the rotation around the axis with Quaternion.AngleAxis, rather than using euler angles. You also don’t need to be using co-routines here.

Here’s something I whipped up quickly to show how it can be simplified:

using UnityEngine;

public class CubeController : MonoBehaviour
{
   #region Inspector Fields

   [SerializeField]
   private float _rotationSpeed = 10f;

   [SerializeField]
   private KeyCode _upKeycode = KeyCode.W;

   [SerializeField]
   private KeyCode _downKeycode = KeyCode.S;

   [SerializeField]
   private KeyCode _leftKeycode = KeyCode.A;

   [SerializeField]
   private KeyCode _rightKeycode = KeyCode.D;

   #endregion

   #region Internal Members

   private Quaternion _targetRotation;

   #endregion

   #region Unity Callbacks   

   private void Awake()
   {
       _targetRotation = transform.rotation;
   }


   private void Update()
   {
       float vertInput = GetAxisInput(_downKeycode, _upKeycode);
       float hozInput = GetAxisInput(_rightKeycode, _leftKeycode);

       if (vertInput != 0f)
       {
           UpdateTargetRotation(90 * vertInput, Vector3.right);
       }

       if (hozInput != 0f)
       {
           UpdateTargetRotation(90 * hozInput, Vector3.up);
       }

       transform.rotation = Quaternion.RotateTowards(transform.rotation, _targetRotation, _rotationSpeed);
   }

   private float GetAxisInput(KeyCode inputNegative, KeyCode inputPositive)
   {
       if (Input.GetKeyDown(inputNegative))
       {
           return -1f;
       }
       else if (Input.GetKeyDown(inputPositive))
       {
           return 1f;
       }
       else
       {
           return 0f;
       }
   }

   private void UpdateTargetRotation(float angle, Vector3 axis)
   {
       Quaternion rotation = Quaternion.AngleAxis(angle, axis);
       _targetRotation = rotation * _targetRotation;
   }

   #endregion
}
1 Like

Thank you both!

The code provided does almost exactly what I wanted. I just needed to modify the Vector directions as they weren’t spinning the gameObject in the direction I wanted. But otherwise yes, this is perfect!

Spiney used a different method of rotation, which is fine. But just so you know, I think the problem with your original code was it used local space instead of global. In other words, if your object is pointed straight up then its personal y runs now along z, so y-rotations are now z-rotations (and z rotations are y-rotations since z was changed to point up). The Inspector also uses local. It goes in order YXZ, which means Y is always global, but X-rotations change based on Y and Z based on Y then X (which is actually what we want most of the time).

It turns out the order in * determines local vs. global. Your transform.localRotation * Quaternion.Euler(0, 0, angle) makes (0,0,angle) be local rotation, since it goes last. That’s just the rule. Flipping to put it first: Quaternion.Euler(0, 0, angle)*transform.localRotation makes it global. That seems bad, but for real every rotation can be used as local or global. That’s just a fact of rotations. Being able to choose using AB or BA is nice, once you get used to it.

If you look at Spiney’s code you’ll find the same flip to global (in UpdateRotation): _targetRotation = rotation * _targetRotation (“rotation” is the new rotation, which is before the *, not after).