<ignore_js_op>
序言:转型手游,问题比想象要复杂。一些问题是研发阶段就能预见的,但是有些问题上线后才发现远超出我们的想象。
从端游转型做手游变化远没有想象简单
可能和公司内很多手游研发团队一样,我们也是从传统PC端游转型做手游的。 我们一度认为手游研发会比较轻松,技术上跟端游比起来相对容易, 人力上也不需要太多投入。但真正开始做之后才发现无论技术还是产品各方面远没有我们想象的简单,好在我们的核心成员都有多年的端游开发经历,不光有相应的技术和经验积累,还养成了一些好的工作习惯及方法,在遇到问题时能够及时调整和灵活应对,我想这也是我们项目研发从总体上看还比较顺利的重要原因之一吧。
最开始遇到的问题主要还是技术上的,我们没有Unity3D引擎的实际项目经验,心里面没底,不知道研发过程中会遇到哪些坑,对于iOS/Android平台编程更是一窍不通,项目进度又紧,不可能有充裕的时间自己去学习和摸索,万幸得到兄弟工作室团队的无私支持让团队少走了很多弯路,我也从他们身上学到很多移动端游戏开发的一线实战经验。
我们的研发团队规模一直都比较小, 直到项目上线也只有十来个人,不少美术同学还是兼职的。 正式运营后才深刻认识到我们的人力规模完全没法适应玩家对于游戏内容的消耗速度。在工作室和产品中心的支持下,队伍得到不小的扩充,目前是30人出头的规模,也算是一个中等规模的团队了吧,然而团队规模扩大了又会凸显出很多新的问题,比方说工作任务间的偶合,多线开发的版本管理等。
对于小团队来说,解决问题可以很直接,只要有可行的方案,可能一句话就搞定了,尤其是我们有几个人做天天飞车之前就已经磨合两三年了,配合起来非常默契。但团队规模大了之后,可能就会涉及到一些团队管理问题了,如何让大家拧成一股绳去做事情,并不是简简单单把问题指出来就可以了, 这时候制度和流程就显得尤为重要了。
早作规范早验证,避免返工。
寻找表现与性能的平衡点很重要
我们的一个习惯就是一开始一定要把游戏最核心的东西、最可能遇到的技术难点,不管是玩法方面的还是表现方面的,彻底捋顺想清楚了做出Demo来验证,然后才是制定标准和量产,这能提高开发效率降低走弯路的机率。
天天飞车的Demo做的非常快大概两三周就基本完成了,包括了无限赛道的动态拼接, 碰撞检测,车辆的碰撞响应,车辆和道具动态生成等等这些最基础的功能, 而且也已经具备了最基本的玩法逻辑比如说惊险超车。虽然以现在的眼光看起来它很原始也有很多问题,但如果撇开画面表现和细节手感之类的不谈,天天飞车最最核心的东西其实就是这些。
对于客户端来说可能最首要的问题就是设备适配问题,Unity3D有很好的封装性,系统和设备的特殊性大多被隐藏起来了,所以兼容适配基本不用操心,就算后期测试时发现一些兼容问题针对性的处理下也就好了。分辨率适配也没有太大问题,业内广泛采用的NGUI界面中间件提供了一套基于锚点和等比缩放的很成熟的方案,我们只需要按照它的规范去做就行了。所以,最需要投入精力的还是性能适配问题。
按照以往做端游的经验,如果有成熟的引擎和技术方案作为基础,性能适配问题的核心其实就是订立合适的资源规范并严格执行。资源规范主要分为两类,一是数量上的,包括单帧渲染的面数及顶点数,DrawCall数,同时载入的贴图和音频容量等等。另一类是技术规格上的,如光照模式,贴图格式,音频格式等。在制定规范前,我们首先依据微信和手 Q的用户设备统计数据确定了游戏的最低配置要求,因为我们的用户就是他们中的一部份,而且很显然,游戏的配置要求越低,将来的用户量也就可能越大。另外因为没有经验不能一上来就埋头自己干,所以我们还参考了怪物大作战的一些规范,他们可以说是公司Unity3D手游的先驱,向他们致敬。
当然了,光有资源规范是不够的,程序上的基础性能优化工作还是必须得做好做足的,对于Unity3D手游来说主要有托管堆内存的合理使用,对象池,资源加载卸载管理等。
作为一款3D游戏,我们也一直在寻找游戏表现和性能的最佳平衡点。一个基本的取舍原则可能就是性价比,看性能的损耗带来的效果提升到底大不大,是不是用户比较关注的。另外在这点上程序和美术之间也经常会出现一些意见分歧,这很正常。对于程序同学来说希望客户端特别省,轻巧而快速。对于美术同学来说希望画面足够精细,能够最大程度的保留设计初衷可能是非常重要的。这个问题目前看来我们处理的还是比较好的,站在客户端程序的角度,一方面有些原则我们必须要坚持,比方说不能使用复杂的实时动态光影,后处理效果等确实与当前大多数移动设备硬件性能不相匹配的技术,另一方面我们也应该和美术同学一起为游戏的表现品质努力,比方说我们实现了一个效果自适应模块,能够根据用户的设备性能选择合适的效果等级,既能让配置较差的设备以比较流畅的帧率运行游戏,高端机型也能够表现出更精细的纹理,更真实的光照,更多的场景细节,更华丽的特效等等,避免了画面品质一刀切的情况,给了美术同学多一些的发挥空间。当然总的来讲,我们的资源规范还是比较严苛的,不得不赞叹我们的美术oa,在如此多的限制下,依然让我们的游戏有很不错的画面品质。
<ignore_js_op>
车库
<ignore_js_op>
高配画面
<ignore_js_op>
低配画面
端游经验用于反外挂,安全问题早作预案
相比端游而言手游面临的网络环境更严峻,我们不可能做到实时通过服务器校验玩家的每一步操作,从延迟,稳定性,流量上讲都不允许。单局的玩法逻辑大都是在客户端完成在结算时才统一上报服务器的,玩家就有可能修改一些关键数据,比方说分数,金币,Buf持续时间等等,从而获得非法收益,这就必然牵扯到令人头疼的反外挂问题。
基于以前端游的积累我们初期就预想到了这个情况,比较早地开始了对抗准备。我们的外挂对抗体系大体分为三层,第一层是客户端的防御,除了必不可少的协议加密之外,客户端对内存中的关键数据也都是密文存储的并且加上了校验码,这样通过烧饼之类的通用内存修改工具就比较难定位数据的具体位置了,而且就算真的找到了内存地址,修改之后也会导致校验失败,这样客户端就能侦测到内存数据的非法篡改。第二层是我们自己服务器的即时对抗,客户端在单局结算时上报的数据中不光有最终的结果还有一些和结果有关联的中间数据,比方说实际生成了多少金币,是否有出现某种特殊车等等,如果外挂只是修改了部份数据,服务器就可以根据校验公式检测出数据被非法篡改过了,服务端kenny以前在QQ飞车就做过类似的对抗,经验非常丰富。最后一层是互娱安全组的后校验,他们采用的检测方法和我们服务器的类似,只是不会对作弊行为进行实时处罚。
还有一个要特别注意的是,在Android平台上,Unity的C#脚本是以JIT方式运行的,apk包里的程序集dll文件很容易被Reflector等工具反编译,一旦被别有用心的人知道了客户端逻辑到底如何运作的,就可能做出一些比较逆天的外挂来。当时我们想了很多办法,一开始是做混淆,但发现执行起来不是很方便,对开发存在一定的限制。后来我们想到一个方法,我们可以对程序集dll文件进行加密,这样通用的反编译工具就打不开了,但苦于我们没有 Unity及其修改过的mono组件的源代码, 也不擅长逆向工程,我们把这个需求提给Unity的开发商,可能出于某些原因,他们也没有太积极的反馈,后来还是安全组给了我们支持,他们通过逆向工程实现了对程序集文件的加解密,之后公司开发/代理的Unity手游应该都有采用这个加密方案。
<ignore_js_op>
崩溃上报灵活处理是解决之道
针对移动应用,MIG研发了一套叫做RQD的崩溃上报系统,MSDK对它作了统一接入,所以凡是接入了MSDK组件的移动端游戏在客户端发生崩溃时都会自动捕捉上报异常的现场信息,对分析崩溃来说最有用的可能就莫过于调用栈了。
RQD 虽然很强大,不光支持C/C++,ObjC这些原生语言的调用栈的还原,也支持Java的调用栈还原,但可惜的是Unity开发的游戏大多的异常其实发生在C#层,iOS平台还好,C#是以AOT方式预先编译成了原生代码,调用栈会被当作C语言的对待,从函数名上也能基本定位到C#中具体发生崩溃的地方。但Android平台就没这么幸运了,C#是被预先编译为IL中间代码,以JIT模式在mono虚拟机中运行的,所以一旦C#层出现异常,RQD报上来的调用栈反映的都是虚拟机自身发生了异常,无法定位异常发生的具体位置,然而Android崩溃率大大高于iOS,恰恰是需要更多关注的。
后来通过猜测和试验,我们发现其实在C#层产生异常时,Unity可以通过回调的方式把异常类型和调用栈告知应用,这正是我们需要的,但如何把这些信息上报又成了问题,因为一旦RQD捕捉到异常就会立即kill掉应用,可能应用都没有机会处理这些信息,我们把情况反馈给了RQD的研发组,但短时间内他们也很难有比较完善而系统的解决方案。后来通过和RQD开发同事不断的探讨终于找到了一种看起来简陋但却行之有效的方案:由他们提供一个发生异常时延迟kill 掉进程的定制版本,在异常发生时我们把需要额外上报的异常信息以追加的方式写入会上报的tomb文件。采用这种方法,我们所需的C#异常信息在 Android平台也能有效地通过RQD上报了,而且能在后台页面比较方便的查看。
<ignore_js_op>
安装包容量缩减方案分享,多种方法协力显功效
天天飞车属于最早那一两批上线的微信/手Q游戏,为了尽量降低玩家进入的门槛,当时对安装包体积的要求还是蛮严格的,至少要控制到40M以下,而当时我们的安装包体积已经突破50M了。
第一步,我们还是检查梳理了一遍资源,看看有没有资源没按照规范来制作,有没有资源是冗余的,这应该是最基本的,这项工作的一部份可以通过自动化的工具来完成,利用Unity提供的编辑器扩展接口来实现还是很方便的。
我们分析过安装包的具体构成,其实还是贴图资源占比最大,所以很自然地应用了TinyPNG之类的减色工具来减少贴图的信息量,实际结果表明这个优化对决大多数贴图表现的影响都是可以接受的,仅有少量的UI贴图需要区别对待一下。
渲染中文字符所使用的矢量字体文件ttf的体积对于手游安装包来说实在是有点大,一个最基本的优化手段就是使用精简字库,但精简过渡又会导致很多字符无法正常渲染,所以最后依然会占用5M左右的空间,当时的一个想法就是可否直接把移动设备操作系统的字体文件拿来用,感谢iOS这个封闭的系统,字库很统一,这个方案确实是可行的,游戏首次运行时通过系统API直接从系统字库提取出字形信息组装成了ttf文件,但Android系统就不行了,各种系统版本字库五花八门,只能还是老实地把精简字体文件打进安装包。
还有一个大头是在音频上,在PC 端上游戏音频一般直接采用mp3/ogg等流行的压缩格式就可以了,但终端设备尤其是安卓设备很多不具备音频的硬件解码能力,CPU运算能力又相对较弱,在做性能分析的时候发现,一些中低端设备,甚至某些比较高端的设备,在音频解压这一块都有相当大的性能消耗。为了流畅的帧率我们最初不得不采用非压缩的 wav格式。但问题很明显,非压缩格式的音频文件体积太大了,因为信息量特别大,打进安装包里也缩减不了太多。后来我们采取了一个两全其美的方案,安装包里依然存放压缩格式的音频,游戏第一运行时,把音频解压成非压缩格式来播放,这块当时节省了8M多空间。
经过这些努力,我们的首发安装包体积成功的控制在了30M的水平,但需要注意的是苹果会对可执行文件加密,这会导致AppStore中的版本会比提交审核的原始包大一些.
手游与省电似乎天生存在矛盾,小技巧暗藏客观效果
与普通的App相比,手游耗电确实要厉害得多,大家也都比较关注这一块,影响手机耗电的因素很多,CPU的占用率,GPU的运算量,屏幕的亮度,网络模式和通讯量等等,都会影响到电量消耗的快慢,然而手机网游恰恰是这些硬件资源的消耗大户,而且消耗的更多往往就能给用户带来更绚丽的表现,更流畅的操作体验。
一方面,我们要做好游戏的性能优化,这是最基础的,因为这样就能以更少的硬件资源消耗带来基本相同的游戏体验,另一方面,我们也可以采取一些小策略,小技巧来达到相对省电的效果,比方说我们将游戏刷新率最高限制在了30帧,因为这对于天天飞车这种类型的3D游戏的流畅体验来说基本足够了,又比如在大厅里面挂机或者暂停游戏的时候, 其实不需要保持比较高的刷新率,这时候就完全可以把帧率降下来,自然而然就获得了省电的效果。当然这只是两个很小的例子,具体游戏可以根据自身情况仔细挖掘,灵活利用,运用的好的话效果应该是相当可观的。
<ignore_js_op>
引擎选择必须要适合自身项目,能否驾驭和掌控至关重要
对于新的手游项目来说,从一开始立项就需要考虑好,选引擎就是要适合自己的,看你的游戏类型,团队规模,还有很重要的一点是看周围环境是怎么样的,如果你选择了一款很少人用的引擎,当你需要交流、需要别人帮助的时候,你可能都找不到人。国人做事讲究天时地利人和,我觉得有一个好的交流分享环境应该算是地利之一吧。
当时我们选择Unity来开发也是有多方面考虑的。首先我们要开发的是一款中小型的,3D的,移动端游戏,需要同时发布到iOS和Android两个平台,我们的团队规模很小,Unity的应用非常广泛,有不少和我们同类型同规模的游戏的成功案例,比如一起车车车,神庙逃亡等,公司也已经有了Unity开发的且已上线的移动端游戏项目,其次Unity本身也是一款开发效率很高的引擎,它的集成开发环境非常好用,资源整合很方便,游戏逻辑主要用C#等托管语言编写,和引擎本身的实现是完全隔离开的,开发,编译效率都非常的高,改完代码,几秒钟就能看到实际跑起来的效果,手游这种特别需要快速迭代的项目再适合不过了。再次,对于3D游戏开发开说,Unity提供的功能也是相当完备的,扩展起来也很方便,可以用C/C++,ObjC,Java编写运算密集型的或者需要直接访问系统底层接口的组件以插件的形式供游戏逻辑层访问,编辑器功能也很容易扩展,此外Unity的AssetStore提供了一个很好的组件交流平台,需要什么额外功能的时候去看看,也许能避免重复发明轮子。
2D 游戏我们目前不推荐Unity来开发,毕竟这个引擎原本是面向3D游戏的,功能也主要是围绕这一块来做的,如果你只是做一个2D游戏,它不可能把你不需要的功能全部都干净的拿掉,肯定会有很多额外的开销。虽然Unity最近的新版本也逐渐在增强对2D游戏的支持,但可能还不太完善,项目案例应该也少,2D 的话我们还是推荐使用公司的自研引擎或者是应用广泛的cocos2D。
对于已上线的产品来说,在游戏玩法不断扩展,新资源不断增加的运营过程中,是否能够把消耗的增加也控制在可接受范围之内,这是很关键的。比方说,Unity引擎比较大的一个问题就是内存控制不是那么的好,不太注意的话很可能让你的内存占用超标。
一方面你自己的程序框架要合理,另一方面就取决于你对引擎的掌握程度了,如果你都不太清楚究竟是什么地方吃掉了CPU时间,什么地方占掉了内存,那可能遇到问题时你就束手无策了,这其实是一个失控的状态,对于一个已上线产品来说是很可怕的。我们不怕问题有多大,最怕的是虽然知道遇到了问题,却搞不清楚问题的具体缘由,更谈不上彻底去解决它了。Unity是一个比较封闭的引擎,少有机会去研究和修改它的源代码,所以对它的掌控,我们目前做的也并不算特别好,现在我们也是在不断的加强,看怎么去彻底的驾驭它。
作者:胡波,人物介绍:2003年开始接触游戏编程,2006年进入游戏行业,2010年加入腾讯,先后参与多款UE3次世代PC端网络游戏的开发和预研,2013年初开始移动端3D网游的研发,见证了天天飞车从萌芽到诞生及成长的整个过程,现为客户端研发负责人。