移动端游戏性能优化——ARPG项目的优化经验

上篇文章移动端游戏性能优化--渲染管线中从渲染管线方面讲述移动游戏的性能分析、瓶颈定位和优化方向。这篇文章总结下笔者全程支持两款游戏中做的性能优化方案。需要注意对某种游戏中有效的优化方案,并不适用于另一种类型的游戏,同样的笔者采用的一些优化方案不一定适用于别的项目,要具体问题具体分析。

从玩家下载游戏、游玩游戏的过程可能会碰到一系列性能问题,即包体、下载、帧率、卡顿、内存、发热等,文章也按照这个顺序行文。由于笔者的工作跟服务器基本不打交道,所以本篇都是客户端优化的内容,由于篇幅原因也不介绍具体的实现细节。

包体优化#

包体过大会影响发行买量的转化率,过大的包体可能使玩家在没有下载完就流失或直接不下载。为了优化包体,我们分析包中每种类型资源的占比,碰到的问题有:

  • 资源冗余
  • 图片容量占比过高,占包体的一半多
  • 三方库的尺寸过大
  • 某些蒙皮、动画过大

经过分析测试后,我们制定了优化策略:

  • 完成冗余资源检查脚本,剔除没有被用到的资源
  • 与美术配合,在不影响美术效果的前提下减小图片的尺寸。最明显的是烘焙贴图和光照贴图内存占用缩减为之前的1/4
  • 删除用不到的三方库,有些三方库由于是共用库的原因导致过大,无法推动优化,比如字节的游戏SDK,约60M左右,悲伤。
  • 在分析的过程中资源包体占用的时候,将同种类型的资源大小排序,会发现前20%甚至10%的资源会占所有同类型资源包体占用的80%,所以着重优化这10%~20%的资源,就能取得明显的优化效果。蒙皮、骨骼、动画都是这种方式。

下载优化#

现在的游戏由于内容量更多,画面更精美,包体必然越来越大,项目研发后期即便优化大小过后,整包大小超过2G已经是家常便饭了。所以势必要将包体分为首包和二包,甚至三包。

我们将包体分为首包、二包(三包效果不好,取消了),在多轮CBT(Close Beta Test)期间尝试了几种下载包体的策略。

  1. 首包满足第一天的游玩需求。在玩家游玩的时候,边玩边下二包。
  2. 进入游戏停留在下载界面下载二包,玩家需要等待下载完成后方可游戏。

对第一种方案游戏流程卡在在二包下载的地方会出现明显的流失,并且由于边玩边下,下载引起的发热很夸张,游玩体验也受影响,并且除了限速没有办法优化。所以我们尝试了第二种方案,并且测试数据比第一种好。

第二种方案是进入游戏后就告诉玩家要下载二包,无法游玩游戏。另外提供后台下载的功能,毕竟玩家不可能在下载界面等待下载完成。还要“安抚”玩家,告知玩家不要杀掉游戏,正在后台下载,经过多长时间会下载完成后可以游戏。

帧率优化#

帧率是客户端优化的重头戏,性能优化大部分的精力要花在对帧率的优化中。瓶颈一般都在CPU、GPU之间产生,笔者从瓶颈产生的位置来介绍各个优化点。

CPU优化#

项目逻辑#

项目程序是引擎的直接使用者,应该优先从排查项目代码逻辑的问题。在这个过程中,需要引擎提供方便的Profiler工具,以统计各部分的时间开销。

这部分笔者接触的不多,大概率有优化空间的地方有:

  • 状态机,控制状态机的更新频率
  • 寻路计算,控制寻路请求的频率,避免扎堆寻路

优化DrawCall#

CPU提交DrawCall有开销,GPU完成DrawCall命令也会有开销,总之就是把DrawCall控制的越少越好。

裁剪与剔除

一个场景包含了地表、物体、植被、特效、水等内容,如果直接把场景中的所有物体提交给GPU肯定是不可以接受的。所以首先要有加速裁剪和剔除的算法,保证有尽量少的DrawCall被提交给GPU。

  • 一般用四叉树/八叉树管理场景,或者根据项目的实际情况选择BSP、BVH等加速结构,根据节点与视锥的相交情况,过滤掉大量不在视锥内物体
  • 设定视距,距离摄像机超过一定距离的物体直接裁减掉,可能影响画质,一般在低端机使用。
  • 设置像素贡献的阈值,物体投影到屏幕上的包围盒面积如果小于阈值,就裁剪掉,可能影响画质,一般在低端机使用
  • 判断物体是否被视锥裁剪,与视锥的求相交可以加速,可以用SIMD指定加速运算
  • 遮挡剔除技术,引擎了使用Potencial Visibility Set(PVS)和Hirachical Z-Buffer Occlusion(HZBO)。

BattleField中指出,传统的树形裁剪有其缺陷,效率比较一般:

  • 树形加速结构具有太多的分支,无法保持较好的数据局部性
  • 无法大量并行

UE4也已经放弃了四叉树裁剪,而是利用CPU进行大量的并行计算视锥裁剪,取得了更好的性能。

RHI线程

CPU提交GPU命令会有时间消耗,可以开辟一个线程专门用于提交GPU命令。这样虽然游戏的更新和渲染会相差一帧,但提升了帧率。UE4的RHI线程就是这个作用。

合批与GPU Instancing

合批是指将相同渲染状态的物体打包到一起,用一个DrawCall完成绘制。

静态合批,是在打包时完成合批的操作,会增加包体。动态合批,是在运行时完成合批操作,由于会读写Buffer,会有一定的性能消耗。合批还有一个明显的缺陷,它生成了一个大的网格,丢失了原来的包围信息,影响裁剪和剔除,所以尽量只合并相近的物体,以保证裁剪和剔除的有效性。

GPU Instancing是通过一个DrawCall绘制若干个相同的对象,也要求渲染对象有相同的渲染状态,需要注意判断状态是否一致能否instancing的开销有可能超过instancing的收益。

动画#

  • 尽量减少关键帧的数据,我们美术是以24PFS为标准制作动画的。
  • 如果模型不可见,一般情况下不需要更新它的动画。但是需要注意处理这种情况,模型不可见,但是它具有大范围移动的动画,可能会被看到。
  • 尽量用线性插值,少用复杂的曲线插值,比如贝塞尔曲线

粒子特效#

我们的项目对特效依赖比较重,需要靠它们表达绚丽的技能效果和装备效果。

粒子系统非常容易出效果,也就会被美术重度使用。它的性能的开销比较大,主要体现在以下几方面:

  • 计算量,大量粒子要进行颜色、曲线的差值,非常耗费CPU的性能
  • 频繁操作内存。粒子的生命周期通常比较短,会有大量的粒子创建/删除,会造成内存的频繁读取和内存碎片
  • 每帧对buffer的更新,粒子的位置、颜色信息需要更新到GPU的buffer中,占用带宽
  • 增加大量DrawCall,粒子特效都是各式各样的,会导致无法合批
  • OverDraw,粒子特效大多数都是透明的,在粒子多的情况下,会造成严重的OverDraw

所以能在引擎层面能做优化不多,而是跟美术一起讨论制作规范,期待从美术制作上寻求优化。

  • 禁用粒子中的阴影、光照、物理碰撞等,因为粒子本身是频率高且明亮,很难看不来有这些效果
  • 控制粒子的发射器数量,因为一个发射器就意味着一个DrawCall
  • 控制发射的粒子数量,减少运算
  • 控制粒子的尺寸和贴图的尺寸,减少OverDraw和带宽

物理#

我们对物理的依赖不高,这部分没做什么优化。

  • 尽量用简单的碰撞体,如cube、sphere,不要用geometry作为碰撞体。
  • 尽量降低物理的模拟频率,当物体不可见时关闭它的物理模拟。

地表#

我们的地形系统每个地表块是一个drawCall,每个地表块里的格子数量表现地表的精细程度,对应着地表块的VertexBuffer,要从程序和美术两方面入手优化:

  • 地表支持LOD系统,在高LOD层级用更简单的渲染代替
  • 控制地表块和地表格子的数量,减少drawcall和带宽

UI#

UI跟粒子特效有一部分共同点,有很多半透明,并且对合批依赖比较严重。最重要的一个解决方案是合并生成图集。

在生成图集的时候有一些要点:

  • 被大量界面重复使用的元素,将他们放到共享图集中;只有两个界面同时用的某些元素,那么将这个元素分别拷贝到这两个界面的图集中。具体做法要根据元素的重用程度而定
  • 每个界面生成自己的图集,使界面内能更好的合批
  • 检查每个界面引用的图集个数,如果超过2个(自身和共用图集),那就是不好的图集

还需要注意在UI制作的时候,会有一些破坏合批的做法:

  • 界面中带有文本,且文本比较复杂,带有阴影、描边、发光等效果
  • UI节点制作混乱,不同图集的元素相互交叉
  • 使用UI特效

所以要跟美术一起制作标准和检查工具,从制作上提升性能

多线程#

将一些比较独立的任务拆分给子线程,避免出现一核工作,多核围观的情况,注意需要处理好线程间的同步操作。

  • 骨骼动画的更新
  • 粒子的更新
  • 场景物体与视锥的相交计算
  • RHI线程,上面提到过,CPU提交GPU命令,会有时间消耗,进而会造成CPU和GPU都会相互等待。独立出一个RHI线程负责提交GPU命令,可以将这部分时间优化掉。
  • 音频视频解码
  • 加密解密,加密解密通常比较复杂,需要耗费很多CPU算力,可以将它们交由子线程处理
  • 网络请求和资源的下载与上传,为创建专门的线程负责完成这些工作

其他#

还有一些个别非常有效的优化,不好分类,就汇总到这里:

  • 区分好开发版本、发行版本,因为开发版本有很多debug功能,会有不小的性能开销,在项目中性能数据采集就消耗了2~3ms
  • 选择合适的编译器和优化选项,之前把NDK-9升级为NDK-12,在小米6上降低了2ms的帧间隔

GPU优化#

这部分针对渲染时GPU负担较高的部分,并给出优化的思路。

合理组织渲染顺序#

GPU切换渲染状态是有开销的,切换RenderTarget、shader、ROP的操作比较高。

移动GPU的渲染架构也对渲染的顺序有一定的要求。下图是TBDR的渲染架构。

所以我们这样组织渲染顺序,以在移动GPU上获取更好的性能。

  • Depth-Pre Pass
  • Shadow
  • Alpha Test
  • 不透明
  • Alpha Blend
  • 后处理
  • UI

渲染路径#

我们引擎使用的渲染路径还是Forward Rendering,已经非常落后了。这种管线材质与光源深度绑定,注定有一些硬伤:

  • 性能与光源数量直接相关,为了更好的美术效果,必须要保证光源的数量
  • 相同材质的物体由于受的光源不同,导致无法合批或Instancing

所以需要更新渲染路径技术弥补Forward Rendering的缺陷,可以学习:

分辨率#

GPU渲染管线中,Pixel Shading、采样贴图的带宽、Alpha Blend等操作都与分辨率有关联,降低分辨率能有效地降低GPU的消耗。

现在的手机厂商很多都是一个超高分辨率的屏幕,但搭配的CPU/GPU性能跟不上;而有些GPU发热很严重,厂商散热做的不好,点名三星。如果用原始屏幕的分辨率,会碰到多种情形:GPU无法处理,帧率比较低;发热极其严重,玩不了几分钟手机就烫手了;发热后会触发降频操作,导致帧率下降严重。

所以降低分辨率是GPU优化中非常重要的一环,可根据设备等级分别降低到1080p、720p、480p。

Pixel Shader#

笔者没有遇到Vertex Shader的瓶颈,所以就只写Pixel Shader的优化方法。

Pixel Shader的开销与语法、光照计算、纹理采样几个方面有关系,由于减少shader计算,降低带宽是通用的优化方案,其他的优化方案已经包含了。所以在这里就只写shader语法相关的开销。

  • 合理提供shader语言支持的低精度类型,能加快计算
  • 尽量少使用消耗特别高的函数,如pow、exp、sin、log等

烘焙#

美术为了场景的表现效果,会加很多的光源。其中有很多光都是静态的,不会发生变化。如果将其放到Pixel Shading中计算光照会导致很大的性能开销。所以要将这些光产生的光照效果和阴影效果预计算到光照贴图和阴影贴图中,在渲染的时候只需要花费采样纹理的开销就可以渲染出同样的效果。

带宽#

带宽是GPU性能的一个重要的因素,不仅影响帧率,也跟发热量有关。所以要想办法尽可能的降低带宽。

  • 降低VertexBuffer和IndexBuffer的带宽

    • 优化模型面数
    • 顶点颜色、顶点UV、索引Buffer都可以用低精度的数据存储
    • TBN向量只需要存储其中的Tangent和Normal,Binormal在Vertex Shader中计算
  • 降低纹理带宽

    • 打包时贴图使用ASTC压缩格式,之前为了兼容海外的不支持ASTC的低端机,在包里加了ETC2格式的贴图,但测试数据表明,这种用户非常少(不到1%),且没有消费能力,之后放弃了ETC2,全面使用ASTC
    • 跟美术沟通,在不降低美术品质的情况下,尽量减小贴图大小,并制定为标准
    • 3D场景中的贴图使用MipMap
    • 在不影响效果的情况下,尽可能地降低RenderTarget的精度和大小

阴影优化#

  • 减少透射阴影物体的数量
  • Decal阴影,开销最小,它是脚下的阴影片,能使用instancing渲染,开销几乎可以忽略不计
  • 调制阴影,有明显的缺陷UE4 Mobile使用动态阴影的一些小结,适用于只有一个角色投影并且不接受自阴影的情况,比较适合低端机。如果有多人同时使用或要求有自阴影,需要另外另外写复杂的逻辑支持自阴影和消除阴影叠加,不如使用CSM

水面渲染#

我们项目对水的渲染要求不高,用的也是比较简单的折射反射RenderTarget的方案。我们在水面渲染上花的精力比较少,所以优化方式也比较简单。

  • 控制折射反射的物体的数量
  • 在对反射情况要求没那么精准的情况下,用环境反射探针周围的环境贴图,代替实时反射贴图

LOD系统#

层次细节系统(LOD)的全称是Level Of Detail,它的思想是对渲染的图像贡献的像素越少,就用越简单的形式渲染该物体,一般是根据物体的包围盒投影到屏幕空间的面积决定其LOD级别的。一般是用两种方式优化性能。

  • 多级别复杂度的模型,超过最大阈值后可以不渲染或者用BillBoard代替
  • 随着LOD级别的增大,可以逐步简化shader,比如减少光照的数量,去除法线效果等

后处理#

后处理是现在游戏渲染中不可缺少的一部分,它对场景渲染完成后的RenderTarget进行各种图像的处理,也就意味着它的消耗会比较高。

  • 大多后处理通常都需要多道工序,需要频繁切换RenderTarget,切换开销非常高
  • 在后处理的shader里需要读取RenderTarget,占用大量的带宽

后期处理的开销不容忽视,哪怕高端机的处理能力能跟上,但是也会带来严重的发热问题。最后我们经过与美术的研讨,决定在所有机器上都开启Bloom,其他的后期处理比如抗锯齿,屏幕空间反射都放弃了。引擎为了进一步支持低端机能开启Bloom,也做了有效的一些工作:

  • 重新审查了一遍Bloom的流程,将Bloom过程中Pixel Shader的部分计算,转移到Vertex Shader,或者由CPU计算通过uniform传递给Pixel Shader
  • 增加开关,进一步降低Bloom所需的RenderTarget的分辨率,在低端机低分辨率的情况下,基本看不出效果差异
  • ToneMapping对于后期处理非常重要,如果对屏幕分辨率的RenderTarget逐像素计算ToneMapping的结果,开销会非常高。我们把ToneMapping的结果生成了323232的颜色查找表(LUT),在ToneMapping的时候只需要查表就可以了

卡顿优化#

玩家在游玩游戏的时候能接受帧数稍微低一些,但是绝不能接受在游玩的时候频繁出现卡顿现象。我们在CBT测试的时候就碰到几个同类型GPU的机器流失率非常高,我们找到相同的机器发现由于编译shader没有处理好,导致玩家游玩的时候走几步就会发生卡顿。

造成卡顿的原因是主线程花费了大量的时间处理某件事,导致无法正常提交渲染命令。优化思路是将需要消耗大量CPU的时间的工作移动交由子线程处理:

  • 绝大部分资源的加载工作由子线程完成
  • 资源的上传和下载
  • HttpPost和HttpGet
  • Shader的编译和连接,并使用BinaryShader加快编译连接速度

在过图的时候会有大量的资源需要加载,大量的对象会在短时间内被创建,难免会出现掉帧或卡顿的情况,我们用一个Loading进度条表示正在过图中,让玩家稍稍等待,感受不到卡顿。

尽量不要在渲染时读写GPU的Buffer,如果一定要读,要支持Double Buffer或Tripple Buffer以避免发生CPU需要等待GPU的情况,详见Synchronizing CPU and GPU Work

还有非常重要的一点,解释性语言一般都会有自动垃圾回收的机制。我们使用的脚本语言是Lua,它在lua消耗的内存达到某些数值后会触发Collect Garbage(GC),造成卡顿。所以我们要控制lua内存的增长值,以及增长速度,对应的优化方案是:

  • 缓存频繁使用的UI
  • 计算Entity的位置、朝向会产生大量的临时数学对象,如Vector3、Matrix等。将他们重用起来,能明显降低临时对象的增长速度;其他的临时对象也是一样的处理思路
  • 在过图Loading时主动触发GC,降低在用户操作过程中发生自动GC的概率

内存#

内存过高会引擎OOM的崩溃,内存使用量过高会使系统频繁将物理内存中的内容交换到虚拟内存中,占用带宽,影响用户体验。所以我们要做好游戏内存使用的管理工作,使游戏在不同档次的机器上保持合理的内存占用量。具体的优化方案:

  • 尽量使用降低RenderTarget的大小和精度
  • RenderTarget Pool,避免创建过多的RenderTarget
  • 优化模型的buffer大小和动画的关键帧数量
  • 在不影响画质的情况下,尽量减小图片的尺寸;打包时使用ASTC压缩格式
  • UI的图集不要使用MipMap;尽量不要使用大面积的背景图,用九宫格+细节图组合
  • UI占用的资源合理组织缓存和释放。因为UI是用户高频操作的部分,如果处理不当,内存会增长非常快
  • 字体库的内存占用比较高,通常是5M ~ 20M。控制使用的字体库数量;使用工具(FontSubsetGUI或FontPruner)去除不需要的字形,将其内存占用降低到2M ~ 3M。工具可以参考关于字体剥离和精简工具 FontSubsetGUI 和 FontPruner 的比较

发热#

发热和耗电基本是一回事,我们尝试了若干种测量方法:

第1、2种方法实在费时且不好控制,最后我们决定用最朴素的想法和方法。我们把游戏的性能开销优化到最小,比如帧率优化到极限,然后限帧,那发热和耗电也就接近了优化的极限。经过我们的测试,除了优化帧率的措施之外,还有一些容易忽略的对耗电发热影响很大的地方:

  • 下载、网络传输对发热影响很大,经过我的测试,游戏中开启不限速的下载会导致发热量很快飙升。我们做的优化不在玩家游玩时开启资源的下载,如果需要下载资源,在用户启动游戏时下载并等待,或开启后台下载
  • 不要让安卓应用一直处于在后台挂起的状态,否则会持续耗电,这个Google会审查
  • 开发省电模式(降低屏幕亮度,减少甚至不渲染),让用户长时间不操作的时候,进入省电模式

性能优化的结果#

经过我们引擎团队与项目开发团队密切合作,在多轮的性能优化后,我们交出了一张令所有人满意的性能答卷。

我们最开始适配的低端机为1G内存的红米1S,但是发现如果支持这种机器,会对我们的资源制作、程序代码都产生很大的影响,经过CBT的运营数据,这种机器并不能给我们什么收益。我们逐步将最低端的机器放宽到Adreno 405(内存大于1.5G)的机器。

在游戏上线时,我们能保证:

在高端机上,

  • 主城5人高阶装备同屏,稳定30帧,无掉帧运行
  • 在200人多人战场PVP,稳定30帧,无明显掉帧

在低端机,我们的目标是多人同屏在主城和PVP战场25帧左右。用非常垃圾的HTC one A9测试的数据为18帧左右,虽然没有达到25帧的目标,但是能让玩家流畅地玩下去。

低端机的内存占用上限约600M,一般情况下为500M左右,线上数据也基本没有发生OOM的崩溃。

对于高端机,我们主城的D值在250左右,一个角色及其所有外显(装备、翅膀、宠物)与特效,大约占50~70D。我们游戏比较重视角色的效果展示,所以分配了很多的资源给角色。由于视角比较近,同屏显示5人就能把屏幕快塞满了,虽然人数不多,但不会显得空荡荡的。这时候D值约有五六百,其他的角色只显示名字和血条,也能让多人同屏有比较好的展现。对于低端机主城约70D,角色10D左右,多人同屏+UI+名字约300D。当然可以降低角色展示的品质,让同屏显示更多的人,具体怎么做,还是看游戏产品的定位。

性能优化过程的非技术经验#

性能优化贯穿游戏研发的始终,在不同的阶段做的事,也有所区别,也不要指望一轮性能优化就能解决所有的性能问题。

初期:在游戏已经有雏形后,要开动一轮性能优化。这次的性能优化的主要目的是与美术制定资源的制作标准,约束美术制作,避免后期大量返工。

中期:在这个过程,游戏会开发新功能,美术也会因为标准无法制作出想要的美术效果,去battle标准的合理性,这时候要根据项目现有的状态更新标准,升级引擎。

后期:这个阶段游戏的主要玩法都已经开发完毕,剩下的就是美术要大量铺量,这个时候要与项目进行深度的优化,将所有性能标准都优化到上线标准。

在项目性能优化的全部过程中,一个引擎的研发人员除了具有专业的渲染知识和性能优化知识外,我认为更重要的是其他方面的能力。这个优化过程中,并不是说一个引擎程序与一个项目程序对项目优化一通,再跟美术制定完性能标准就完事了。性能优化需要是一个引擎部门、项目程序、策划、美术、测试团队协作的大型任务,这需要一定的协调和组织能力。我们的合作模式定期开会,将性能任务优化任务拆分成引擎、策划、程序、美术、QA的任务,项目PM负责项目部分优化任务的推进,我负责引擎部分优化任务的推进。这是个宝贵的经历,所有参与者都付出了很大的努力,不仅推进项目性能优化的成功,性能优化分队也拥有了极佳的默契,个人也获得了技术和经验的提升。

很幸运,我们拥有一个很厉害的QA,在性能优化过程中起的作用至关重要。他不仅对各种型号的机器如数家珍,对机器的性能和评分也有较深的理解。他负责测试各种适配下的性能数据,验证我们的优化是否有效,适配是否合格,为整个团队指明了性能优化的方向。并且在适配大量的机器,对机器有合适的分档,使我们在高中低配置的机器上都有很好的性能表现。在此,特别感谢他!

资源规范#

在项目研发前期与美术制定好各种情形下的制作规范是至关重要的,在一个约定的范围内制作资源,可以很大程度地避免后期大面积返工现象,也让美术人员有一定的性能意识,这对提升团队整体的开发效率是至关重要的。下面是我们之前制定资源的部分资源制作标准。

骨骼#

  • 骨头数<70

场景#

粒子特效#

  • 粒子所用的模型面数不能超过1500

场景模型#

Skinned Mesh#

参考#

  1. 移动游戏性能优化通用技法 - 0向往0 - 博客园
  2. 【《Real-Time Rendering 3rd》 提炼总结】(十二) 渲染管线优化方法论:从瓶颈定位到优化策略_浅墨_毛星云的博客-CSDN博客_渲染管线优化
posted @   silence394  阅读(0)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示