在日前的Unite 2017开发者大会中,巨人网络资深客户端软件工程师李东旭分享了《球球大作战》客户端优化的经验。
在《球球大作战》上线之前,国内市场没有与之相似的休闲竞技产品,解决多人实时在线竞技的技术手段也需要重新摸索,而随着这款产品在版本和内容的迭代过程中,更多的新功能也要求它能够有更好的优化。
针对这些问题,李东旭进行了逐一的解答并分享了他们一步步摸索下来的经验。以下内容经游戏葡萄整理发布(附全部演讲PPT):
大家好,我是李东旭,来自巨人网络,目前在球球项目组从事游戏性开发、功能开发还有系统优化工作。今天给大家带来的是《球球大作战》客户端优化经验分享。主要分两块,第一块讲的是关于版本迭代,第二块是关于我们遇到的问题,以及解决方案。
先来简单介绍一下这个游戏,《球球大作战》是全球首款实时对战的休闲手游,一经推出就受到苹果、安卓双端推荐,迅速风靡全球,取得众多佳绩,深受玩家喜爱。目前该游戏已在全球100多个国家同步上线,并在苹果App Store中国区游戏免费榜稳居Top10,超过1亿玩家已投入其中并热爱上这款游戏。
球球在2015年5月份立项,第一个版本两周后就上线了。这个版本主要是核心玩法,由两个人制作完成,一个前端一个服务器。因为游戏玩法比较新颖,所以我们无法预估玩家的情况,于是第一个版本的目标就尽快让它面对玩家,让玩家来评估这个新颖的玩法的接受程度。
没想到推出后,玩家反馈这个游戏非常好玩,非常新颖,以前在手游上没见过。之后我们就开始大力推进这个项目,可以看到,在我们后续迭代的过程中,基本都是每隔两周、三周就发布一个新版本,速度非常快。
其实昨天我们也发了一个新版本,基本这两年,每隔两到三周就发一个新版本。这跟普通的大型手游有很大区别,一般的大型手游,他们的开发耗时比较长,从立项到第一个版本见玩家,基本就已经过了大半年以上了。
这种情况下,如果团队有过大型游戏的经验还好一些,但是对那些没经验的团队来说,这带来的风险是非常高的。你不知道你做的这些东西,玩家喜不喜欢,很有可能就失败了,失败的概率也非常高。
这里给大家展示一下我们的版本迭代体系。我们先有一个需求,然后就进行开发,开发完成之后就立马进行测试,OK的话就直接发布了。周期就控制在两到三周,每个版本都按照这样来进行一个循环。
这样有一个好处,迭代非常快,能在做完一个核心玩法之后,马上发布出去来验证这个玩法到底好不好。这在游戏前期,效果非常好,但是到后期,你的项目越来越庞大,已经很难做到两到三周了。尤其是测试这一块,测试的工作量会越来越大,所以我们引入了一个测试服的体系。
测试服就是我们单独做一个测试版本,它比我们的现在版本功能要新许多,一些新的东西在里面给玩家测试。我们会保证一部分玩家能优先玩到新的功能,然后根据他们对新功能的反馈来继续完善,OK的话就发布。这就和MIUI的开发版跟稳定版挺像的,有了测试服之后,也不能做到完美,后面项目越来越复杂,两到三周就很难实现了。
为了更加完善我们的玩法,以及需求,我们加入了一个玩家反馈的体系,有了这个体系,我们在功能方面,能更正确地做一些选择。这里的玩家反馈的来源如下图,主要的就是通过QQ群、游戏内反馈,还有玩家见面会这三个大块来直接面对面跟玩家沟通,取得玩家的建议。尤其是每次版本发完之后,有可能会遇到一些问题或者bug,通过游戏内的反馈,能及时看到一些问题,然后通过热更新,就能立刻解决。
接下来我分享一下,在之前那些版本迭代过程中,我们遇到的一些问题,以及解决方案。主要有三个问题,第一个问题是同步,第二个是卡顿,第三个是我们年初的时候做了一个LBS的玩法,怎么把真实的地图显示在Unity里。
先看第一个,同步问题。
球球是一个实时对战的手游,在同步这块肯定免不了,我们也在一些同步的机制上、模式上,也做了一些选择,然后根据我们游戏的特性,选择合理的同步方案。
分析游戏的特性,能看到首先,球球这个游戏的规则主要是球跟球之间的计算关系,它们的关系规则简单,可以用公式来概括。用的最多就是一些物理的公式,这些比较简单,我们就很容易做出来。
其次,就是快速游戏,这个游戏中途可以随意加入退出,而且我们的进入游戏是没有载入时间的,点击按纽,就直接进去了。
最后一个就是实时观战,玩家可以观战其他玩家,在这种情况下,客户端是没有任何操作的,全靠服务器把数据发过来。
根据这些特性,我们也做了一些选择。
最终我们选择了状态同步机制。这个机制有一些好处,就是开发效率比较高,这个开发效率是相对而言的,对别的游戏状态同步可能比较麻烦,但是对我们游戏的这种机制,其实开发效率非常高的。
然后客户端的计算量大大降低,方便性能优化。客户端的一些球跟球之间的逻辑,是能用公式来概括的,而且球跟球之间的计算,因为球的数量很多,所以计算量非常大。所以这一点我们让客户端不需要计算,直接通过服务器计算,然后把结果发过来。同时因为是在服务器上计算,它的反外挂功能也比较简单。
断线重连方面,因为计算都在服务器,所有信息都在服务器,一旦断线,就可以立马连上去,恢复当前的状态。传输协议我们现在用的是TCP,TCP大部分情况下还好,主要是在延迟的情况下,TCP比UDP要差一点,之后我们会逐步将TCP替换成UDP,或者是让TCP和UDP共存。
我们看一下服务器逻辑,其实第一个版本我们服务器是不参与计算的,就是P2P的模式,客户端跟其他客户端相互连接,服务器只负责转发客户端之间的消息。这种情况下延迟非常高,因为手机跟手机之间,还有网络跟网络之间都是不一样的,大多数情况下你看到一个球,另一个玩家就没看到,然后他被吃了,他不知道怎么回事。
所以第二个版本,我们就把计算全部挪到服务器上。服务器会模拟一个流程,以50帧每秒的频率刷新,模拟一个真实的游戏内环境。发给客户端的话,按照10次每秒的频率,主要发一些球的坐标跟速度,还有吃与被吃,删球加球的逻辑。因为这种同步方式对流量要求比较大,所以为了优化传输流量,我们让客户端只发送对应视野里的数据。
客户端逻辑方面,话同步方式采用了影子追踪,只发送操作数据。服务器每秒十次发送到客户端的数据,会经过转化变成一个数据球,然后数据球是不参与渲染的,数据球会在Updete里面一直跑,如果网络延迟,这个球还在跑,就能保证客户端的球跟服务器对应的球是同步在运动,不会因为网络的停止而停止移动。
渲染球主要跟着这个数据球作为一个插入平滑,一直追着它跑。这样有一个好处就是,当网络波动的时候,数据球可能会抖动,但是渲染球一直在平滑,在玩家看来基本感觉不到。在网络延迟非常大的情况下,还是会因为卡顿和延迟,导致玩家的操作不太流畅,这块我们还在做深入的优化工作。
第二个问题就是卡顿问题。
卡顿问题,基本大家都遇到过,然后就是怎么优化的话,网上做优化的有很多的方案,这块就相同的优化点,我就不怎么详细说了。主要介绍一下我们这个球球在卡顿这块的一些,因为特殊原因造成的卡顿是怎么优化的。
如下图,球球主要卡顿点就在于,因为服务器是十次每秒刷新,因为是十次,不像客户端帧数比较高,所以你发一次的话,这个数据量里面球的数量其实非常大的,这就会导致发过来这一刻,要处理大量的数据,会造成卡顿。
还有就是美术方面的问题,因为我们球上面挂了很多组件,这些组件尤其光环和圣衣这个东西越做越炫,会引起卡顿,而且我们一局游戏,有50多个人,而且中途还会再加入新的玩家,如果在进这局游戏之前把所有东西都加载好,这个内存是肯定承受不了的。
而且我们的游戏是可以立即进入的,这就造成一个问题,我们必须要在玩的过程中来加载美术资源,这样也会引起卡顿。
遇到一个卡顿的话,我们得找这个卡顿在哪里,然后找到的卡顿点,我们还得分析是由于什么东西造成的。
球球这块,主要就是这两个地方:球体组件加载卡顿,大视野大量球刷新卡顿。
先看一下球体组件的加载。现在球球里的光环跟圣衣已经有三百多个了,最坏的情况下,可能每个玩家用的光环跟圣衣都不一样,这样在一局游戏里面都显示出来的话,对游戏的内存、渲染的压力非常之大。
现在玩家审美要求越来越高,我们做的东西也越来越炫,这样造成非常多的卡顿,接下来给大家介绍一下我们是怎么优化这块。美术这块优化,这次没怎么讲,因为网上美术这块怎么优化有很多,我主要讲是怎么加载的。
我们先看一下组件的构成,首先是球体,它其实是一个2D的慢视平面;其次是名字,名字这块我们是用自己写的一个组件,自己写的好处就是效力比较高,当然也可以用Unity自带的功能制作;然后国旗也是慢视,不是用UI做的;另外还有光环、拖尾圣衣、箭头。这些组件的加载压力都非常大,我们在优化这块,主要用到的是分帧处理。
分帧处理大家应该多少都了解,就是把一堆大量的计算全部拆开来,分散到到各个帧里面,这样的话帧数就能很平稳,不会出现一个大的CPU曲线的波峰。每个球依次进行加载,一个球加载完了,再加载另一个球,这样的好处,有可能球2还没有加载完,就已经被吃掉了。这样有一个好处,通过省去这些不必要的加载项,我们能减少大量的计算和加载。而且玩家看到的画面也是比较流畅的,不会出现突然画面不动,然后又开始动了的情况。
优化后,我们发现帧数是上来了,但是还是有很大的波动,这块主要原因就是大视野大量球的刷新卡顿。
因为我们的游戏视野是不固定的,不像其他游戏视野一直固定的,在同一屏里面看的东西基本不会高的太多,我们游戏就不同了。有可能一个屏幕里面,可能看到成百上千个元素还有物体。而且玩家在玩的过程中,如果玩得好的话,视野是来回变动的,服务器发数据的话,因为只发我们看到的数据,所以我们玩家在来回切换的情况下,服务器发的数据量也是很多的,包含大量球的添加删除数据。这样的操作对客户端的压力也非常大,所以客户端就需要做一些计算和处理,这里就很考验一个场景管理的框架。
然后这块我们主要是有两个策略,就是双缓冲列表还有分帧添加和删除。分帧这块在组件加载那块已经讲过了,逻辑差不多。双缓冲列表简单来说,就是两种列表。第一个就是添加列表,第二个就是删除列表,有了这个列表,在实际加载球的过程中,我们可以有列表作为缓冲。这个好处就是有了缓冲,如果有重复的球,我们就不需要重复地进行加载,可以在这个列表里面进行合并或者去除。
比如说当一个新加载球要过来的时候,我们如果判断这个,或者判断这个场景里面有这个球,或者这个列表里面有这个球,我们就可以不用处理。如果一个删除的球进来的话,我们判断这个删除列表里面已经有了这个球,我们也可以不用处理。或者就算没有,可以看到如果添加列表里面有,我们可以把添加列表里面要删除的球去掉,这样我们就能省掉很大量的工作。
这块的话,又引出了一个问题,因为列表是用List,如果要找这个列表很长,可能有几百个元素,用List的话,因为要一个个辨别,效率非常低,我们就把List和字典做了一个结合,既查找得快,也能删除得快。
分帧添加删除这块,跟组件加载不一样的地方每一帧加载或者删除的元素是动态变化的,可以根据手机帧率来调整。如果手机帧率特别高,可以每帧处理的元素就很多,如果帧率低了,我们就可以动态地减少,这样可以控制手机帧率来回波动的情况。
看一下优化的效果,大概是这样的效果,优化后整个波动就比较平缓,大家可以看到,上面有一些小波峰,就是一些处理的结果,但是很难出现那种大的波动,这是一个理想的情况。其实玩的过程中,还是会出现波动情况。因为美术资源那块有一个东西如果非常大的话,也会造成一定的影响。这块美术资源后期我们会做一个低端机型的美术资源包来替换,这样可以保证在低端机上能流畅玩下去。
第三个问题,LBS真实地图的显示。
年初的时候,我们打算做一个BS的玩法,因为看到之前有《Pokemon GO》结合了地图和游戏的玩法。在没研究之前,我们觉得挺简单,就把地图显示进来,后来做的时候发现,其实非常难,难点主要在于你怎么把地图显示进来。
因为我们不是专门做地图的,我们没有数据,就算有了数据,我们不知道怎么来画出来,显示出来。当时想到的一个比较完美的方案就是让地图加载到Unity里面,然后我们可以在这之上,可以做一些复杂的UI、放一些模型,这是我们的目标。
实际执行的过程中发现,其实没有现成的方案,参考国内陆图的SDK,发现没有专门给Unity做的SDK,大多数SDK的主要效果就是Unity的渲染跟SDK的渲染,它们是相互独立的。你想看地图得切到SDK那个上面去,这样的话Unity的一些UI就看不见了。如果你要做一些UI操作的话,就必须在底层,SDK那块再写一套,再写一套是可以,因为市面上有一些游戏是这样做过,但是有一个问题,你这样做出来的UI,只能做的非常简单,很难做一些复杂操作。
比如说你在游戏里面其他界面,因为玩家有可能会查看其他网页的信息,这个信息界面又要在游戏里面做一遍。我们玩家的个人主页其实已经非常复杂了,里面有非常多的东西,如果重新做一遍,一个是难度比较高,在底层做不好实现,第二个就是时间的话要求也非常紧,可能也做不到。
当时看到《Pokemon GO》,他也是能在Unity里面做的,我们就很好奇他怎么做的,发现他们跟谷歌有深入合作,就是谷歌专门给他做了一套东西,让他在Unity里面显示出来。这块我们询问了大量的SDK的工作人员,好消息就是,我们找到了高德,跟高德深度地合作,我们给他提供Unity的一些技术,他们提供SDK的技术,通过Unity底层接口,把我们Unity那边的图像传到SDK,这样SDK就不用渲染在页面上,就渲染在图片上,我们就实现了可以看到地图的效果。而且因为是SDK本身在画,所以效率非常高的。
我们实现了一个浏览器的功能,就可以把地图按照一块一块的图片来加载,来显示在Unity里面,这是一个折衷。当时这种方案,有两个问题,第一个问题就是流量比较大,而且有可能某一块地图加载不当,就显示不是特别好,第二个问题是因为用了真正的图片,斜着看,那些字都是斜的,很难有立体的感觉。幸好最后做成这样了一个效果,这样玩法的话,玩家也是非常喜欢的。
好的,我的分享就到这里。
联系客服