手游摇杆设计:基于Flashui与Detour

藏书馆

欢迎来到网易游戏学院藏书馆

手游摇杆设计:基于Flashui与Detour

作者:庄钟杰(程序)

0. 摘要

  本文总结了我们游戏虚拟摇杆实现过程中遇到的问题, 着重介绍flashui多点触摸与detour寻路算法, 包括:

  如何在flashui上设计一套稳定的多触点摇杆按钮响应系统, 在手残党疯狂高速点击之下仍能正常工作?

  如何用detour模块, 设计适合摇杆方向控制的寻路方案?

1. 虚拟摇杆需求与术语

  我们要实现的摇杆系统如Fig.1所示:

  左侧为方向控制器joystick.

  右侧为按钮面板joypanel, 里面包含了若干功能按钮joybutton. 功能按钮的形状有圆形和1/4环形.

Fig.1 摇杆术语

2. 多点触摸方案

  手游摇杆系统首要解决的是多点触摸问题, 摆在我们面前的有两套方案:

  引擎内置Python层. 通过注册game.on_touches_began, game.on_touches_moved, game.on_touches_ended回调函数, 处理屏幕上出现的触摸事件. (具体用法请参考Reference3)

  Flashui内置AS层. 使用addEventListerner为元件增加TouchEvent处理函数, 可侦听该元件上的TOUCH_BEGIN, 
TOUCH_MOVE, TOUCH_END事件. (具体用法请参考Reference2)

  很多项目组都采用了第1种方案, 然而我们是二次开发, 为了与既有代码兼容, 我们最终选择了第2种方案, 在flashui层检测多触点事件.

3. 方向控制Joystick实现与性能优化

3.1 Joytick实现

  先讨论在Flashui层如何实现joystick的触摸检测, 代码如下. m_joystick是Fig.1左侧joystick元件, m_touch_id用来记录TOUCH_BEGIN时的触点ID. 这里有一个小技巧: 摇杆TOUCH_BEGIN后, 把TOUCH_MOVE和TOUCH_END侦听器注册到stage上, 这样可以做到手在整个屏幕上移动, 无需保持在摇杆的区域内.

巨坑来袭... 第2触点问题

  上面的Joystick实现逻辑上看不出什么问题, 在PC/iOS上也验证过没有问题. 然而到安卓机器上, 诡异的事情发生了: 当一个手指A持续移动摇杆, 另一个手指B在屏幕另一个位置上按下并抬起后, 游戏帧率直接由30帧降到10帧! 更诡异的事情是, 移动摇杆的手指A抬起后, 游戏又恢复回30帧. 总之: 移动摇杆过程中, 一旦出现第2触点的按下/抬起, 帧率立刻骤减; 而全部触点抬起后, 又恢复正常.

  咨询了李大师/牛奶等大大们, 他们都木有遇到过, 只能自力更生. 于是祭出NeoX引擎内置的Profiling. NeoX Profiling的好处是, 可以在release版的引擎中, 通过脚本打印出各个模块消耗的时间,  具体使用可参考Reference1. 在脚本里写了一个函数log_performance, 每秒钟打印一次性能数据.

  查看数据后, 发现帧率骤减时, InputUpdate消耗的时间竟达到了60ms, 而正常情况下改值<5ms. InputUpdate是我在引擎里增加的, 用来记录engine/game/game.cpp里这段代码的耗时:

  继续跟进代码, 发现问题出现flashui的touch-mouse事件映射上. 原来, NeoX为了保证flashui的mouse事件在手机设备上也能响应, 会自动把第1个touch事件当成mouse事件传入flashui处理. 基于未知原因, 第2个触点抬起后, 第1个touch事件导致的mouse事件处理变得相当耗时. 这个未知原因, 我怀疑可能是:

  我们游戏使用了单movie结构, 即只有一个pymovie, 所有的swf都通过AS动态加载到这个pymovie上. swf资源总量达120个, 同时加载到内存的有数十个, 每个swf有自己的AS脚本侦听mouse事件. 这样的量级极有可能容易出问题.

  虽然不知道这个坑的具体原理, 但是定位了问题, 绕开这个坑的方法就有了. 我们采用的方法是: 每次joystick开始移动时, 禁用flashui movie的鼠标事件; 结束移动时, 启用鼠标事件. NeoX把mouse事件传入flashui处理之前, 发现该movie的mouse事件被禁用了, 于是立刻返回. 如此这般, 帧率终于正常了.

  在Python层用下面代码控制movie的mouse事件处理.

4.png

  关于第二触点问题的一些事实:

  G4/H2都不会出现这种问题.

  禁用鼠标是一种绕坑的方案, 不是填坑的方案. 在joystick移动时, 无法同时点击侦听mouse事件的按钮, 此为最大之弊端.

3.2 寻路方案

  与原来H2的触屏输入模式相比, 摇杆对于寻路的性能要求更高, 体现在:

  触屏模式下, 玩家每秒点击的次数不会太多(每秒5次就很累了), 每点击一次需要做一次寻路. 另外, 玩家还可以保持手指持续接触屏幕, 游戏会以10次/秒的频率进行寻路. 

  摇杆模式下, 玩家会一直移动摇杆, 10次/秒的频率实际上看起来还是不够精细, 特别是当玩家绕圈圈时, 可以明显感觉角色转向角度不够平滑.

  触屏输入的寻路要求是: 次数少, 目标距离远. 摇杆的寻路要求是: 次数多, 目标距离近. 我们在摇杆的寻路方案上做了3次迭代.

摇杆寻路迭代1: 基于get_path

  第1次迭代完全按照触屏模式的寻路方案做, 直接使用detour.get_path, 并且控制每秒寻路的次数不超过10次. 

  get_path寻路算法流程大致如下:

  1.找到起点的最近可达点s.

  2.找到终点的最近可达点e.

  3.在s与e之间规划一条路径p. (最耗时)

  这个方案的缺点, 除了上面提到的角色转向角度不甚平滑, 还有另外一个: 当副本中怪物比较多时, AI消耗较大, 而持续的每秒10次寻路造成的消耗就显示出来了, 容易造成卡顿, 在中低端安卓机上特别明显. Python代码如下:

摇杆寻路迭代2: 基于find_nearest_poly

  我们考虑了一下, 摇杆每次移动角色时, 不需要移太长的距离. 假如每秒10次移动, 那么每次移动的目标点只要比角色在这段时间内能达到的距离稍微大一点就可以. 因为在角色还没抵达目标点停下时, 又有一个新的移动指令. 至此, 我们想了一种短距快速寻路的方案.
    短距快速寻路基于一种假设: 指定的目标点离当前位置比较近, 中间不需要绕过障碍物, 可达路径为一条直线. 其流程如下:

  1.使用find_nearest_poly找到终点的最近可达点e.

  2.如果e与当前位置的距离d, e与当前位置的detour高度差h均在一定范围内, 则认为假设成立, 直接以当前位置和e作为路径.

  3.如果2的判定不成立, 则fallback回get_path寻路模式.

  通过Profile, find_nearest_poly寻路比get_path大约快2~3倍, 而且寻路结果貌似还不错 (后面会说到, 这是一种错觉). 我们直接把每秒执行的寻路次数调到20次, 在安卓上流畅运行. 代码如下:

摇杆寻路迭代3: 基于ray_cast

  正当我们洋洋得意时, 半个月后策划带来了不幸的消息: find_nearest_poly寻路方案在多数情况下运行良好, 然而某些情况下可以用摇杆控制角色, 强行通过锁区开关Swtich, 如Fig.2所示: 本来角色应该被锁在围栏左边, 打完怪物后, 围栏才打开, 让玩家通过. 但是用摇杆可以在某些角度下穿越围栏, 于是玩家到了围栏的右边.

Fig.2 穿越围栏

  锁区开关是通过detour.set_detour_flag对标识某些area不可通过, find_nearest_poly某些情况下不能判断起点与终点之间是否被不可通过区域隔开. 我们需要用到另一个更严格的方案, 即ray_cast. ray_cast可以在起点和终点之间找到一条可以通过的直线路径, 而消耗比get_path少. 换了ray_cast之后, 摇杆再也不能穿越Switch了! 代码:

  总结下这3种寻路方案:

  get_path. 适用于长距离, 有Switch的情况, 计算出复杂的折线路径.

  ray_cast. 适用于短距离, 有Switch的情况, 计算出直线路径.

  find_nearest_poly. 适用于超短距离, 无Switch的情况, 计算出直线路径.

  游戏实际数据表明, 这3种方案的耗时比例大约为 3:1.5:1

4. 功能按钮面板Joypanel实现

  Joypanel的实现相对简单, 因为我们可以在Flashui上对每个元件进行Touch事件的侦听. 对于btn0, 使用下面的AS脚本:

  按钮面板有8个按钮, 为每个按钮的Touch Begin/Move/End事件做处理. 然而对于像毛老师这种手速快的玩家, 经常会造成scaleform Touch事件的丢失or乱序, 导致按钮响应不正常. 比如:

  技能按钮不能回弹, 需要再点击一下才能恢复.

  攻击按钮一直处于按下状态, 怎么按也不能恢复. (该按钮上的Touch End事件丢失了)

  折腾了很长一段时间之后, 我们认输了. 在这么多按钮各自为政, 响应touch事件的情况下, 容错性很低, 而且在手机上这些按钮彼此靠得很近, 按下按钮A的同时可能同时按下了按钮B, 抬起操作可能又被按钮C检测到. 总之, 我们需要的是一套容错性更强的机制.
  Joypanel按钮事件优化

  我们使用一个透明底盘M, 把8个按钮都包围起来(如Fig.3所示), 只处理底盘M的Touch Begin/Move/End事件, 在这3个事件里去判定哪个按钮并按下, 同时增加一些容错处理. Joypanel优化前需要20+触屏处理事件, 现在只需3个, 稳定性杠杠滴.

Fig.3 Joypanel透明底盘

5. 总结

  摇杆的多点触摸检测优先用game.on_touches_*函数簇, 这个远比ui层提供的机制要稳定得多.

  flashui如果出现多触点引发的帧率降低, 可尝试把movie.enable_mouse设置为False.

  用detour进行高频率、短距离寻路时, 尽量考虑用ray_cast/find_nearest_poly替代get_path, 可以提高不少性能.

6. References

  1.如何使用profiler 来查找CPU的性能问题.  http://km.netease.com/wiki/show?page_id=3326

  2.ActionScript3 Touch, multitouch and gesture input. http://help.adobe.com/en_US/as3/dev/WSb2ba3b1aad8a27b0-6ffb37601221e58cc29-8000.html

  3.NeoX 手势、键盘、鼠标输入相关.  http://km.netease.com/wiki/show?page_id=4283