1. 程式人生 > 程式設計 >Unity UI或3D場景實現跟隨手機陀螺儀的晃動效果

Unity UI或3D場景實現跟隨手機陀螺儀的晃動效果

需求

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

分析

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

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

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

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

我用到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,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,-1.0),轉角為90度時:(-1.0,0.0),轉角在90度到180度中時:(-0.6,0.8)

當手機右手邊向下傾斜時,某個角度(轉角小於90度)的返回值為:(0.6,-0.8),轉角為90度時:(1.0,0.0),轉角在90度到180度中時:(0.8,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,//右手邊向下傾斜
}
 
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,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,true,Change);
 }
 
 void Change(float value)
 {
  transform.localPosition = new Vector3(value,transform.localPosition.y);
 }
}

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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。