说是不完全指南,因为有些硬伤在之前已经说过了,本文偏重具体解决方案。
斜面上人物运动
学过物理的同学都知道,物体的静止是相对的,我们也一直在随着地球在运动。但是游戏的世界中,两个物体的位置是人为设定的,也就是说,当我们设置地板运动时,如果人物不运动,那它就不是静止的,所以我们设置地板运动的时候同时也要设置人物运动。如果人物是相对静止的话还好,我们只要通过不断计算改变人物的xyz坐标即可,但是问题是,人物也许也在运动,我们可以通过操纵摇杆进行人物坐标的变化,这时候似乎看起来二者都要同时改变人物坐标,会造成冲突。
其实不用想那么复杂,首先先搞懂人物运动是怎样一个概念。人物可以通过摇杆操纵或者被敌人击打到达下一个目的点,虽然这块运动的逻辑不是我写的,但是看源码发现运动的实质是根据方向和距离确定下一帧要运动到的位置的坐标。这一块是专门做游戏开发的程序员交流后,他是根据局部坐标转世界坐标的方法,根据人相对于所在砖块的位置确定的,我想了想是个好方法,但是转换步骤太多了,所以没用。我的思路比较简单,根据相对静止来做,不管斜面运动到什么地方,它上面的每个点一定会对应没运动时平面上对应的点。同样,反推来看,假设人物已经在斜面上进行运动了,那么无论它运动到什么地方,一定会对应它在相应的平面上运动的一个点,而这两个点的坐标我们是可以通过斜面的方向向量进行换算得出的。
这样一来思路就很清晰了,之前摇杆控制或人物被击打后得出的下一个目的点的坐标会直接赋给人物的位置属性,现在我们需要判断下人物是否在斜面上,如果在斜面上则不直接赋值,而是把这个坐标保存下来,根据斜面翻转的角度计算人物在斜面上的坐标再赋值。至于斜面上的各种状态(比如有陷阱掉落或者后续还需要增加更复杂的状态),都可以根据对应平面上的坐标进行计算或判断,然后映射到斜面坐标。
解决完这个坑,还有一个坑是是人物跑上斜面、或者从斜面跑下来,这里有比较复杂的地板状态控制和人物状态控制,倒没有什么实际的技术含量,但是需要理清楚各个状态已经状态变化时人物的行为和是否进行坐标变换。
比如有次遇到人从斜面上掉落后发现落地位置不大对,与之而来的各种问题也出现了,比如人从斜面上走下斜面后就无法行走了。开始不知道先解决哪个好因为两个问题都有点大,第二个问题大概明白是走下斜面后状态未及时改变的原因,但是为什么会不改变呢?排查了半天也找不到原因;落地位置这个问题更奇怪,因为之前是正常的。仔细一想后一个问题也许是前一个问题引起的,由于运动轨迹不正确,导致映射在平面上的坐标不正确,导致检测不准确,不过是个猜想。为了方便调试,首先把代码切换回单机版,这样可以排查落地位置问题,通过各种排查询问发现地板翻转的速度函数改变了,所以在工具类里面封装了一个方法,用于处理人物随斜面运动的旋转角度,这样之后地板翻转再处理人物翻转的角度和位置更加方便,之后翻转的速度函数再有变化我这里也方便修改,调整完毕后再测试此时两个问题都恢复正常了。
关于人物在斜面上坐标的计算,由于斜面可能有四个方向的翻转,之前的方法为了清晰直接分四个方向单独写的,每个方向有翻转方法和掉落代码。但是存在几个问题,第一,存在大量重复计算;第二,不利于修改,比如上面修改角度和轨迹,如果按之前改的话需要改很多代码;第三,代码冗余,四个方向逻辑其实差不多,但基本上每行代码都有些不一样的地方,比如正负号,比如坐标设置,比如加减运算等。当时设计方法的时候也考虑后看能不能把主要逻辑抽离出来,但是想了好久也没有想到好方法。后来的做法主要是使用闭包进行封装,保存一些重复的计算结果,同时根据地板翻转时传入的参数设置正负值、以及确定设置哪个轴的坐标,将主方法(翻转和掉落)抛出,这样可以使代码更加清晰简洁利于维护,不用每次都计算复杂的逻辑,也使运行效率得以提高。
这方面还有一个坑是,性能比较差的时候,人在斜面上行走会直接走到地板外面,排查了好久,发现各个信号量都是预想的值,逻辑上没什么问题,但是有时候判断人物是否在翻转地板上时会返回false,所以猜想是不是因为性能比较差,某次循环里一些逻辑还没来得及执行就进入下一次循环了,所以在行走的地方加了一些容错判断。
如何封装一个十分仿真的假进度条
为什么只能做假进度条呢?因为我们只能判断静态资源是否加载完毕,无法判断接口是否请求到、场景是否渲染完毕,而实践发现这个时间有时候是很长的,如果在静态资源加载完了我们就到达100%就会停留很长时间。所以首先在loading类中定义了三种加载策略:
- 根据加载进度
- 平均推进
- 用1减去当前进度计算推进值以便在一定时间达到100%的进度
对于一些页面初始化时有请求时间比较长的接口,先让其跟随加载进度到达一定值,然后调用平均推进的方法,接口加载完成后设置一个延时,同时用1减去当前进度除以延时时间计算推进值使其在延时时间内进度到达100%,延时完毕后实例化页面;而对于页面初始化时无很长时间的接口请求情况,直接根据当前资源加载进度值改变loading即可。
物理引擎计算修正优化
使用物理引擎获得速度值的时候遇到一个很大的问题,有时候两个物体碰撞后速度方向完全和预期方向相反。感谢游戏策划大大,想到可能是某帧检测时子弹已经穿过了物体才开始检测碰撞。解释一下,物理引擎本身一直在做迭代计算,有点像初中物理课上玩的打点计时器,所以可能两个物体刚刚接触的那一帧没有检测碰撞,但是两个物体快要分离的时候检测到了碰撞,这个时候力的左右会给物体一个反方向的速度。解决方案也简单,可以做一个速度方向和子弹发射方向角度差值的判断,大于一定值直接将速度方向反向,为了游戏效果,有时候不能完全用真实碰撞的数据,比如子弹擦边打物体的时候实际会把物体向旁边推而不是像前,游戏中这样视觉上很难确定击打者,体验上不是很好,所以这一步也可以做一个优化,控制一下被击打者的速度方向和子弹发射方向夹角的范围,超过直接取差值的一半或三分之一。
碰撞检测问题
为了加强精确度,游戏中使用了layaAir引擎和物理引擎双重碰撞检测,但是出现了一种情况,经常会检测不到碰撞,后来发现是发生两者检测不同步的情况,即这帧laya检测完毕之后下一帧物理检测才开始,或者反过来,导致有时候明明打到了碰撞检测却不通过。后来直接把laya的碰撞检测去掉了,但是因为物理物体和实际物体不能完全重合,物理检测会发生不精确的情况,比如根据实践发现物理球的大小在半径为1左右产生的速度角度视觉上来看最理想,但是这个时候物理物体是碰撞到了,实际物体视觉上看上去并没有碰撞到,并且表现很明显。
所以解决方法是,检测到的时候保存检测到的状态,二者检测状态都为true的时候判定其碰撞到,碰撞处理逻辑中再重置状态。
圆角斑马线条纹loading条设置
做玩家头像的时候遇到圆形和圆角矩形的裁剪,但是laya里面不能设置图片border-radius,哪怕新增DOM节点加进去,这个属性也不生效。后来发现可以绘制一个矢量图形,加到图片的mask属性下。圆角矩形图片我的实现是绘制一个圆,圆的直径略大于图片的宽度,这样遮罩裁剪出来就是圆角矩形了,但是微信获取的头像应该不一定正好是正方形,长方形相交就不是圆角矩形了,这个后面应该还要改,不过直接绘制一个圆角矩形也是可以的,这个后面根据确定的UI再改吧,知道方法后改起来也不是很困难。
由于有花纹和圆角,不好直接拉伸变换,也不好直接用laya的loading条,受之前设置圆形头像方法的启发,给loading的progress bar加了个固定长度的mask,然后改变progress bar的长度,就可以达到效果了。另外发现laya不能加载gif动画,所以条纹不断移动的动画使用图集动画完成。
销毁对象实例
发现有时候destory sprite对象的时候,执行完了对象在内存中依然存在,需要再执行一次,在性能较差的机器上尤为明显。猜想是不是由于掉帧,错过了这个方法的执行,而我们每次在执行完destory后为了释放内存都会手动解除对象引用置为null,下帧执行时又找不到这个对象了,所以destroy有时候不能执行。想到的方法是在执行完之后的一段时间内找到需要销毁的对象,判断是否存在于舞台上,如果存在再次执行销毁,这样可以对掉帧对象消失不掉的问题进行一定程度上的容错。
还有一种方式是维护一个需要销毁的对象的队列,每当有对象需要销毁时执行完destroy方法后将他加入到该队列中,每隔一段时间遍历下该队列,如果对象仍存在于场景中,再执行一次destory方法,同时从队列中移除。
抓包工具使用
说来惭愧,一年前刚刚考完《计算机网络》,很多基本的东西不算忘得一干二净,好久不用也忘了十之八九。用抓包工具是因为服务端有个数据包没发过来,导致我们后续的开发几乎无法进行,由于我不怎么用公司的windows电脑,所以为了我们调试方便,在我电脑上部署了一份服务端代码,整个项目:游戏服务端-node中转-客户端,node反映不是它那层没转发,请服务端打个log看一下,服务端表示他们打log太麻烦,而且不一定是他们的问题,他们很忙没空理我们,组里大大们说那我们抓个包给他们看,于是大大在旁指导,我们进行了愉快地甩锅活动。
安装wireshark,开始抓包,过滤数据包,过滤规则网上一搜一大片,也不列了。我们需要匹配的是一个tcp包,已知前四个字节的16进制码,所以需要过滤协议、地址、端口,需要匹配的相应位数的字节,开始进行过滤的时候总是过滤不到想要的包,后来发现tcp首部的字节没算上,用总长度减去data的长度得到首部的字节数,然后就可以从data的第一个字节开始过滤想获取的数据包了。
屏幕适配问题
laya是有自己自带的适配模式的,并且可供选择。
这也是一般实现移动端适配的几种方式,预先定义一些CSS类,然后根据不同元素选择其对应的适配方式,该拉伸拉伸该裁剪裁剪。但这里的适配是针对整个画布的,所有的东西都被绘制在了一个canvas上,如果拉伸以适应整个屏幕的话肯定会遇上元素变形问题;如果裁剪,由于元素的定位是相对于画布的不是相对于屏幕的,处于边缘的元素会被裁剪掉;还有一种方式是“缩小以全部显示”,这样会留黑边或白边。另外一点是laya提供了自动横屏功能,即设置为横屏游戏的时候,将整个canvas旋转,在竖屏状态下横屏显示,通过以上分析,其实最可能可行的适配模式是有裁剪、全屏显示、元素不变形(有黑边或变形体验比较差)。但是在safari或微信浏览器下,横屏和竖屏的时候屏幕大小是不一致的(比如微信那个横条),这就导致了用户旋转屏幕的时候,屏幕大小其实是不一致的。而以上所有适配模式都要设置舞台宽高,引擎会根据舞台宽高和设备宽高进行适配操作,比如裁剪,一般是设置成设计图的尺寸,设计图的尺寸是减掉微信横条的屏幕的尺寸比,这样竖屏大部分手机基本上都可以正常显示,但是横屏状态大部分手机就基本上都不能正常显示了,因为舞台宽高比率和实际设备的宽高比率差太多。另外还有刚刚说大部分,还有一些手机比如全面屏的一些机型(都不敢说x了…一般全面屏的手机),除了微信顶条,还实际可视区域的大小还需要减去底部操作栏;最后从长远性来看,游戏如果要接入其它平台,比如facebook,尺寸又不一定一致了。
如何实现屏幕适配呢?我们发现,fixedwidth,fixedheight,fixedauto,noborder这几种是等比缩放的,经过各种效果比较最终选择noborder,按最大比率缩放。
总结一下之前所说的问题:
第一,可视区域大小与屏幕大小不一致
第二,横竖屏切换时可视区域比例变动
针对第一点,开始的做法是结合laya的适配模式对微信内页面进行了适配,Android、ios不同机型测过基本正常。基本思路是将舞台大小和场景大小设置为减去微信标题栏的,然后对个别靠边元素再进行调整。但这样做有几个问题,第一,虽然现在是只做微信内的应用,但是如果这样做的话后期扩展到其它平台会有难度;第二,有些机型不仅要减去标题栏,还有底栏,这个之前是稍微对这些机型调整了一下靠边元素的坐标,但其实不是很具有通用性。
Laya中适配的一个难点是舞台宽高不能动态设置,官方有提供API进行改变舞台宽高,但是在设置屏幕自动旋转的情况下直接调用API会存在各种问题,比如重新设置后发现屏幕的方向变了,或者进行了位移。舞台宽高既然不好设置,那设置场景宽高,只要场景宽高与实际手机一致,那也不会出现边缘被裁剪的情况。
具体做法,首先动态获取手机宽高(边长为高),设置舞台宽度为设计稿宽度,设置舞台高度 = 设计稿宽度*手机高度/手机宽度。每个页面的高度也在进入时改成此高度,同时把页面push进一个数组,每次改变屏幕方向的时候再获取一次宽高,遍历后重新设置。
这么做还有一个好处是后面可以把2D资源重新组合一下,把不需要经常改变的东西静态缓存成位图,减少sprite实例,一定程度上也能提高性能。
第二点,横竖屏切换时可视区域比例变动。这个要说解决方法也不是完全没有,把靠边元素遍历一般重新设置一下就好,但是消耗太大。和产品以及设计大大商量了一下加了个锁屏提示,当横屏时提示锁屏。