MMORPG战斗系统随笔(四)、优化客户端游戏性能

   转载请标明出处http://www.cnblogs.com/zblade/

        说到游戏性能,这是一个永恒的话题。在游戏开发的过程中,性能问题一直是我们研发需要关注的一个节点。当然,说句客观话,很多程序员在写代码的时候,是不会过多的在意其代码的性能的。其实这是一种很危险的思想,一个不断进阶的程序员,需要持续的关注自己的代码质量,同时需要借助工具来反查自己编写的代码质量。很多初阶的程序员,在编写代码的时候,是单纯的以完成任务为目标的,其实在写完后,可以没事翻看以前自己的代码,就会有很多不一样的视角。比如,代码的整洁性是否注意,可读性是否有注意,注释是否有相应的标写,这是一个基本的代码规范。

  除了基本代码规范,进一步的需要考虑的是代码的质量,也就是性能是否有消耗,是否写出一些消耗较大的逻辑。对于性能的优化,就需要相应的性能工具来进行计算和统计,不然所谓的性能优化都属于自我安慰,没有数据支持。

  下面我回忆总结一下自己参与的一些性能优化工作,也算一个个人总结吧。

一、对lua导出表的优化

  这是我最早而且持续时间最长的一个优化工作。在我们的游戏中,策划会有大量的配置表,全都配置在excel表中,通过编写导表工具,可以将excel表导出为对应的lua表,用于在游戏中进行lua表的加载和查找。我总结一下优化的几个节点:

        1、最初版本的导表

        最初版本的lua导表,是将excel中对应的key-value值逐一导出,其基本的格式可以表示为:

local skill = {
   {Id = 1, CD = 5, SkillTarget =1,...}    
   {Id = 2, CD = 3, SkillTarget =2,...}    
   {Id = 3, CD = 6, SkillTarget =1,...}    
}

  这种通过在table中插入多个table的方式,每个子table为hash的存储方式。仔细分析一下存储的类型,对于table而言,其基本的存储方式分为hash和array两种方式。hash的存储会有较快的读取操作,但是会带来更多的内存占用,因为其需要申请内存来存储key值,不单单只存储value值。对于array的存储方式,lua在底层的实现的时候,并不是我们常见的采用2的幂次的大小来存储,也就是,不是采用2,4,8,16,32...这种大小的存储,而是实际的根据数组的大小来分配对应的大小数组。这是后面一次优化中,通过实现设置对应大小的数组来分配内存,发现对比并没有对应的优化,后来查看源码才发现其数组的分配规则。

       最初版本的所有lua导出表都采用hash的存储方式,那么可以想象有多大的内存占用~大概在50M左右~这是一个非常可怕的内存占用,如果在游戏加载的过程中,需要加载这么大的配置表,那么游戏加载会有多缓慢,可以体会一下 :D

      2、初次优化的导表

       其实,最开始引起我们关注的并不是其存储方式,而是策划配置的某些excel表过于巨大,某些单一的表就会大到几M的数量级,分析其中的数据,很多数据并不是反复分散变化的,而是集中在较多的几个高频中,这就引申出一种优化的策略:提取高频的配置作为默认配置,少数低频的配置采用对应的配置,这样就可以得到对应的一个内存优化。具体的优化策略可以详见我的这篇文章:table重构index方法优化内存http://www.cnblogs.com/zblade/

       这篇文章的基本思想,就是通过重构和提前高频的方法,缓存高频,每次查找的时候,默认去高频中查找,如果没有,则走自身的查找。可以查看文章中的对比存储方法,写的比较详细。

    采用这种优化方法后,整体的lua导出表得到大大的优化,整体缩减了接近20M左右,可见我们的策划有多喜欢配置同样的配置 Orz

      3、进一步的优化导表

    通过上面的一次优化后,我们大大的优化了游戏的lua表内存占用,整体游戏在加载的时候,lua表统计的内存占用在30M左右。如果我们只满足于这一点,那么就不会有下一步的优化了。后续在第二次的优化上,我还进行了一些特定的优化,但是都没有太过于亮眼的优化,包括前面提到的用array的方式代替hash存储,也研究过设置array的大小,不采用2的幂次大小分配内存,事实的统计显示其实lua本身的内存分配就是采用 按需分配的,不会过多的分配内存。

    最好的优化方法,就是多和别人交流,这是我对自己优化的一个另类总结吧。在上一次的优化后,都没有太大的性能提升,但是这部分的内存占用又一直处于一个比较大的部分,后来在和其他项目组交流的时候,提供了一种静态加载的实现思路。对于lua导出表,可以用一种静态分表加载的方式。特别是对于占用内存比较大的一些表(具体每个表的占用可以做一个内存统计排序),可以分开成多份,这样在最初的游戏加载的时候,加载的是头文件部分,这部分是不包含具体的配置表信息的,具体的配置表信息存放在分表文件夹中。在实际使用的时候,比如将技能表分为part1-20,每个分表大小100,定位要获取id为1001的技能的配置,这时候去加载其对应的分表part11, 加载进来后定位取到1001的技能配置,这样就不会多余的加载part12-20这部分的数据表。

     通过静态分表加载的方式,游戏在最初加载lua表的时候,对于分表实现的lua表大大降低了游戏的内存占用,效率提升非常明显,内存占用缩小到20M左右=。=

       基于这样的实现方式,进一步分析,其实这些lua表并不需要在游戏启动的时候加载,只需要在每次第一次使用相关表的时候加载,再加载进来,同时对于大的内存表进行分块加载,综合这样的实现方式,对lua表的优化最终达到一个可以接受的地步。

二、3D模型的优化

  在3D游戏中,美术在3DMAX中做完游戏模型的相关建模和动画后,会导入到unity中作为游戏资源。通常,这样的美术资源是含有冗余的资源,可以在unity中对这些冗余的资源进行优化,降低美术资源的内存占用。这儿我例举两种美术资源相关的优化:模型UV的优化和动画animation冗余节点的优化。

  1、模型UV的优化

   一半导入unity的角色模型,都含有冗余的多套UV,对于这些对于的UV,我们是可以用一定的方法来实现剔除的。由于u3d中模型的类型为FBX类型,所遇需要采用FBX SDK,结合FBX SDK的API进行UV的剔除。这儿给出一个FBX SDK的官网:FBX SDK官网

       FBX SDK的官网给出了两种实现方法:C++和python,对应的都有详细的API讲解和例子。我主要采用python的方法,搭建基本的pyhton环境后,就可以查看相关的API接口,从而实现对应的代码编写。具体代码由于保密我就不提供了,我可以给出相关的实现思路:首先是查找到指定文件夹下的所有FBX文件,然后对于这些FBX文件进行读取,获取其下面的所有节点,对于每一个节点,读取其属性,如果属性包含UV,则读取UV的数量,如果数量大于1,则说明有多余的UV,则将其移除,然后保存

  通过移除多余的UV,可以实现对FBX文件的一次"瘦身",从而降低美术资源对游戏内存的占用。如果有什么需要交流的,可以在下面留言进一步讨论。

      2、动画animation冗余节点的优化

       模型的动画,在美术制作完后,会被导入到unity中,进而可以在animation窗口中查看。其实我们分析模型的animation,我们可以发现,大部分节点的animation在插值过程中,其实质是不会变换的,基本为1。由于每个节点的animation主要修改 rotation/scale/position这三个transform属性,所以对于rotation和scale可以进行一次剔除:如果整体animation的插值都为1,则这样的一条animationCurve是可以剔除的。不知道有没有读者也有相关的优化策略,可以在下面留言进一步的讨论。

  我就说说我用FBX SDK优化animation冗余节点的思路:首先获取到FBX模型,然后获取到对应的FbxAnimStack,也就是animationClip,获取到animationClip后,进一步获取到animationLayer,基于animationLayer,可以获取到每个animationLayer的animationChannels,也就是rotation/scale对应的xyz三个通道,分析这三个通道的通道值,如果都在1的误差允许范围附近,则可以移除这个rotation/scale。

      最后存储修改后的FBX文件,可以发现以前较多的animation经过优化后,都被剔除过滤掉了,这说明很多时候我们的animation其实只是在修改position这一个属性,并不会修改rotation/scale这2个属性,这是可以被优化的。

三、头顶文字描边的优化

  熟悉MMORPG游戏的都知道,在游戏中角色头顶都会有名字,称号等相关的文字信息,实时的反应玩家的信息,便于游戏的交互。在MMO游戏中,会有很多的玩家同时在场,同时也会有较多的怪物NPC等游戏角色的存在,而为了追求游戏体验,都会对玩家头顶的文字进行描边,总结一下对头顶文字描边的优化过程:

  1、4个基准点的描边

  最开始提出要添加头顶文字的描边后,和leader商量了一下,就只是单纯的对玩家头顶文字进行四个点的描边。这里面可能会说一些渲染相关的知识,通常我们在进行渲染描边的时候,我们最开始是需要填入需要渲染的UV三角形的,如果有不是很了解的同学,可以搜查一下渲染的过程,填充完渲染三角形后,CPU才会把这些三角形传递到GPU中,设置渲染状态,从而进行三角形的渲染,得到最后的渲染结果。所以描边要么在CPU中进行,要么在GPU中进行,我选择的是在CPU中进行,在填充三角形的UV的时候,选择扩展这些UV数组。

  基本的实现思想是:原始的UV数组一份,然后分别在左上,右上,左下,右下的四个基准点进行拓展,这样就会得到五个数组,最终描绘出来一个描绘了外轮廓的描边结果,大概结果示意如下图:

                                                           

  2、四方向的描边

  基于四个基准点的UV扩展,是可以实现基本的描边效果,但是仔细推敲就会发现,这其实是对文字的外轮廓进行一个拓展而已,获得的效果其实并不是非常好,如果对描边效果有一定要求,这样的描边并不是很优秀的。果不其然,高层要求优化描边效果。我仔细推想了一下,就把四个基准点的描边变为四个方向的描边,分别为向左,向右,向上,向下的UV扩展,如果用一个文字来表示效果,可以表示为:

                                                                                                              

  读到这儿,我想你也可以自己在头脑中描绘出这样的5份UV,一份为原始的UV,一份为向左偏移一定offset的UV,一份为向右偏移一定offset的UV,一份为向上偏移一定offset的UV,一份为向下偏移一定offset的UV,这5份UV在进过三角形设置后,会传递到GPU中进行三角形描绘,得到的结果就会是比较理想的描边结果,分别描绘了文字的上下左右四个方向。

  这种描边方式有很好的描边效果,带来的消耗,也是可以直接理解的,通过四份UV的复制,而不是简单的四个点的复制,对于内存的占用会更大。可见想要有好的效果,还是需要一定的付出。最后这种描边也被优化掉了,最后采用的是shadow来代替描边,通过动态合批的处理来降低内存占用。不过从这次描边的优化,可以更了解整个渲染的过程,CPU是如何计算UV的,怎么设置UV,最后在GPU上渲染得到对应的效果,都是一个比较直观的过程。

 四、代码的优化

   写到这儿,也提一下对代码质量的优化吧。现在比较主流的热更都会采用lua来实现逻辑,lua这种脚本语言,上手极其容易,但是如果使用不是很仔细,还是会带来一些不必要的问题。table作为Lua的基本构造点,在使用的时候,会有一些可以规避的地方。比如在频繁更新或者使用的代码部分,不要反复申请table,这会使得虚拟机不断的去进行内存分配。我们可以将这些频繁使用的功能相同的table作为一个内部变量存储,在第一次进行申请,当前更新后,将其内部的值赋值为nil,这样下次再使用的时候,是在当前table的基础上进行扩展,这样采取扩展table而不是频繁构建新table的方式,可以避免内存碎片的产生。

  此外在检测代码质量的时候,最好做好相关的工具,进行各种性能统计,我们才能得到实际的性能数据,通过性能数据的分析修改可能存在的问题点。比如对于高频变化的数据,是否采用增删改的方式更能提升性能,对于高频检测的方法,是否通过特殊的规则可以降低检测的次数等等,这些都需要结合实际的应用设计来修改。

  当然,游戏还包含一部分的优化,就是UI部分的优化,这部分可以参考MMO雨松的博客,他主要负责这部分的工作,所以可以参考他的博客,不知道最近他有没有更新博客,哈哈,估计太累了~

       好了,本文也算一个小结,后续我看还有什么需要继续写的,我会再接着写博客,下篇文章见~

posted @ 2017-10-11 10:37  zblade  阅读(3999)  评论(5编辑  收藏  举报