VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > Python基础教程 >
  • Unity UI或3D场景实现跟随手机陀螺仪的晃动效果

需求

当游戏显示3d场景及其UI的时候。玩家左右晃动手机的时候,UI界面会随之左右偏移。上下晃动的时候,3D场景会随之上下偏移。手机停止晃动的时候,如若偏移的UI或场景,停顿一会后自动恢复到初始默认位置。

分析

首先本文功能应对的是横屏游戏(竖屏游戏的话也差不多一样,大家自己拓展下),假设当我们拿起手机玩游戏,手机会有四个部位,分别为左手拿的左手边和右手拿的右边,以及屏幕内容的上方和下方(下文中会用左手边,右手边,上方,下方来描述)。每个部位的倾斜都会造成UI或场景的偏移效果

我们可以先用一个枚举来定义这四个部位的倾斜情况

?
1
2
3
4
5
6
7
8
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方法),用来每帧检测当前的手机状态,并调用对应状态的注册事件。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
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用于挂载在需要根据手机状态偏移的组件上,用于设置偏移的参数,以及对应状态下计算偏移的量

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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);
 }
 }
}