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, 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); } }
这样就大致实现了需要的效果了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 关于C#中yield关键字的深入解析