Unity UI或3D场景实现跟随手机陀螺仪的晃动效果
作者:王王王渣渣 发布时间:2021-12-09 01:09:32
需求
当游戏显示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, 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);
}
}
这样就大致实现了需要的效果了。
来源:https://blog.csdn.net/wangjiangrong/article/details/100895061


猜你喜欢
- 前言继承是面向对象语法的三大特征之一。继承可以降低代码编写的冗余度,提高编程的效率。通过继承,子类获得了父类的成员变量和方法。一个子类如何继
- MyBatis-Plus 使用简单,内置通用 Mapper、通用 Service,仅仅通过少量配置,即可实现单表大部分 CRUD 操作。下面
- @Resource和@Autowired是Spring Framework中两种常用的注入方式,它们的作用是在Spring容器中自动装配Be
- Spring Boot中可以在配置文件中直接进行数据库配置,spring.datasource.username= rootspring.d
- 当一个列表项目很多,并且每个项目可以进入到其它Activity或者Fragment时,保存之前列表的位置是一个比较不错的功能,今天研究了一下
- 我就废话不多说了,大家还是直接看代码吧~package cn.nxl2018;class Test{ //十进制常量赋值 &n
- mybatis update并非所有字段需要更新mybatis update需求:更新字段作为参数,未更新字段不传入解决办法<upda
- 一. MediaPlayer 状态机 介绍Android MediaPlayer 状态即图例 :1. Idle (闲置) 状态 和 End
- 前言在上一篇,我们谈到了jvm垃圾回收算法详细解析,并了解了JVM中针对堆区中不同的分代采用不同的垃圾回收算法在了解了垃圾回收算法之后,很多
- 在开源中国看到的操作ini文件的,写的还不看,留着以后用using System;using System.IO;using System.
- 本文是引用开源图表库框架 MPAndroidChart的LineChart地址:https://github.com/PhilJay/MPA
- 注意:页面分别拖拽一个FileUpload、Button1、Label1、GridView控件,并新建一个UploadedExcel文件夹D
- FastDFS 的作者余庆先生已经为我们开发好了 Java 对应的 SDK。这里需要解释一下:作者余庆并没有及时更新最新的 Java SDK
- java中Path是什么?在计算机上安装Java后,需要设置PATH环境变量以便从任何目录方便地运行可执行文件(javac.exe,java
- 1、CyclicBarrier:一个同步辅助类,用于协调多个子线程,让多个子线程在这个屏障前等待,直到所有子线程都到达了这个屏障时,再一起继
- 1、spring aop实现首先application-test.yml增加如下数据源的配置spring: datasource
- spring boot 请求后缀匹配spring boot 项目中添加这个类可以实现url不同后缀区分了public class UrlMa
- 01 高效设置我们先来讲讲有哪些设置调整之后 IDEA 会更好用。先说明一点:这里只是根据我个人喜好和习惯的建议,IDEA 的默认配置已经很
- 1. 要求获取指定文件夹下(包含子文件夹),所有指定后缀(如txt)的文件路径(即文件所在目录+文件名),返回一个字符串数组。2. 代码获取
- 概念介绍什么是死信死信可以理解成没有被正常消费的消息,在RabbitMQ中以下几种情况会被认定为死信:消费者使用basic.reject或b