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);
    }
}

这样就大致实现了需要的效果了。

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