一、Android平台上拖动不平滑的现象
团队使用的热更新框架还是比较老的ToLua,拖动地图的逻辑,实现的方案是,在地图上放置一个合适大小的Image组件作为Raycast Target的对象,统一接受和处理跟地图拖动相关的逻辑。
在C#层专门建立一个DragManager作为拖动事件管理器,其内部实现了IDragHandler,IPointerDownHandler,IPointerUpHandler,IScrollHandler等EventSystem事件接口。DragManager根据游戏交互的需求,实现了:单指拖动,双指拖动,短按,长按等接口。Lua层注册对应的函数,实现对应的响应时间事件。
单指拖动地图在Lua层的逻辑为,计算当前次的单指拖动事件在屏幕上的位置,以及上次事件在屏幕上的位置,计算屏幕上的拖动位移。由于我们游戏是2D游戏,使用的平视相机,因此可以简单的将屏幕上的位移通过相机参数简单映射为世界坐标系相机的移动距离,计算方式既可以使用相机的ScreenToWorldPoint接口,也可以通过orthographicSize和Screen.height的比例换算。
经过测试,这个方法在iOS平台没有问题,拖动起来非常的丝滑。但是在Android上,拖动起来,有抖动现象,显得游戏卡顿。
二、探究过程
这种卡顿,初步的设想有以下几种可能:
1、渲染层面的问题、掉帧等
由于我们游戏测试环境下一直有帧率显示,用高性能的安卓设备测,完全能跑满60fps,并没有掉帧现象,但是视觉效果上还是显得卡顿。此外,由于我们游戏会锁定帧率,并没有开启垂直同步VSync,经测试,也排除了这种原因。
2、事件接口的问题
除了IDragHandler以外,还尝试了自己在Update里使用Input.GetTouch的方式来替代OnDrag事件,但是经过测试,依然会卡顿。
3、生命周期函数选择问题
除了在Update里尝试GetTouch,还在FixedUpdate,LateUpdate里尝试了这样的方案,也依然是抖动的。
4、Lua的调用时机的问题
由于在Project Setting里的Script Execution Order中将LuaLooper的时机提前,有可能导致Lua的update和渲染不在同一帧。为此,选择了尝试移除Lua的控制逻辑,用最简单的C#脚本去实现。但是经过测试也依然存在抖动现象。
5、时间获取不对
由于计算用的是Time.deltaTime,有可能是这个时间和触摸事件发生的时间不一致,因此尝试在C#中,使用System.DateTime.Now去统计时长,并传递给Lua层使用。但是即便这么处理,也依然存在抖动的现象。
6、屏幕刷新率问题
由于测试设备有高刷,120帧,怀疑可能和刷新率有关。为此关闭手机的高刷测试,但是测试结果没有改善。
初步结论,最可能原因有
1、Unity3d游戏引擎给OnDrag接口或者GetTouch返回的位置值,在Android平台本身就是不及时的,不平滑的
2、获取事件时获取到position和事件发生的时机还是不对,但是由于Unity有自己的循环周期,无法准确获取事件触发真实时间。
二、插值函数
既然获取到数值不平滑,能够采取的一个常见策略就是进行插值。一般使用的差值方式有几种:
1、使用SmoothDamp函数
cs
var smoothPos = Vector3.SmoothDamp(cPos, targetPos, ref currentVelocity, smoothTime);
2、使用Lerp函数并用一个固定的参数,比如10*dt
cs
Vector3.Lerp(currentPos, targetPos, lerpFactor * Time.deltaTime)
3、使用Lerp函数并用幂指数函数等作为参数,比如
cs
1.0 - Mathf.Pow(System.Math.E, - powFactor * System.Math.E * dt)
三、选定方案
最终将调用时机(OnDrag,Update,FixedUpdate,LateUpdate)、插值方式(SmooothDamp,Lerp,LerpPow)以及具体的参数值(smoothTime,lerpFactor , powFactor ) 用不同按钮控制,实现实时调控参数,并打包到安卓测试机上测试。
实现效果最好的是LateUpdate + SmoothDamp + 合适的参数smoothTime 的方式效果最好,因此选择了在LateUpdate读取GetTouch,用SmoothDamp作为插值函数,并到合适的参数。
四、进一步探究
以上的方案其实在Android端已经可以取得相对较佳的表现了,唯一一个问题是,拖动的时候由于有插值延时的逻辑在,拖动地图并没有像iOS那么跟手。
如果希望进一步优化,可以尝试的方向有,在Android原生层使用onTouchEvent事件去获取准确的值,并返回到C#层去使用。但是需要处理和现有事件框架的结合,并做测试。
java
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
if(action == MotionEvent.ACTION_DOWN){
long eventTime = event.getEventTime();
float x = event.getX();
float y = event.getY();
Log.d(TAG, "onTouchEvent: ACTION_DOWN " + x + " " + y + " " + eventTime);
}
else if(action == MotionEvent.ACTION_MOVE){
long eventTime = event.getEventTime();
float x = event.getX();
float y = event.getY();
Log.d(TAG, "onTouchEvent: ACTION_MOVE " + x + " " + y + " " + eventTime);
}
else if(action == MotionEvent.ACTION_UP){
long eventTime = event.getEventTime();
float x = event.getX();
float y = event.getY();
Log.d(TAG, "onTouchEvent: ACTION_UP " + x + " " + y + " " + eventTime);
}
return super.onTouchEvent(event);
}
由于时间关系,原生层的优化并没有来得及进行完。后续有结果另行记录。