Search code examples
unity-game-engineaccelerometerquaternionsgyroscopeeuler-angles

Calibrating a gyroscope for different "flat" positions, then extracting x and y tilt


I have a top down 2D ball game like the old Labyrinth ball game. My game is played on a mobile phone. The user controls it by tilting the phone. The idea is that when you set the screen at a slant, the ball realistically rolls to the "bottom" of the slant. For example, if the phone is sitting flat on a table, the ball shouldn't be going anywhere. But if I tilt the device up towards me, like a book, then the ball should fall down to the bottom of the screen. This is easy to do with the accelerometer, BUT...

My question is how to calibrate the gyroscope so the game can easily be played from any starting "flat" 3d rotation of the device. I'd like the user to be able to "calibrate" the game so that however they want to hold the device, they can control the ball properly. I'm thinking:

  1. If the phone is flat on the table, then tilting it will work "normally", as described in the first paragraph.

  2. If the phone is upside down, then it should be the opposite of #1. This is for people who want to are holding the phone over their head in bed, or something.

  3. If the phone is, say, held parallel to a wall (like a wall-mounted TV), then it should act as though the wall were a flat table, if that makes sense.

Currently, my code is:

public void OnHitCalibrate() {
    _offset = Input.gyro.attitude;
}

public void Update() {

    if(!_hasGyro) return;

    Quaternion reading = Input.gyro.attitude;
    reading *= Quaternion.Inverse(_offset); //subtract the offset
    _gyro.rotation = reading;
}

This gets the KIND of input I want, but I don't understand how the axes of the quaternion returned by Input.gyro.attitude help me find my 2d tilt values. For example, when the device is pointed north, and the attitude is the same as Quaternion.Identity, the values seem to change reasonably -- when I turn the attitude into a euler angle, changes in the x value correspond to my y tilt, and changes in the y value correspond to changes in my x tilt. But when I spin around, all that changes, and when I turn the phone more parallel to the wall, the numbers don't seem directly correlated to the axes in the euler angle, even when I use my OnHitCalibrate function and subtract the offset. Is there any way to interpret the attitude so I can always know which way the device is being turned?

My end goal is to distill the attitude into a Vector2 representing force -- where both x and y are between -1 and 1, where 1 represents maximum force up or too the right, -1 is maximum force down or to the left, and 0 means no change in force for the ball.


Solution

  • UPDATE: Solved it! I've attached the class that handles my gyroscope offsets. Hopefully it helps you! Note that it's coded for Unity C#, and has a few of my custom classes in it already.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public enum TiltModule { GYROSCOPE, ACCELEROMETER, KEYBOARD_ONLY, NONE }
    
    
    public class CalibratedGyroscope : SingletonKTBehavior<CalibratedGyroscope>
    {
        public bool _supported;
    
        private Quaternion _off;
        private Vector3 _offEuler;
        int _activeSemaphore = 0;
        private float _degreesForFullTilt = 10;
    
        public Vector2 _lastTilt;
    
        public void Init() {
            _off = Quaternion.identity;
            _supported = SystemInfo.supportsGyroscope;
        }
    
        public bool Activate(bool isActivated) {
            if(isActivated) _activeSemaphore++;
            else _activeSemaphore--;
    
            _activeSemaphore = Mathf.Max(_activeSemaphore, 0);
    
            if(_activeSemaphore > 0) {
                if(_supported) {
                    Input.gyro.enabled = true;
                } else {
                    return false; //everything not ok; you requested gyro but can't have it!
                }
            } else {
                if(_supported) {
                    Input.gyro.enabled = false;
                }
            }
            return true; //everything ok;
    
        }
    
        public void Deactivate() {
            _activeSemaphore = 0;
        }
    
        public void SetCurrentReadingAsFlat() {
            _off = Input.gyro.attitude;
            _offEuler = _off.eulerAngles;
        }
    
        public Vector3 GetReading() {
            if(_supported) {
                return (Quaternion.Inverse(_off) * Input.gyro.attitude).eulerAngles;
            } else {
                Debug.LogError("Tried to get gyroscope reading on a device which didn't have one.");
                return Vector3.zero;
            }
        }
    
        public Vector2 Get2DTilt() {
            Vector3 reading = GetReading();
            
            Vector2 tilt = new Vector2(
                -Mathf.DeltaAngle(reading.y, 0),
                Mathf.DeltaAngle(reading.x, 0)
            );
    
            //can't go over max
            tilt.x = Mathf.InverseLerp( -_degreesForFullTilt, _degreesForFullTilt, tilt.x) * 2 - 1;
            tilt.y = Mathf.InverseLerp( -_degreesForFullTilt, _degreesForFullTilt, tilt.y) * 2 - 1;
            
            //get phase
            tilt.x = Mathf.Clamp(tilt.x, -1, 1);
            tilt.y = Mathf.Clamp(tilt.y, -1, 1);
    
            _lastTilt = tilt;
    
            return tilt;
        }
    
        public string GetExplanation() {
            Vector3 reading = GetReading();
            
            string msg = "";
            
            msg += "OFF: " + _offEuler + "\n";
    
            Vector2 tilt = new Vector2(
                -Mathf.DeltaAngle(reading.y, 0),
                Mathf.DeltaAngle(reading.x, 0)
            );
    
            msg += "DELTA: " + tilt + "\n";
    
            //can't go over max
            tilt.x = Mathf.InverseLerp( -_degreesForFullTilt, _degreesForFullTilt, tilt.x) * 2 - 1;
            tilt.y = Mathf.InverseLerp( -_degreesForFullTilt, _degreesForFullTilt, tilt.y) * 2 - 1;
            
            msg += "LERPED: " + tilt + "\n";
    
            //get phase
            tilt.x = Mathf.Clamp(tilt.x, -1, 1);
            tilt.y = Mathf.Clamp(tilt.y, -1, 1);
    
            msg += "CLAMPED: " + tilt + "\n";
    
            return msg;
    
        }
    
        public void SetDegreesForFullTilt(float degrees) {
            _degreesForFullTilt = degrees;
        }
    
        
    }