Cocos2dx中Lua游戏性能优化指南

渲染优化

合并图集(Atlas)

离散纹理资源会打断引擎的渲染合批优化,建议将常用资源合并成2048*20481024*20481024*1024等尺寸的图集(atlas)。

合并原则: 小图,如尺寸在256*256以内的纹理。 图标,如人物头像,资源(gold, mine,stone, etc.)图标,Button背景,九宫资源等。 公共,将在各个模块常用的纹理资源放一张公共纹理中,在整个游戏生命期内常驻内存。 玩法,将不同玩法模块的资源单独合并到相应的图集,不同玩法模块中的资源很少共用,以玩法模块为单位进行合并图集,即能达到自动合批的效果,又不至于占用太多内存资源。

消除透明结点

透明结点是指getOpacity() == 0的结点,透明节点会增加drawcall的数量同时增加GPU的渲染负担(注:GPU并不会主动丢弃全透明的像素而不参与blend操作),有两种常用的方式来产生透明结点: 1. setOpacity(0),主动设置为透明。 2. 通过cc.Action由引擎设置为透明。

消除透明结点的方法目前需要因地制宜,线上引擎无法自动处理,游戏开发同学可以使用Snapdragon ProfilerNsight来分析当前场景内的透明物体。一般可用的优化方法有如下:

  • setOpacity(0)的地方使用setVisible(false)替代。有部分需求可能是在隐藏node后,同时需要接受点击事件,这样可以使用不参与渲染的node代替,如cc.Node和cc.Layer。
  • Action导致的透明node,一般可能是使用了FadeOut/FadeTo,此时可以将FadeOut/FadeTo封装到一个Sequence中,在action结束后,处理同上,例如:
local fadeout = cc.FadeOut:create(5)
    local callfunc= cc.CallFunc:create(function()
        -- 全透明后隐藏label
        label:setVisible(false) 
    end)
    local seq     = cc.Sequence:create(fadeout, callfunc);
    label:runAction(seq);

调整Node节点的加载/addChild顺序

目前cocos2d-x采用临近合并drawcall的策略,即只合并相临可合并的节点,影响合批的因素有: 相邻两个Node使用Shader不同。 采用的Shader使用了自定义uniform或MVP矩阵。 相邻两个Node使用了不同的纹理。 blend方式,不同的BlendFunc不能合并渲染。 * ClippingNode、ScrollView和Layer等不参与合并。

因此可以在不改变布局效果的情况下手动调整addChild的顺序,如果是使用CocosStudio制作的,也可以在编辑器中调整。

如上图所示,这是原有的ABCD的渲染的渲染方式,由于纹理的原因A/B/C/D并不能合并批次进行渲染。但是如果在效果上存在这样的关系,B和C两者在视觉上没有先后关系,即C不会覆盖到B,则可以手动调整节点的创建顺序,如下图:

则这样可以做到AC被合并,BD也可以被合并,即从最初的4个drawcall降低到2个drawcall。

注意,如果能将Tex1和Tex2合并到一张纹理上,则ABCD可以被合并到一个drawcall,这是最理想的情况。

纹理优化

缩小纹理尺寸

在不明显影响视觉效果情况下,缩小纹理尺寸可以达到如下效果,注意,这里说的不是atlas图集的大小

  1. 减少包体,对小游戏来说,这是一个可重要的衡量标准。
  2. 减少加载时间,场景切换,UI加载等速度变快。
  3. 减少内存占用,在低端机上,内存很重要,特别是hago的兼容机型中会有一些内存很小的机器。
  4. 降低GPU带宽,带宽对手机GPU来说是稀缺资源,一般也是GPU耗电的元凶,小纹理(atla)具有更好的缓存局部性。

JPG陷阱

开发/美术同学往往会发现,将某些不透明的png大图转换成jpg格式后,文件大小骤降。此时需要注意,由于硬件并不支持jpg格式,与png一样,需要转换到RGB888的格式。这样在加载到游戏内存后,并没有降低内存的占用,并且在有些机器上,jpg的解码速度很慢,这需要开发同学多测试,大包体和解码速度上进行权衡。

具体的jpg解码速度,1680x1050尺寸纹理:

备注:由于Hago的cocos2d-x引擎不支持在android上使用jpg格式,因此也就不存在这个问题,统一使用png格式就好了。

压缩纹理

已制作好的资源,也可以在不改变纹理尺寸的情况下,使用压缩纹理来减少纹理的开销,但效果需要美术同学确认:

  • 安卓上可以使用ETC1纹理,由于ETC1不支持alpha,因此需要使用单独的纹理来做为alpha。
  • IOS上可以使用PVR,但PVR要求尺寸是POT(pow of two) ,使用并不灵活,但适合atlas图集(一般都是POT的)。

如有需要ETC1的方案,请联系[xieyukun@yy.com]

字体优化

使用自定义字体文件 FNT

适用于有限的字体的情况,比如,纯英文字符a-zA-Z0-9的情况,使用自定义字体往往可以达到最优的效果(这里不考虑包体大小的情况)。使用FNT能有助于合并渲染。

使用TTF字体

TTF字体的效果与FNT字体相当,但TTF字体库一般会比较大,包含的字符比较全,但往往需要针对不同国家进行单独处理。

在没有字体效果(如outline, shadow)的情况下,TTF字体也可以自动合并drawcall。

(少用)系统字体

如果可以,应当尽量减少系统字体的使用,每个不含相同内容的label或text,都会创建一个新的纹理,如果游戏中有频繁的字体变动,如时间,分数,聊天等都会创建大量的字体纹理,且不具有可复用性。

但系统字体的使用可以减少包体大小,减少维护成本,因此需权衡利弊。

减少字体效果的使用

字体效果的使用,往往会带来更多的drawcall,同时也会打断合并批次,如:

  1. outline, 两个drawcall来完成一次的文本渲染。
  2. shadow, 两个drawcall来完成一次的文本渲染。
  3. glow, 仅需要一次drawcall。
  4. shadow + outline, 三次drawcall。

序列帧动画

序列帧动画的优点是简单实用,美术制作也比较简单。

但是过度的序列帧动画会带来至少如下几个比较大的问题: 1. 包体变大。 2. 游戏占用内存变大。 3. 占用的GPU带宽变大。 4. 资源加载变慢。 5. 手机耗电增加。

因此需要适当控制序列帧动画的量,最好仅用于帧数较少和纹理较小的动画。对于具有复杂动作的角色之类使用骨骼动画来代替,如SpineAnimation,可以显著减少纹理的数量。

减少Spine动画的骨骼数量及顶点数量

大量的Spine动画会导致严重的游戏性能问题,这是由于每帧都要进行大量的骨骼更新和顶点蒙皮计算,这(位移,旋转,缩放)包含了大量复杂的数学运算(乘法)。

如果需要创建大量只具有少量动作的角色(怪物),可能使用序列帧动画,以适量的空间换取宝贵的计算时间。

适当的LOD(Level Of Detail),美术同学输出多套资源,根据机器性能来选择不同的资源。

优先使用Cocos2d-x的原生动画系统

Cocos2d-x引擎自身提供的相对比较简单的动画系统,如位移,缩放,旋转,变色,序列帧等,其优化如下:

  1. 简单,cocostudio下可视化编辑,所见即所得。
  2. 良好的兼容性,基本不会出现像spine的各种问题。
  3. 可控性好,可以对其节点进行各种操作,如替换纹理等。

九宫格纹理资源

九宫格纹理的目的是为了减少纹理大小,具体原理,可以网上查找相关资源。但是错误的九宫使用往往会起到相反的作用,美术同学(往往)可能不能正确判断是否需要使用九宫格,这时就有可能将一个非九宫的资源错误用成九宫格,这样会带来顶点个数从4个变成16个,三角形个数从二个变成18。如下图所示的两个Image,此些不需要九宫格。

少用Clipping操作

事实证明,ClippingNode/ClippingRect会引起严重的性能下降,这是因为每次Clipping操作都需要clear、读、写stencil,同时也会打断正常的drawcall合并操作。

如下几种情况的可替换方案:

  1. 如果只是静态裁剪纹理,如将方形图显示成圆形:直接提供相关的纹理,或使用RenderTarget/RenderTexture生成圆形纹理,后续直接使用RT。
  2. 如果对复杂节点进行clipping,但结点变化并不频繁:可以使用RenderTexture来生成一次,后续直接使用RT来显示即可。

逻辑优化

减少Lua函数调用

对于Lua脚本语言而言,语言本身并不会对简单函数进行内联操作。因此对于频繁调用的小函数,可以考虑在开发后期手动进行展开,以减少函数调用的开销。如人物某些属性的getter方法。

通过缓存一些不变量,也可以达到减少函数调用的目的。

数学运算的优化

在CPU的执行中,除法和乘法会消耗更多的CPU周期,而加/减往往只需要一个时钟周期。可以在某些情况下牺牲一定的精度,可以换来更多的效率提升。例如,在一个自动捡宝石的玩法中,在主角靠近宝石时自动捡起宝石,无需玩家操作:

通常的做法是根据玩家与宝石的距离进行判断,其逻辑如下:

if (sqrt((px - sx)**2 + (py - sy)**2) < MIN_DISTANCE){
        // TODO
    }

这里用到了两次的指数运算和一次开方运算,在CPU中这些都是很费的操作,往往需要比乘除法更多的CPU时钟周期。一种简化的方式是:

xx = (px - sx) * (px - sx);
    yy = (py - sy) * (py - sy);

    if (xx + yy < MIN_DISTANCE_2){
        // TODO
    }

注意MIN_DISTANCE_2MIN_DISTANCE的平方,作为一个常量,整个游戏期间只需要计算一次。 这是对第一种写法优化,消除了指数运算和开方运算,截止目前为止,所有的判断依然是精确的,但依然有两次乘法。如果对精度要求不高,可以允许一定误差的情况下,可以使用曼哈顿距离来做进一步的优化:

xx = abs(px - sx);
    yy = abs(py - sy);

    if (xx + yy < MIN_DISTANCE){
        // TODO
    }

这只是一种常见的特殊情况,并不适用于很多场景,而减少复杂的数学运算对手游而言确实很重要,例如网上有一些公开的快速开方,快速求距离等优化算法。

逻辑分块

继续回到刚刚提到的捡宝石的case,由于需要判断人物和宝石的距离来判断是否可以捡起,如果场景中有很多宝石,哪些宝石可以被捡起呢?当人物移动时遍历所有的宝石,这当然可以。但如果宝石非常多,几百,上千呢?每次让脚本loop这么多次,可能游戏会很卡,怎么办?

试试将场景划分成不同的逻辑block吧(block的尺寸大于可捡起宝石的最小距离)。每个block包含了该区域内的宝石索引,当人物移动时,只需要判断玩家周围最多9个block即可。

逻辑分块的功能当然不仅仅可以处理这种需求: 1. 延迟加载,超出屏幕或距离玩家太远的block可以延后加载。 2. 及时销毁,距离玩家太远的block也可以从场景树中移除,减轻内存压力和引擎/脚本更新压力。

延迟加载/异步回调

场景加载或某些玩法界面有时需要加载大量的Node和纹理资源,从而导致进行场景和打开界面比较缓慢(卡顿),这时可以使用延迟加载和异步回调来解决这些问题,以下有一些启发性的case:

  • 显示前100(更多)名的玩家积分排行榜,如果在打开界面是一次创建如此多的item,打开界面一定会很卡。解决方法可以只创建10个或更少item,当划动列表时复用这些item来动态更新数据,也可以在划动时动态创建(或clone)相应的item。
  • 在会显示很多玩家头像的界面中,可以使用纹理的异步加载,当纹理加载完成后,通过回调来创建相关的头像图标。这会有一种显示问题,就是界面打开后,会有图标陆续出来。同时也是增加逻辑的复杂度,如在资源加载完成之后,界面已关闭等异常情况。在使用之前,需要与策划/产品沟通,权衡。
  • 在场景较大的玩法中,可以优先加载玩家周围的场景,将场景显示给玩家后,继续加载距离较远的场景。如果玩家只能慢慢走过去,这就留出了足够的时间来加载远处场景,但如果玩家可以瞬间移动到远处,这个策略就不再适用,或需要更多优化。
  • 分帧加载,这也是延迟加载的一种具体实现方式,比如将100条item,按每帧创建5个,则可以在20帧内创建完毕,同时也不会让玩家觉得界面加载太慢。

减少每帧要做的逻辑

一般游戏是以60FPS或30FPS来运行的,如果有大量的每帧都是执行的逻辑,势必给客户端造成较大的性能压力。可以考虑将每帧的逻辑以定时器的形式扩大执行间隔(如0.1s一次),如AI,大部分的AI可能并不需要每帧去计算。

有些需求可能需要实时检查数据的变动,与其每帧去检查更新,使用事件机制往往会更具有效率和减少耦合,参考观察者模式

缓存

缓存是提升游戏运行性能的一把利刃:

  • 资源缓存:减轻IO压力,同时也能减轻资源加载时的卡顿现象。
  • Node池: 各种Node的创建都是复杂,需要设置很多状态,有时也需要分配很多内存和显存。
  • 脚本对象池:有些脚本对象的创建也是比较费时,如比较大的表及其初始化。

注意,缓存同样是一把双刃剑,不加节制的使用,可能会耗尽内存和显存,因此也需要一定的策略和废弃某些缓存项,如使用LRU机制。

性能/设备分级

手机设备千差万别,不可能使用相同的逻辑来满足所有人的需求。即不能一味满足低端机玩家,而忽略高端机玩家都美和细节的追求,也不能只满足高端机玩家,而罔顾低端机玩家能玩这一最基本的需求。一个优秀的游戏会最大程度的照顾各类玩家的需求。

设备性能分级可以由不同的手段进行实现: 1. 设备列表的形式,这种方式最为准确,但工作量巨大,需要QA同学测试各种不同机型,给出详细分类列表,并且后续维护成本极高,时刻都有新设备面市。 2. 根据设备的主要指标进行衡量,如CPU型号,内存等,这种方式灵活,但不精确,在执行中往往最具可执行性,毕竟放眼世界,CPU/内存制造商就那么几家。 3. 1和2两种方式进行结合,甚至可以基于线上游戏的性能指数反馈来自动维护设备列表。

针对不同的等级(极致/高/中/低)来呈现不同的内容,俗称LOD (level of detail,细节等级),如少加载一些装饰性的元素,MMO游戏中少加载一些玩家等,当然,这些缺少的部分不应该影响到玩法本身。

动态帧率

帧率往往决定的了游戏的整体耗电程度,如在满帧的情况下60帧一定会比30帧耗电更快,如果游戏并不很追求丝滑的情况,30帧足以满足大多数人的需求。如果产品/策划同学一定要求60帧,那么可以考虑一些策略能在手机性能吃紧的情况下,将帧率动态调整到30帧。如果60FPS的游戏,在某些手机上只能到50帧以下,就可以这样做,因为这种情况下,已表明手机不堪重负,长此以往,系统会主动降频,致使更加雪上加霜。

在玩家被动挂机的情况下,也可以考虑降低帧率来节省电量。

Profiling

所有的优化应当建立在有理有据的基础上,2-8原则往往是一个好的参考,并且游戏中的热点往往不会太多,顺手的profiling工具往往能起到事半功倍的功效。在此推荐几款好的profiling工具:

  • 手动打桩:原始但有效的手段,对疑似热点的逻辑进行打点测试,统计执行时长。
  • Nsight:Nvidia出品,windows平台的渲染相关的profiling神器,要求独立N显卡,最好GTX 1050+吧。
  • Snapdragon Profiler: 高通出品,仅支持高通平台,要求820之后的CPU,可以针对安卓手机平台进行Profiling。
  • adb shell top: 可以直接查看安卓的进程运行情况,cpu, memory等:
Tasks: 732 total,   3 running, 727 sleeping,   0 stopped,   1 zombie                                                    
Mem:   5772888k total,  5588992k used,   183896k free,   127860k buffers                                                
Swap:  2621436k total,        0k used,  2621436k free,  2539616k cached                                                 
800%cpu  77%user   4%nice  38%sys 664%idle   0%iow   6%irq  10%sirq   0%host                                            
  PID USER         PR  NI VIRT  RES  SHR S[%CPU] %MEM     TIME+ ARGS                                                    
 3987 u0_a151      10 -10 1.8G 147M  75M S 62.3   2.6   0:29.63 com.yy.runtime
  • adb shell dumpsys: dumpsys meminfo com.yy.runtime
polaris:/ $ dumpsys meminfo com.yy.runtime
Applications Memory Usage (in Kilobytes):
Uptime: 170706 Realtime: 170706

** MEMINFO in pid 3987 [com.yy.runtime] **
                   Pss  Private  Private  SwapPss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap    36976    35360        0        0    56448    49602     6845
  Dalvik Heap     1357     1188        0        0     2589     1053     1536
 Dalvik Other      910      904        0        0                           
        Stack       60       60        0        0                           
       Ashmem        4        0        0        0                           
      Gfx dev    19032    18864      168        0                           
    Other dev       21        0       20        0                           
     .so mmap    17660      676    14496        0                           
    .jar mmap        1        0        0        0                           
    .apk mmap      277        0        8        0                           
    .ttf mmap      175        0       96        0                           
    .dex mmap     6345        4     3528        0                           
    .oat mmap      711        0       36        0                           
    .art mmap     5694     3944       52        0                           
   Other mmap       32        4        0        0                           
   EGL mtrack    21724    21724        0        0                           
    GL mtrack    16356    16356        0        0                           
      Unknown    29488    16972    12472        0                           
        TOTAL   156823   116056    30876        0    59037    50655     8381
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值