0
\$\begingroup\$

So I'm trying to build a simple mechanic where the player see's 3 sides of a cube at a time: top, left and front(I called the front-side "Right" in my code, due to the orthographic view). The player can click on the front side or left side of the cube to rotate the cube so that the side that was pressed moves and becomes the top face. I want to smooth/animate the rotation.Cube Example

The red faces are the faces that the user can click on.

My Logic seems to work quite well when I click on the left face (Rotating along the x axis), but doesn't seem to work well for the front/right face(It usually works once and then does weird stuff on the next click, this is on the z-axis)

public class CubeRotator : MonoBehaviour
{
    private const float STOP_THRESHHOLD = 0.1f;
    private const float ROTATE_AMOUNT = 90f;

    [SerializeField, Header("Touch Quads Colliders")]
    private Collider touchQuadLeft;
    [SerializeField]
    private Collider touchQuadRight;

    [SerializeField, Space(10)]
    private float timeToRotate;

    private bool mustRotateLeft = false;
    private bool mustRotateRight = false;

    private float targetX = 0.0f;
    private float targetZ = 0.0f;

    private float xVelocity = 0.0F;
    private float zVelocity = 0.0F;

    int layerMask;

    private void Start()
    {
        layerMask = LayerMask.GetMask("Touch Quad");
    }

    void Update()
    {
        ListenForTouch();
        RotateCube();
    }

    private void ListenForTouch()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Vector2 touchPoint = Input.mousePosition;
            RaycastHit rayCastHit;
            Ray ray = Camera.main.ScreenPointToRay(touchPoint);

            if (Physics.Raycast(ray, out rayCastHit, 150f, layerMask))
            {
                if (rayCastHit.collider == touchQuadLeft)
                {
                    mustRotateLeft = true;
                    targetZ = targetZ - ROTATE_AMOUNT;
                    zVelocity = 0f;
                }

                else if (rayCastHit.collider == touchQuadRight)
                {
                    mustRotateRight = true;
                    targetX = targetX + 90f;
                    xVelocity = 0f;
                }
            }
        }
    }

    private void RotateCube()
    {
        float zAngle = transform.rotation.eulerAngles.z;
        float xAngle = transform.rotation.eulerAngles.x;

        if (mustRotateLeft)
        {
            zAngle = Mathf.SmoothDampAngle(zAngle, targetZ, ref zVelocity, timeToRotate);

            if (Mathf.Abs(zVelocity) <= STOP_THRESHHOLD)
            {
                mustRotateLeft = false;
            }
        }

        if (mustRotateRight)
        {
            xAngle = Mathf.SmoothDampAngle(xAngle, targetX, ref xVelocity, timeToRotate);

            if (Mathf.Abs(xVelocity) <= STOP_THRESHHOLD)
            {
                mustRotateRight = false;
            }
        }

        transform.rotation = Quaternion.Euler(xAngle, 0f, zAngle);
    }
}

Inspector Values:

Inspector Values

I've tried several things, the only problem I can think of is, gimbal lock. Thanks in advance.

\$\endgroup\$
4
  • \$\begingroup\$ I answered a similar question a little while ago. Your problem is in assuming that adding an increment to a Euler angle is the same thing as composing that extra rotation at the end. It's not - when you sum up angles like this the order of rotation gets lost and you only have the total to work with. With rotation, order matters. Instead you want to compose your new rotation with the current, and blend to that target - quaternions are good for this blending. \$\endgroup\$
    – DMGregory
    Commented Jun 10, 2017 at 15:28
  • \$\begingroup\$ I read your answer, the illustrations makes a lot of sense. So if I can't just add Euler Angles together, how would I apply rotations to my Quaternion? Would I have to use some matrix magic? \$\endgroup\$ Commented Jun 10, 2017 at 15:52
  • \$\begingroup\$ Similar to Fix 1 in that question, you'd store your current state as a quaternion instead of a collection of angles. Then you'd form the rotation change that you want to apply as another quaternion, using something like Quaternion.Euler or AngleAxis. Then update your target by applying this change to it: currentTarget = rotationChange * currentTarget; You can then use Quaternion.Lerp or RotateTowards to blend smoothly to this target rotation. \$\endgroup\$
    – DMGregory
    Commented Jun 10, 2017 at 16:04
  • \$\begingroup\$ Basically, quaternions (the things that actually can describe rotations in 3D space) aren't commutative, which is why order matters. Octernions (which describe rotations in 4D space) are noncommutative and nonassociative. \$\endgroup\$ Commented Jun 10, 2017 at 16:09

1 Answer 1

0
\$\begingroup\$

You can change your script with this code:

using UnityEngine;
using System.Collections;

public class CubeRotation : MonoBehaviour {

    private const float ROTATE_AMOUNT = 90f;

    [SerializeField, Header("Touch Quads Colliders")]
    private Collider touchQuadLeft;
    [SerializeField]
    private Collider touchQuadRight;

    [SerializeField, Space(10)]
    private float timeToRotate = 20;

    int layerMask;

    private void Start() {
        layerMask = LayerMask.GetMask("Touch Quad");
    }

    void Update() {
        ListenForTouch();
    }

    private void ListenForTouch() {
        if (Input.GetMouseButtonDown(0)) {
            Vector2 touchPoint = Input.mousePosition;
            RaycastHit rayCastHit;
            Ray ray = Camera.main.ScreenPointToRay(touchPoint);
            if (Physics.Raycast(ray, out rayCastHit, 150f, layerMask)) {
                if (rayCastHit.collider == touchQuadLeft) {
                    StartCoroutine(RotateCube(Vector3.forward));
                }
                else if (rayCastHit.collider == touchQuadRight) {
                    StartCoroutine(RotateCube(Vector3.right));
                }
            }
        }
    }

    IEnumerator RotateCube(Vector3 direction) {
        float step = ROTATE_AMOUNT / timeToRotate;
        for (float t = 0; t < ROTATE_AMOUNT; t += step) {
            transform.Rotate(direction, step, Space.World);
            yield return null;
        }
    }
}

This will always rotate the cube "upwards", and without bothering about Quaternions and such.

If you need further explanations about parts of the code, feel free to ask ofc.

\$\endgroup\$
5
  • \$\begingroup\$ Ok cool let me try that :) \$\endgroup\$ Commented Jun 10, 2017 at 15:28
  • \$\begingroup\$ So this didn't work, It actually became worse. The reason is that Mathf.SmoothDampAngle returns the final rotation after the transformation and not the amount that still needs to be rotated. So transform.Rotate is to rotate by an amount and transform.rotation = sets the rotation. \$\endgroup\$ Commented Jun 10, 2017 at 15:35
  • \$\begingroup\$ I didn't check all your code yet, I'll give it a look on Unity then in its entirety. \$\endgroup\$
    – Galandil
    Commented Jun 10, 2017 at 15:38
  • \$\begingroup\$ Sorry for the delay, I had to go away from the PC. You can check the edited answer. \$\endgroup\$
    – Galandil
    Commented Jun 10, 2017 at 18:40
  • \$\begingroup\$ Thank you very much :) this worked perfectly. I now understand what you were trying to tell me when you said I should use transform.Rotate and Space.World. I do appreciate it. If anyone reads this answer please also refer to the comments of the question, the answer explains why this happens \$\endgroup\$ Commented Jun 12, 2017 at 16:02

Not the answer you're looking for? Browse other questions tagged .