内存 vs 帧率 - 龙之谷手游优化实战(Unite2017分享实录)
关于Unite
Unite大会是由Unity举办的全球开发者大会,至今已有10年的历史。Unite现已成为游戏行业,VR/AR行业中最具有权威性和影响力的活动。
欢乐互娱资深引擎开发工程师 - 庞池海
庞池海:大家好,今天带来大会分享主要内容是手游龙之谷开发过程当中性能优化的各种经验,在短短40分钟时间内,不可能将性能优化讲的面面俱到,因为这是一个比较系统复杂的工程。希望借这次大会的机会和大家交流一下经验,取长补短。今天的分享比较偏技术。
首先我叫庞池海,是欢乐互娱的技术引擎开发工程师,平时的主要工作是做引擎方面的优化和一些引擎方面的支持,比如渲染、物理、动画等方面,以前参与过一些端游引擎的开发,最近两年主要是关注Unity平台之类的引擎。
这是今天分享的主要内容,比较简单。第一部分先简单介绍一下龙之谷手游以及优化的简介,直接进入重点,大家比较关心的两个方面,一个是内存一个是帧率。
相信大家最近都试玩过手游龙之谷这款游戏,它首先是一款无锁定的动作网游,有PVP和PVE多种玩法,PVP有战利要求,PVE是公平模式,比较讲究技能的搭配和走位。它原来是端游的IP,然后腾讯代理,微信和手Q双平台上线。这么多先天的优势和玩家的期待,使得龙之谷的优化非常让人有一种使命感。
首先我们明确优化的需求。由于是上了腾讯,所以根据腾讯以往发行游戏的经验,他们给我们提出一些比较明确的需求。我们一般看最低性能需求,如果在最低性能需求满足的情况下,其他性能都会比较好的满足。首先是安卓2G低端机型要流畅运行在20到25FPS,内存不超过350MB,iOS1G低端机型流畅运行20到25FPS,内存不超过300MB。中国最新数据表面,国内游很多1G安卓。还有场景加载速度,启动过场场景速度尽量控制在5秒以内。还有优化是对性能和效果的权衡,这点是非常重要的,不能只考虑性能而不讲究美术效果,或者反过来只追求美术效果,玩都不能玩的话,游戏也是不成功的。
我们是有专职人员做优化,讲一下平日工作的流程。第一点是发现问题,发现问题主要是有方面,一个是平时人员测试以及线上反馈,中国安卓机型非常多,有些问题一定会在线上集中爆发。平日优化人员查找具体的问题,主要看代码和资源。定位问题,一般是使用UnityEditor日常profiler定位,真机工具定位,Unity也可以直接连真机,还有用了第三方工具,还有一些GPU调试工具,比如高通的,还有instrument也是非常强大的方式,通过打bundle,替换dll等方式加速迭代周期。解决问题,个别问题特殊解决,我更提倡前期制定比较详细的美术规范和代码优化的规范,可以在后期解决很多不必要的错误。
下面简单介绍一下Unity引擎与龙之谷。我们是从4.x开始运用,现在版本是4.7.2,现在比较大型的游戏已经没有使用4.7的版本,估计这是我们最后一个使用4.7比较成熟的游戏,我们也在计划向最新版本移植。我们应该会跟进Unity最新版本,使用Mecanim动画系统,我们是动作网游,所以动画比较复杂,这里只有一层,其实还有很多层。使用NGUI,但是对NGUI底层做了一些适当的修改。比较特殊一点,我们使用了地形系统,Unity原生的地形,很多游戏不太提倡直接使用地形系统,不过这个对美术确实是比较友好,我希望Unity今后能做一些移动端的优化,这样可以减少一些我们额外不必要的操作。特效使用了animation动画和默认particle System。某些机型上使用了后处理的效果;编辑器扩展自定义了一些工具。
刚才是简单的介绍,下面介绍一下主要的。
第一点讲一下内存组成,这从开发者角度讲并不是跟严格的分配。我们有两个部分,资源内存一个是美术资源一个是数据资源,数据资源是策划的配置类型数据,美术资源是最大的。然后是代码内存,一个是Mono内存还有引擎使用内存。第三方库,SDK、一些插件。这三个部分的内存是我们需要的关注的地方。资源内存和代码内存,无论是网上还是各个群里的分享,都有比较好的检测方式。我想说第三方库需要值得注意,第三方库有些时候会封装的比较彻底,可能不知道它是不是占用很多内存,我们以前碰到占用大量内存的情况。
下面讲一下资源的控制,刚才说了内存组成的部分,我们要对这些内存进行控制。首先是最大的明确美术资源的预算,希望是在前期的时候,就确定美术的预算,尽量使用符合规范的美术资源,比如贴图的用量要规定,模型的用量也要规定,动作的复杂度、特效的复杂度,这是四项最主要的资源用量。控制游戏玩法体验,这是从策划角度来明确资源的用量,比如说同屏玩家的数量,有些低端机型上支持不了太多玩家,我们可以适当削减,比如怪物的数量,进入关卡如果游戏中发现比较卡顿的话,对关卡怪物数量进行控制。场景的规模,场景在某些时候某些特定玩法下过大,就要根据场景规模设定具体的玩法。
很多程序员都有体会,某个美术同学突然过来说,这个能不能加些面,多些骨骼,这时候就需要程序员来衡量,性能和效果到底怎么权衡。最好是让美术内部竞争,比如场景用量多一些其他方面的用量是不是能少一些。如果大家都用很多的话,游戏是做不成的。
下面讲一下具体的控制。首先是控制场景的规模,增加重用和动态加载一些关卡物件,我们并没有把所有的物件都放在场景里面,场景里面有不少动态加载的,可以进行一些充分的利用。控制场景静态合并的数量,自行进行mesh合作。Unity有一个功能,可以将场景中材质相同符合一定条件的模型进行合并,降低数量。但是这里面也有一个坏处,它会增大内存的大小还有打包时候包体的大小,mesh合并以后会生出更多的mesh,这里需要权衡什么是需要合并的,什么是不需要合并的,我们并不是对所有的物体都进行静态合并,让美术自动参与处理一些mesh让它不进行合并,这个需要具体问题具体分析。
角色怪物,根据关卡脚本分批加载怪物,控制同一波怪物的数量。这个比较直观,你突然出现5个怪和突然出现50个怪肯定效率不一样。然后拆分带alpha的怪物贴图,我们把有些怪的alpha拆分出来,这样会减少alpha的大小,带来的色差也会有很大程度的改善。还有适当减少mipmap的使用,对于一些比较小的贴图,我们会尽量不生成mipmap。
动作资源,动作资源也是一块比较大的部分,我们游戏是动作类型游戏,动作类型很大,我们不压缩的资源现在可能有两个g,动作资源压缩我提两个点,一个是删除关键帧。虽然Unity可以做关键帧方面的优化,但还是有一些误差,我们可以在工具里面写插件,用一些算法,把相同关键帧删除掉,然后自定义关键帧的压缩,把数据精度压缩。特效是要监控particlesystem和动画的数量,如果一个场景或游戏中的动画达到不合理的程度,会给内存带来比较严重的开销。比如说有一次检测到particlesystem超过一千,这时候内存会有非常厉害的增长。UI是按需区分图集,平衡共有图集数量和使用率,这是比较长期的工作。因为UI的变化巨大,美术可能一直在修改UI,这个工作要结合美术定期优化,并不是说你优化了一次就可以了,UI我们也是拆分alpha,对于没有渐变过度的alpha图进行尺寸压缩。比如规整的正方形或者圆形,可以进行尺寸压缩,效果变化不是很大。
资源及时卸载。及时释放不使用的资源,这也是老生常谈。ab及时卸载,prefab间减少资源依赖,使用动态加载,可以使ab及时释放。控制object池使用数量,对于大规模的场景,你怪物比较大的情况下,比如波数比较多的情况下,object池数量非常大,这样就需要删除以前使用的object。
避免内存泄露。被依赖的ab过早卸载会导致资源重复加载,比如一张图已经加载了,ab卸载掉了,其他地方要用这个图又要加载ab,这样会导致重复的资源叠加。一些临时资源的重复创建,我们看到一个问题,www.texture,这是两张代码,第一段代码会创建一张texture,第二张代码会创建两万张texture内存会爆掉。资源过场景被代码引用无法释放,谁分配谁释放,释放和分配需要成对的出现。然后是工具排查,使用profile截取detail memory。
下面简单介绍一个例子,这里是一个工具,可以直接接上apk跑起来,这里可以看到内存增长的情况,我们看到这个内存增长是比较平稳的,我们可以具体选某一项内存的数据,然后点击右边的小箭头,就可以进去看到某一时间段详细内存分配的状况和当前内存是否一致,右边可以看到具体什么东西分配了。由于IOS的优势,我们可以看到Unity底层分配的情况,我们就可以排查一些具体的问题。如果这里内存不正常的话,上面表是会这样斜下上,而不是很平缓的。
另外一个是关于网络的例子,我们使用了protobuf优化,建议使用多线程序列化反序列化。protobuf有一个反射的机制,在移动平台上有比较大性能的开销,一个速度比较慢,它的反射信息存在那里,解析过的协议不需要再存在,它会存反射的信息,所以会有比较大内存的开销,这里建议减少协议数量,降低反射内存消耗。
下面一个例子是表格二进制化。前期大约20mb table使用txt读取解析,使用binary读取可以节省一半运行时内存,string使用intern处理,共享存储相同的数据结构,减少再次索引的数量,比如说表格加载储量可能需要对数据结构进行重新的制定或者重新生成数据结构,达到一些索引的快速方式,比如dictionary、queue的方式,表格比较大的时候也会有很多开销,会有很多零散的分配,所以尽量介绍这些方面的开销。还有一点是尽量减少后处理等需要rt的数量和大小,UI上如果使用3D渲染模型就直接挂在上面,这样会减少一些rt的数量。还有减少shaderMap等的大小,可以使用自定义实现。
帧率方面的优化,最大的要点还是drawCall。刚才也说过合并场景材质,减少静态物体的DrawCall,这里面还有DrawCall与内存和包体大小的矛盾。由于是换装的游戏,进行了人物mesh合并,换装部件越多,DrawCall就越多。特效分层显示,就是对特效设layer,程序按配置显示。这样会减少低端机上不重要特效,或者额外特效的显示,希望Unity内置功能,这样开发人员就可以省去很大不必要的操作。还有裁剪控制,occlusion culling、镜头控制,我们有2.5D、3D几种模式。
下面讲一下skinmesh换装例子,它有很多部件,这么多部件如果按传统做法,一个部件一个DrawCall。我们组成是需要同步显示20个人物,需要像九宫格一样走到哪里显示在哪里,这样20个人物有10个部件,人物就需要占用200个DrawCall,这个在低端机上不太能接受。我们就对人物做了一些预处理,首先是mesh提取出来,把各个部件的mesh都提取出来,把它分成各个部分。mesh会做一些预处理,让游戏中合并的时候减少一些开销,在游戏中会根据不同部件拼成整体的mesh,因为我们不是整体的套装。在游戏中使用一个材质,让所有身上的贴图混合成一张,当然这里有得有失,这么多贴图混合肯定有比较大的消耗,但是在手机上进行测试,一次混合会比八次DrawCall更具有性能上的优势,性能开销不是特别大,我们在一些低端机上进行测试,大概会降低5帧左右。这就是游戏中的最终效果。
下面介绍一下UI方面的优化。动静分离,降低DrawCall的数量,这个事情是需要一直进行的,不是说一次改后以后就不用管了。基本上每个版本上线之前,都会对UI进行DrawCall的优化,降低DrawCall的数量。UI其实也用了刚才讲的合并方式,虽然会帮你合并,但是我们又再次做了混合,当然UI的混合和人物混合不太一样,因为人物分辨率占屏幕空间比较小,所以性能消耗不是特别大,但是UI可能会造成比较大的影响,这个需要具体测试和权衡。针对飘字要做特别优化,飘字一般是游戏开发过程中,UI变化频繁比较大的部分,我们需要控制飘字的频率,减少UImesh的动态更新,控制飘字最大的数量。我们还适当降低一些UI的更新频率,还有只在变化时更新UI,CD的变化都可以使用这种方式。
减少更新。这是Unity官方推荐的animator optimize game object,还有及时停止不必要的update,减少Billboard和Animation的updade操作。比如位移旋转,都会消耗比较大,特别在低端机上,适当减少update操作,会带来帧率优化。
渲染效率的控制,后面才讲渲染,因为我们这个游戏光影效果并不强烈,所以渲染上主要不是在光照方面,主要是在特效方面,渲染不是这么重要。我们是按需重写默认的shader,Unity虽然内置很多强大的shader,但是出于对移动平台的考虑,尽量不要使用一些太大的。因为Unity会考虑到很多方面,它会比较高大全。优化一些比较复杂的shader,特别特效需要图形程序或者TA特别优化,去除cutout,合并blend,优化指令数。还有使用数学手段,不使用贴图采样。还有Unity提供一个shader lod的功能,shader有一个全局的设置,机型的不同来设置不同lod使用层级。
帧率还有一个比较大的方面就是卡顿。帧率低是一个方面,帧率突然降了很多又是另外一个方面,卡顿主要有两方面,一方面是加载,我们使用加载队列的方式,将人物在加载请求放在队列中,因为是一个块一个块加载,减少了一个物体加载的耗时,分到各个帧异步加载。然后在关卡中要平衡预加载以及及时加载的数量,预加载数量多了,对切场景有一定影响,加载少了,可能后面就会有一定的卡顿。我们是在游戏开始过程中,将需要预加载的对象加载到队列里面,在游戏空闲的时候,进行加载。有些时候会切割一些比较大尺寸资源的加载。还要小心其他操作对加载的影响,我们发现ab卸载的时候,会先把所有已经需要在加载队列加载全部以后,才可以卸载。
尽量减少GC,这个也是老生常谈的,尽可能减少new,勿以善小而不为勿以恶小而为之,能不使用new就不适用,优化不合适的new。使用objech pool,自定义数组分配池,重写容器类。还有减少C语言特性上的内存分配,减少反射的使用还有封包拆包的操作。刚才也有人提到过,尽量把一些耗时的操作放在C++层中进行操作,这也是一个优化的方向。
还有一点是代码耗时过长,这个比较少,但是也会存在。比如对敏感词的过滤,敏感词很大,聊天内容很多的话,会对耗时造成比较大的影响。序列化反序列化,使用多线程操作网络数据,UI初始化和重建,分治法离散操作到各帧,而不是在一帧中加载一个比较大的UI,之类各种各样的一些注意事项。