Search code examples
c#unity-game-enginerotationquaternionsgyroscope

Making a ball balancing game with the gyroscope of mobile devices


as the title already says, i'm currently working on a small ball balancing game using the gyroscope rotation to rotate a floating platform with a ball on it. The problem i always encounter is: i can't lock the y axis. I used euler angles and it worked perfectly, but then i have to deal with gimbal lock. Because i don't want to deal with this issue of euler, i tried using quaternions instead and it does work in some kind of way. I can set the rotation and can define a calibration offset for the gyroscope. If i wouldn't calculate this offset, the rotation would be absolute and not relative to the ingame world space. But i always have some sort of y rotation going on, even if i set it to 0 for the quaternion.

For example: i start the game, calibrate the gyro to my rotation but after that rotate around the y axis in real world space. The platform keeps facing the camera, but so doesn't the rotation axes.

The normal behaviour and how it always should be (no rotation on y axis in real world space): https://www.nani-games.net/share/umSc6XH1vE.gif

The actual behaviour and how it shouldn't be (rotation on y axis in real world space): https://www.nani-games.net/share/AVicmV4fWe.gif

A list of what i already tried:

  • Using euler angles
  • Using a combination of euler angles and quaternions in order to make up a new quaternion with desired parameters / rotations
  • Using Quaternion.AngleAxis in order to change the order of the euler axes
  • Checking the box "Freeze Y Rotation" of the platform's rigidbody under constraints (this doesn't work because i'm not applying force to rotate but rather set the rotation directly)

How my scene is build up: https://www.nani-games.net/share/Unity_fAQKW7cfPK.png

I have two scripts which are used to get the gyroscope rotation, make it useful in unity and use it to rotate other objects.

A static script called DeviceRotation. It activates the gyro and gets the rotation from it:

using UnityEngine;

static class DeviceRotation
{
    public static Quaternion offsetRotation;

    private static bool gyroInitialized = false;

    public static bool HasGyroscope
    {
        get
        {
            return SystemInfo.supportsGyroscope;
        }
    }

    public static Quaternion Get()
    {
        if (!gyroInitialized)
        {
            InitGyro();
        }

        return HasGyroscope
            ? ReadGyroscopeRotation()
            : Quaternion.identity;
    }

    private static void InitGyro()
    {
        if (HasGyroscope)
        {
            Input.gyro.enabled = true;
            Input.gyro.updateInterval = 0.0167f; // 60Hz
        }
        gyroInitialized = true;
    }

    private static Quaternion ReadGyroscopeRotation()
    {
        return new Quaternion(0.5f, 0.5f, -0.5f, 0.5f) * Input.gyro.attitude * new Quaternion(0, 0, 1, 0);
    }

    public static void calibrateCoords()
    {
        Quaternion deviceRotation = Get();
        offsetRotation = deviceRotation;
    }
}

The second script which is appended to the platform object. It gets a calibration quaternion, calculates a new quaternion out of the calibration quaternion and a constantly updating quaternion (both taken from the gyroscopes rotation) and then sets the platforms transform to this new rotation:

using UnityEngine;
using UnityEngine.UI;

public class Gyroscope : MonoBehaviour
{
    public Button resetSphere;
    public Button calibrate;
    public GameObject sphere;

    public Text platformCoords;
    public Text gyroOffsetCoords;

    void Start()
    {
        DeviceRotation.calibrateCoords();
        calibrate.onClick.AddListener(DeviceRotation.calibrateCoords);

        setGyroRotation();
        resetSphere.onClick.AddListener(ResetSphere);
    }

    void Update()
    {
        setGyroRotation();

        Vector3 offsetRotationEuler = DeviceRotation.offsetRotation.eulerAngles;
        gyroOffsetCoords.text = "GyroOffset:\n" +
                                "X: " + offsetRotationEuler.x + "\n" +
                                "Y: " + offsetRotationEuler.y + "\n" +
                                "Z: " + offsetRotationEuler.z;
    }

    private void ResetSphere()
    {
        GameObject temp_sphere = Instantiate(sphere);
        temp_sphere.transform.position = new Vector3(0, 3, 0);
    }

    private void setGyroRotation()
    {
        Quaternion deviceRotation = DeviceRotation.Get();
        Quaternion newRotation = Quaternion.Inverse(DeviceRotation.offsetRotation) * deviceRotation;
        transform.localRotation = newRotation;

        Vector3 platformAngles = transform.localRotation.eulerAngles;
        platformCoords.text = "Platform:" + "\n" +
                              "X: " + platformAngles.x + "\n" +
                              "Y: " + platformAngles.y + "\n" +
                              "Z: " + platformAngles.z;
    }
}

So, to conclude everything: my problem is that i cannot lock one axis (the y axis) of a quaternion. I also tried a combination of using euler to make the desired rotation i want and then converting it to a quaternion (that's why those two functions QuaternionToEuler and EulerToQuaternion exist in the script OffsetGyro), but unfortunately this caused gimbal lock (what a surprise i guess).

I really need help at this quaternion problem, i'm stuck with it since several days.


Solution

  • So, i played with everything for a bit and tried to randomly create a possible solution. This is my current function setGyroRotation:

    private void setGyroRotation()
    {
        Quaternion deviceRotation = DeviceRotation.Get();
        Quaternion newRotation = Quaternion.Inverse(DeviceRotation.offsetRotation) * deviceRotation; // Calculating "initial" rotation
        axisObject.transform.rotation = newRotation; // Setting the axis' rotation to our "initial" rotation
        Vector3 unfixedUp = newRotation * Vector3.up;
    
        float angle = Vector3.Angle(Vector3.up, unfixedUp);
        Vector3 axis = Vector3.Cross(Vector3.up, unfixedUp);
    
        if (axis == Vector3.zero)
        {
            transform.rotation = Quaternion.AngleAxis(unfixedUp.y >= 0f ? 0f : 180f, Vector3.right);
        }
        else
        {
            transform.rotation = Quaternion.AngleAxis(angle, axis);
        }
    
        Vector3 platformAngles = transform.localRotation.eulerAngles;
        platformCoords.text = "Platform:" + "\n" +
                                "X: " + platformAngles.x + "\n" +
                                "Y: " + platformAngles.y + "\n" +
                                "Z: " + platformAngles.z;
    }
    

    It almost does work as expected: no gimbal lock (atleast not one which occurs easily. Only if you rotate your phone about 180 degrees on x or z) and y axis is freezed, but the initial / calibration rotation axes still are a big problem. Here's a gif which hopefully displays the problem a bit better than the other ones: https://www.nani-games.net/share/93eHK0Fz1t.gif

    The axis above the platform displays the rotation of the gyro with an offset applied.