Unity UI或3d場景(跟隨手機陀螺儀)的晃動效果

需求

當遊戲顯示3d場景及其UI的時候。玩家左右晃動手機的時候,UI界面會隨之左右偏移。上下晃動的時候,3D場景會隨之上下偏移。手機停止晃動的時候,如若偏移的UI或場景,停頓一會後自動恢復到初始默認位置。

分析

首先本文功能應對的是橫屏遊戲(豎屏遊戲的話也差不多一樣,大家自己拓展下),假設當我們拿起手機玩遊戲,手機會有四個部位,分別爲左手拿的左手邊和右手拿的右邊,以及屏幕內容的上方和下方(下文中會用左手邊,右手邊,上方,下方來描述)。每個部位的傾斜都會造成UI或場景的偏移效果

我們可以先用一個枚舉來定義這四個部位的傾斜情況

public enum EGyroType
{
    NoRotate,//不旋轉
    ToUp,//手機下方向上傾斜
    ToDown,//手機下方向下傾斜
    ToLeft,//左手邊向下傾斜
    ToRight,//右手邊向下傾斜
}

接着我們可以使用Unity的陀螺儀接口Input.gyro的一些屬性,來判斷當前手機的傾斜狀態,Gyroscope有如下屬性:

rotationRate 

Returns rotation rate as measured by the device's gyroscope.
返回設備陀螺儀測量的旋轉速率。
rotationRateUnbiased Returns unbiased rotation rate as measured by the device's gyroscope.
返回由設備陀螺儀測量的無偏旋轉速率。

gravity

Returns the gravity acceleration vector expressed in the device's reference frame.
返回在設備參考幀的重力加速度。
userAcceleration Returns the acceleration that the user is giving to the device.
返回用戶提供 給設備的加速度。
attitude Returns the attitude of the device. 返回設備的姿態。
enabled Sets or retrieves status of this gyroscope. 設置或檢索該陀螺儀的狀態。
updateInterval Sets or retrieves gyroscope interval in seconds.
設置或檢索該陀螺儀的間隔,以秒爲單位。

我用到enabled和gravity兩個屬性,enabled用於打開或者關閉陀螺儀功能,而gravity返回的是一個Vector3變量,具體情況對應的返回值,通過打印Log在android手機上顯示如下(橫屏遊戲,紀錄了某種情況下的某個不特定的角度的gravity值):

當手機橫着屏幕朝上水平放置在桌上的時候,返回值爲:(0.0, 0.0, -1.0)

上下傾斜:

當手機下方向上傾斜時,某個角度(轉角小於90度)的返回值爲:(0.0, 0.4, -0.9),角度再大的話屏幕的內容會翻轉過來。

當手機下方向下傾斜時,某個角度(轉角小於90度)的返回值爲:(0.0, -0.5, -0.9),轉角爲90度時:(0.0, -1.0, 0.0),轉角在90度到180度中時:(0.0, -0.8, 0.6),180度時即屏幕正朝下爲:(0.0, 0.0, 1.0),若角度再大一點爲:(0.0, 0.3, 0.9),直至屏幕內容翻轉過來。

我們可以發現

1.當 z < 0 , y > 0:當y的值變大則爲ToUp,變小則爲ToDown

2.當 z < 0 , y < 0:當y的值變大則爲ToUp,變小則爲ToDown

3.當 z > 0 , y < 0:當y的值變大則爲ToDown,變小則爲ToUp

4.當 z > 0 , y > 0:當y的值變大則爲ToDown,變小則爲ToUp

5.當 z < 0 變爲 z > 0,則爲ToDown,反之則爲ToUp

前四條總結下來就是,當 z < 0,y的值變大則爲ToUp,變小則爲ToDown。當 z > 0,y的值變大則爲ToDown,變小則爲ToUp

左右傾斜:

當手機左手邊向下傾斜時,某個角度(轉角小於90度)的返回值爲:(-0.2, 0.0, -1.0),轉角爲90度時:(-1.0, 0.0, 0.0),轉角在90度到180度中時:(-0.6, 0.0, 0.8)

當手機右手邊向下傾斜時,某個角度(轉角小於90度)的返回值爲:(0.6, 0.0, -0.8),轉角爲90度時:(1.0, 0.0, 0.0),轉角在90度到180度中時:(0.8, 0.0, 0.5)

可以總結出

1.當 z < 0 , x < 0:當x的值變小則爲ToLeft,變大則爲ToRight

2.當 z > 0 , x < 0:當x的值變大則爲ToLeft,變小則爲ToRight

3.當 z < 0 , x > 0:當x的值變大則爲ToRight,變小則爲ToLeft

4.當 z > 0 , x > 0:當x的值變小則爲ToRight,變大則爲ToLeft

即,當 z < 0,x的值變小則爲ToLeft,變大則爲ToRight。當 z > 0,x的值變大則爲ToLeft,變小則爲ToRight

5.當 z < 0 變爲 z > 0,若 x < 0 則爲ToLeft,否則則爲ToRight

6.當 z > 0 變爲 z < 0,若 x < 0 則爲ToRight,否則則爲ToLeft

然後我們可以根據這些性質推斷出手機的當前狀態,然後去執行我們想要執行的操作。

根據需求,無論是移動物體,還是轉動攝像機來達到偏移的效果,都會有一個最大偏移值,偏移速度,不轉動的時候等待的一個間隔時間,這幾個參數需要設置。

 

具體實現

首先我們寫一個腳本GyroManager,掛載在場景的一個GameObject上(也可以處理成爲單例,在別處調用裏面的Start,Update方法),用來每幀檢測當前的手機狀態,並調用對應狀態的註冊事件。

using System;
using UnityEngine;

public enum EGyroType
{
    NoRotate,//不旋轉
    ToUp,//手機下方向上傾斜
    ToDown,//手機下方向下傾斜
    ToLeft,//左手邊向下傾斜
    ToRight,//右手邊向下傾斜
}

public class GyroManager : MonoBehaviour
{
    Gyroscope mGyro;//陀螺儀
    Vector2 mCurrentLandscapeGyroValue, mCurrentPortraitGyroValue;//當前的水平垂直的gravity值
    Vector2 mLastLandscapeGyroValue, mLastPortraitGyroValue;//上一次的水平垂直的gravity值

    public EGyroType LandscapeEGyroType, PortraitEGyroType;//手機的水平垂直狀態
    float mPrecision = 0.015f;//精度,若前後兩次gravity值在精度內,則認爲當前沒有旋轉
    public int LandscapeGyroDifference, PortraitGyroDifference;//模擬的一個旋轉速度,gravity值差異越大,則該值越大

    bool mIsEnable;//是否開啓陀螺儀

    private void Start()
    {
        mGyro = Input.gyro;
        SetGyroEnable(true);
    }

    //每種狀態下需要執行的事件
    public Action LandscapeTransToDefault;
    public Action<int> LandscapeTransToAdd;
    public Action<int> LandscapeTransToReduce;

    public Action PortraitTransToDefault;
    public Action<int> PortraitTransToAdd;
    public Action<int> PortraitTransToReduce;

    public void ResetLandscape()
    {
        LandscapeEGyroType = EGyroType.NoRotate;
        SetLandScapeValue();
        mLastLandscapeGyroValue = mCurrentLandscapeGyroValue;
        LandscapeGyroDifference = 0;
    }

    public void ResetPortrait()
    {
        PortraitEGyroType = EGyroType.NoRotate;
        SetPortraitValue();
        mLastPortraitGyroValue = Vector2.zero;
        PortraitGyroDifference = 0;
    }

    void Update()
    {
        if (mIsEnable)
        {
            GetEGyroType();

            //根據解析出來的手機狀態,執行對應事件
            if (LandscapeEGyroType == EGyroType.ToLeft)
            {
                LandscapeTransToReduce?.Invoke(LandscapeGyroDifference);
            }
            else if (LandscapeEGyroType == EGyroType.ToRight)
            {
                LandscapeTransToAdd?.Invoke(LandscapeGyroDifference);
            }
            else
            {
                LandscapeTransToDefault?.Invoke();
            }

            if (PortraitEGyroType == EGyroType.ToDown)
            {
                PortraitTransToReduce?.Invoke(PortraitGyroDifference);
            }
            else if (PortraitEGyroType == EGyroType.ToUp)
            {
                PortraitTransToAdd?.Invoke(PortraitGyroDifference);
            }
            else
            {
                PortraitTransToDefault?.Invoke();
            }
        }
    }

    //開啓或關閉陀螺儀
    public void SetGyroEnable(bool isEnable)
    {
        if (mIsEnable != isEnable)
        {
            mIsEnable = isEnable;
            ResetLandscape();
            ResetPortrait();
            mGyro.enabled = isEnable;
        }
    }

    //解析當前手機狀態
    public void GetEGyroType()
    {
        SetLandScapeValue();
        //Landscape
        if (IsEquals(mCurrentLandscapeGyroValue.x, mLastLandscapeGyroValue.x, true))
        {
            LandscapeEGyroType = EGyroType.NoRotate;
            LandscapeGyroDifference = 0;
        }
        else
        {
            LandscapeGyroDifference = (int)(Mathf.Abs(mCurrentLandscapeGyroValue.x - mLastLandscapeGyroValue.x) * 60);

            if (mCurrentLandscapeGyroValue.y < 0 && mLastLandscapeGyroValue.y < 0)
            {
                //當 z < 0,x的值變小則爲ToLeft,變大則爲ToRight
                if (mCurrentLandscapeGyroValue.x < mLastLandscapeGyroValue.x)
                {
                    LandscapeEGyroType = EGyroType.ToLeft;
                }
                else
                {
                    LandscapeEGyroType = EGyroType.ToRight;
                }
            }
            else if (mCurrentLandscapeGyroValue.y > 0 && mLastLandscapeGyroValue.y > 0)
            {
                //當 z > 0,x的值變大則爲ToLeft,變小則爲ToRight
                if (mCurrentLandscapeGyroValue.x < mLastLandscapeGyroValue.x)
                {
                    LandscapeEGyroType = EGyroType.ToRight;
                }
                else
                {
                    LandscapeEGyroType = EGyroType.ToLeft;
                }
            }
            else
            {
                if (mCurrentLandscapeGyroValue.y < mLastLandscapeGyroValue.y)
                {
                    //當 z < 0 變爲 z > 0,若 x < 0 則爲ToLeft,否則則爲ToRight
                    if (mCurrentLandscapeGyroValue.x > 0)
                    {
                        LandscapeEGyroType = EGyroType.ToLeft;
                    }
                    else
                    {
                        LandscapeEGyroType = EGyroType.ToRight;
                    }
                }
                else
                {
                    //當 z > 0 變爲 z<0,若 x< 0 則爲ToRight,否則則爲ToLeft
                    if (mCurrentLandscapeGyroValue.x < 0)
                    {
                        LandscapeEGyroType = EGyroType.ToLeft;
                    }
                    else
                    {
                        LandscapeEGyroType = EGyroType.ToRight;
                    }
                }
            }
        }
        mLastLandscapeGyroValue = mCurrentLandscapeGyroValue;

        SetPortraitValue();
        //Portrait
        if (IsEquals(mCurrentPortraitGyroValue.x, mLastPortraitGyroValue.x, false))
        {
            PortraitEGyroType = EGyroType.NoRotate;
            PortraitGyroDifference = 0;
        }
        else
        {
            PortraitGyroDifference = (int)(Mathf.Abs(mCurrentPortraitGyroValue.x - mLastPortraitGyroValue.x) * 60);

            if (mCurrentPortraitGyroValue.y < 0 && mLastPortraitGyroValue.y < 0)
            {
                //當 z< 0,y的值變大則爲ToUp,變小則爲ToDown
                if (mCurrentPortraitGyroValue.x < mLastPortraitGyroValue.x)
                {
                    PortraitEGyroType = EGyroType.ToDown;
                }
                else
                {
                    PortraitEGyroType = EGyroType.ToUp;
                }
            }
            else if (mCurrentPortraitGyroValue.y > 0 && mLastPortraitGyroValue.y > 0)
            {
                //當 z > 0,y的值變大則爲ToDown,變小則爲ToUp
                if (mCurrentPortraitGyroValue.x < mLastPortraitGyroValue.x)
                {
                    PortraitEGyroType = EGyroType.ToUp;
                }
                else
                {
                    PortraitEGyroType = EGyroType.ToDown;
                }
            }
            else
            {
                //當 z<0 變爲 z > 0,則爲ToDown,反之則爲ToUp
                if (mCurrentPortraitGyroValue.y < mLastPortraitGyroValue.y)
                {
                    //>0 變 <0
                    PortraitEGyroType = EGyroType.ToUp;
                }
                else
                {
                    PortraitEGyroType = EGyroType.ToDown;
                }
            }
        }
        mLastPortraitGyroValue = mCurrentPortraitGyroValue;
    }

    //讀取gravity值
    public void SetLandScapeValue()
    {
        mCurrentLandscapeGyroValue.x = mGyro.gravity.x;
        mCurrentLandscapeGyroValue.y = mGyro.gravity.z;
    }

    public void SetPortraitValue()
    {
        mCurrentPortraitGyroValue.x = mGyro.gravity.y;
        mCurrentPortraitGyroValue.y = mGyro.gravity.z;
    }

    //前後兩次是否相等
    bool IsEquals(float a, float b, bool isLandscape)
    {
        if ((isLandscape && LandscapeEGyroType == EGyroType.NoRotate) || (!isLandscape && PortraitEGyroType == EGyroType.NoRotate))
        {
            if (Mathf.Abs(a - b) < 0.025f)
            {
                return true;
            }
        }
        if (Mathf.Abs(a - b) < mPrecision)
        {
            return true;
        }
        return false;
    }
}

接着我們寫個腳本GyroBase用於掛載在需要根據手機狀態偏移的組件上,用於設置偏移的參數,以及對應狀態下計算偏移的量

using System;
using UnityEngine;

public class GyroBase
{
    public float MaxValue;//最大偏移值
    public float DefaultValue;//初始位置
    float mCurrentValue;//當前偏移量

    public float Speed;//速度
    public float DuringTime;//等待間隔
    float mCurrentDuringTime;//當前時間間隔

    public Action<float> ValueChanged;//偏移事件

    public GyroManager mManager;

    float mBackSpeed;//回彈速度(一個減速過程)
    float BackSpeed
    {
        get
        {
            if (mBackSpeed > mMinSpeed)
            {
                mBackSpeed = Mathf.Max(mBackSpeed - Speed * mDeltaTime, mMinSpeed);
            }
            return mBackSpeed;
        }
    }

    float mMinSpeed;//最小速度
    float mDeltaTime;//Time.deltaTime

    bool mIsLandScape;//檢測手機水平轉動還是垂直轉動
    bool mIsResetBackProperty = false;

    //初始化賦值
    public void Init(float maxValue, float defaultValue, float speed, float duringTime, bool isLandscape, Action<float> action)
    {
        MaxValue = maxValue;
        DefaultValue = defaultValue;
        Speed = speed;
        DuringTime = duringTime;
        mMinSpeed = Speed * 0.2f;
        mCurrentValue = DefaultValue;
        mIsLandScape = isLandscape;

        if (mIsLandScape)
        {
            mManager.LandscapeTransToDefault += TransToDefault;
            mManager.LandscapeTransToAdd += TransToAdd;
            mManager.LandscapeTransToReduce += TransToReduce;
        }
        else
        {
            mManager.PortraitTransToDefault += TransToDefault;
            mManager.PortraitTransToAdd += TransToAdd;
            mManager.PortraitTransToReduce += TransToReduce;
        }

        ValueChanged = action;
    }

    //事件清除
    public void Clear()
    {
        if (mIsLandScape)
        {
            mManager.LandscapeTransToDefault -= TransToDefault;
            mManager.LandscapeTransToAdd -= TransToAdd;
            mManager.LandscapeTransToReduce -= TransToReduce;
        }
        else
        {
            mManager.PortraitTransToDefault -= TransToDefault;
            mManager.PortraitTransToAdd -= TransToAdd;
            mManager.PortraitTransToReduce -= TransToReduce;
        }
    }

    //重設回彈參數
    void ResetBackProperty()
    {
        if (!mIsResetBackProperty)
        {
            mIsResetBackProperty = true;
            mBackSpeed = Speed * 0.8f;
            mCurrentDuringTime = 0;
        }
    }

    //手機沒轉動的時候,超過間隔時間則減速回彈至默認位置
    void TransToDefault()
    {
        mIsResetBackProperty = false;
        mDeltaTime = Time.deltaTime;
        mCurrentDuringTime += mDeltaTime;
        if (mCurrentDuringTime > 1)
        {
            ValueToDefault();
            ValueChanged?.Invoke(mCurrentValue);
        }
    }

    //偏移增加
    void TransToAdd(int difference)
    {
        ResetBackProperty();
        ValueAddSpeed(difference);
        ValueChanged?.Invoke(mCurrentValue);
    }

    //偏移減小
    void TransToReduce(int difference)
    {
        ResetBackProperty();
        ValueReduceSpeed(difference);
        ValueChanged?.Invoke(mCurrentValue);
    }

    void ValueToDefault()
    {
        if (mCurrentValue > DefaultValue)
        {
            mCurrentValue = Mathf.Max(mCurrentValue - BackSpeed * mDeltaTime, DefaultValue);
        }
        else if (mCurrentValue < DefaultValue)
        {
            mCurrentValue = Mathf.Min(mCurrentValue + BackSpeed * mDeltaTime, DefaultValue);
        }
    }

    void ValueAddSpeed(int difference)
    {
        if (mCurrentValue < DefaultValue + MaxValue)
        {
            mCurrentValue = Mathf.Min(mCurrentValue + Speed * mDeltaTime * difference, DefaultValue + MaxValue);
        }
    }

    void ValueReduceSpeed(int difference)
    {
        if (mCurrentValue > DefaultValue - MaxValue)
        {
            mCurrentValue = Mathf.Max(mCurrentValue - Speed * mDeltaTime * difference, DefaultValue - MaxValue);
        }
    }
}

使用

例如,我們3D場景會隨手機的垂直轉動而上下偏移,我們可以通過旋轉攝像機的x軸來實現,我們只需寫個簡單的腳本掛載在攝像機上即可

public class CameraGyro : MonoBehaviour
{
    public GyroManager mManager;

    Transform mTransform;
    Vector3 mCameraAngle;

    GyroBase mGyroBase;

    void Start()
	{
        mTransform = transform;
        mCameraAngle = Vector3.zero;

        mGyroBase = new GyroBase();
        mGyroBase.mManager = mManager;
        mGyroBase.Init(5, 0, 5, 1, false, Change);
    }

    void Change(float value)
    {
        mCameraAngle.x = value;
        mTransform.localEulerAngles = mCameraAngle;
    }
}

因爲自己工程的UI場景並不是所有UI都會隨手機水平翻轉而轉動,所以就不能直接通過攝像頭來解決,而需要移動需要偏移的UI部分,所以我們可以寫個組件只掛載在需要偏移的UI部分上

public class UIGyro : MonoBehaviour
{
    public GyroManager mManager;

    void Start()
	{
        GyroBase mGyroBase = new GyroBase();
        mGyroBase.mManager = mManager;
        mGyroBase.Init(80, transform.localPosition.x, 80, 1, true, Change);
    }

    void Change(float value)
    {
        transform.localPosition = new Vector3(value, transform.localPosition.y);
    }
}

這樣就大致實現了需要的效果了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章