本文将探讨如何使用 Unity 和 Arm 分析工具来找出手机游戏中存在的性能问题。此外还有一些优化游戏内容的最佳方法。
要找出游戏中存在的性能问题,首先应在不同的设备上对它进行测试。而最好的方法则是在一台真实设备上运行,然后收集它的性能情况。诸如 Unity Profiler 和 Frame Debugger 之类的工具能够帮助用户深入了解游戏软件是如何调用资源的。此外, Arm Mobile Studio 一类的工具则能从设备上捕获性能计数器的活动数据,从而让用户准确地了解游戏运行过程中 CPU 和 GPU 资源的使用情况。笔者使用的设备中包含了一块 Mali GPU,但本文的观点也适用于采用其他 GPU 的移动设备。
笔者测试的是一款 ARPG 游戏。在游戏中,玩家要通过近战和法术攻击对抗一波又一波不断来袭的敌人。随着屏幕中出现的敌人数量越来越多,粒子数量显著增加,视觉效果后期处理也愈发复杂。很快,GPU 就不堪重负。
接下来,笔者用 Unity Profiler 运行游戏,来观察设备是否出现性能下降。很快,笔者就发现了一些重要的问题,如后处理效果、固定时间步长和实例化峰值。
后处理效果 是导致游戏运行过程中 CPU 性能不佳的主要原因。
其中,能使游戏场景中的明亮区域发光的光晕效果带来的负担最大。
从上面的屏幕截图中可以看出,渲染相机耗费时间巨大,并且会超越帧数限制。主线程等待渲染命令完成后才能准备下一帧。接下来让我们使用 Unity Frame Debugger 来对问题进行进一步的分析。
首先要注意到的是,游戏是在设备的全屏分辨率下进行渲染的。考虑到游戏内容的复杂性,这会给普通移动设备的 GPU 带来过度的压力。将分辨率降低到 1080p 甚至 720p 这样更合理的水平,可以显著降低游戏渲染,特别是后期处理效果的成本。
接下来要观察的是光晕处理过程中,有 25 个光晕效果的绘制调用。每次绘制调用均代表一个目标缓冲区,其大小从设备全屏分辨率的一半开始。此后,每次迭代时该分辨率减半。降低初始渲染分辨率是减少潜在迭代次数的一种方法。另一种选择是修改光晕效果源代码来减少迭代次数,并设置一些合理的限制。由于处理这些效果需要花费大量的时间,因此在这种情况下,最好暂时禁用后处理效果。至少在能够保证游戏的其他部分以每秒 30 帧的帧率平稳运行之前,不要使用后处理效果。
此项目的另一个改进是减少固定时间步长间隔的频率。可以发现,目前固定时间步长间隔的频率足够短,可以在一帧中多次调用;默认情况下,Unity 将其设置为 0.02 或 50Hz。对于目标为每秒 30 帧的手游,可以尝试将 时间步长 设为 0.04。因为当时间步长为 0.333 时,如果要将帧率维持在 30 帧,那么就可能出现某一帧用时特别长,导致在下一帧中要进行两次调用。这就意味着需要更长的时间,而一帧用时变长将成为恶性循环,无法打破。用户还可以设置允许的 最大时间步长 来阻止某帧用时过长,挤占后续帧的时间。
此时间步长的持续时间会给使用 FixedUpdate 函数的脚本,以及以固定间隔更新的 Unity 内部系统带来影响,例如物理效果和动画。
就本项目而言,只有物理和 Cinemachine 插件对时间耗用影响较大,每次调用大约需 3 毫秒;一次调用意味着系统已完全更新(尽管额外调用 5 次就意味着每帧浪费的时间会达到 15 毫秒)。
这是由于后期处理效果缓慢导致的。对此,前文提到通过减少固定时间步长频率来为 CPU 减轻不必要的负担,该建议仍然有效,此外关闭物理和 Cinemachine 插件也可以减少时间消耗。
在分析期间,可以在帧时间内看到峰值。在 CPU 分析器的层次结构视图中跟踪峰值后,可以发现它们源于 NPC 的实例化。
对此最常见的解决方案是,提前实例化角色并将它们闲置于 对象池中。然后从对象池中抓取这些 NPC,这样就消除了实例化成本。如果需要更多 NPC,可以根据需要扩展对象池。
同样的问题也会在使用技能时出现,因为该行为也在实例化对象。
对象池是解决这些问题最简单的方法。尽管加载时间可能会受到影响,但在运行时可以获得更加平滑的帧速率,在此例中,使用对象池方案是两害相较取其轻的选择。
我们还使用 Arm Mobile Studio 深入了解了游戏的行为。使用 Arm Mobile Studio 中的工具,我们能够获得 CPU 和 GPU 的性能计数器活动数据,由此我们可以清楚地看到游戏如何使用移动设备的资源。
你可以在此处下载免费的 ArmMobileStudio。共包含 4 个工具:
为我们提供游戏性能的快速小结,旨在用于定期的性能检查。它能够快速生成报告,特别是如果你将其构建到持续集成工作流中,并与每晚构建系统一起运用时,它的速度优势将会更加明显。为我们提供游戏性能的快速小结,旨在用于定期的性能检查。它能够快速生成报告,特别是如果你将其构建到持续集成工作流中,并与每晚构建系统一起运用时,它的速度优势将会更加明显。
在游戏的前 2 分钟内,Performance Advisor 显示我们的平均帧数为每秒 17 帧。帧率分析图表开头的绿色部分表示游戏正在加载阶段,然后图表突然变为蓝色,表示游戏在疲于处理片段(Fragment),并且始终保持这种状态。该现象意味着设备中的 GPU 正在努力处理片段工作负载,这表明游戏所需的工作量过大,或是游戏未能有效地处理像素。
我们在游戏中添加了区域注释,帧速率分析图表显示了我们自定义的区域名称。在图表标有“S,”标记的地方,Performance Advisor 截取了游戏的屏幕截图来帮助我们了解当时游戏画面如何。你可以配置在帧数低于指定值时进行屏幕截图。此时由于每秒帧数始终保持较低值,Performance Advisor 将以每 200 帧的默认间隔截取屏幕截图。
让我们来看看每帧 GPU 循环数的图表,我们为此设备添加了每帧 2800 万个循环的 预算 。在帧速率为每秒 30 帧的前提下,我们估计这是该设备能够处理的最大循环数。我们能够发现GPU 的循环数明显超出了这个预算,并且循环数随着时间的推移而增加。
Performance Advisor 在发现问题时可以提供优化建议。从每帧着色器循环数图表中,我们可以看到执行引擎循环的数量较高。在 Mali 着色器核心中,执行引擎负责处理算术运算。Performance Advisor 已将此标记为一个问题,并建议我们减少着色器中的计算。
对此有一个简单的解决方法。你可以将着色器变量的精度由 highp 调整为 mediump,这种调整对画面不会有明显影响,但可以显著减少着色器运算消耗。关于如何执行此操作的信息,请参阅我们文档中的“ 着色器数据类型和精度 ”章节。此外,正如我们在前文中讲到 Unity Frame Debugger 时提到的,游戏当前正在以设备的全屏分辨率进行渲染。我们为降低游戏渲染分辨率(降低至 1080p 或 720p)所做的任何更改也将降低片段着色成本。
我们为此设备设定了每帧 500,000 个顶点的预算。而在大约 45 秒内就超出了这个预算,顶点数量也随着时间的推移稳步增加。
查看每帧图元数图表可知,尽管可见图元的数量保持相对不变,但处理的图元总数也在随着时间的推移而增加。在游戏的前 2 分钟内,唯一创建的新对象是敌方 NPC,而它们很快就被英雄消灭了。这表明即使敌人被消灭,它们的几何图形仍然存在 - 尽管它们并不可见。
GPU 无法处理游戏需求的原因多种多样,因此我们需要进一步利用 Arm 分析工具 — Streamline。Streamline 能让我们更深入地了解这种繁重的片段工作负载,此外,通过查看其他计数器,可以找到减少负载的方法。
在 Streamline 中查看游戏的同一部分,我们可以绘制出一系列图表,这些图表显示了几何图形和像素处理不同阶段的 GPU 计数器活动。这些图示阐明了 GPU 是如何处理游戏内容的,以及是否进行了不必要的处理。
Mali GPU 采用基于图块的方法来处理图形工作负载,其中屏幕空间被分成多个图块,每个图块按顺序处理完成。首先对每个图块执行几何处理,然后在像素处理期间为像素着色。
我们已知设备中的 GPU 已被片段工作负载耗尽,因此需要寻找能够减轻像素处理阶段负荷的方法。
其中一种减轻像素处理负荷的方法是从一开始就降低需要像素处理的几何图形的复杂性。在像素处理之前,完全关闭屏幕外或背面的几何形状;但是仅部分覆盖 2x2 像素四边形的小三角形会降低片段效率,并且每个输出像素具有较高的带宽成本。
Streamline 中的Mali 几何使用率和Mali 几何剔除率图表显示了 GPU 处理几何图形的有效性。我们可以看到发送到 GPU 的图元数量,以及在几何图形处理过程中剔除的图元数量。在此阶段剔除的内容将不会继续进行像素处理。虽然这样很好,但我们还可以更有效地组织内容,使不可见图元完全不会发送出去。
在 Mali 几何使用率 图表中,我们可以看到,107 万个图元在所选时间范围(约 0.05 秒)内进入几何处理(橙色线),但在此阶段剔除了 70 万个图元(红线)。
Mali 几何剔除率图表显示了它们被剔除的原因。大约有一半图元是通过正/背面测试(橙线)剔除的,由于这些是 3D 物体背面的三角形,因此被剔除也在预期之中。更重要的是,31.9% 的图元由样品测试(紫线)剔除的,而在理想情况下,这个数值应当小于 5%。样品测试表明,这些图元太小而无法栅格化,未能达到单个样本点,因此被认为是不可见的。当具有复杂网格的对象远离相机时,网格中的三角形太小而不可见,这种情况就可能发生。较高的数值表明游戏对象在屏幕上的位置很远,其网格却过于复杂。
对于大到足以通过样本测试但仍然只覆盖几个像素的图元,这个问题更加棘手。这些“微三角形”被发送进行像素处理,而处理成本又很高。这是由于在片段着色过程中,三角形被栅格化为 2×2 像素的补丁,称为四边形。微三角形只占据四边形内的一部分像素,但必须将整个四边形发送进行处理。这意味着片段着色器将在硬件中使用空闲通道运行,从而降低着色器的执行效率。
为了检查是否存在微三角形问题,我们可以使用 Streamline 中的 Mali 核心工作负载属性 图表来监控覆盖效率。理想情况下,该数值应小于 10%。我们可以看到,在某些部分中,部分覆盖率(绿线)非常高,甚至超过 70%。该数值表明内容中包含高密度的微三角形,证实了前文强调的高样本剔除率问题。
最终出现在屏幕上的几何体需要根据其位置适当调整大小。远端的复杂布景对场景意义不大,无需详尽呈现。我们可以使用 细节层次 (LOD) 网 格处理这些远离相机的对象,减小其复杂性,节约处理能力与 DRAM 带宽。 或者不使用几何体,改用纹理和法线贴图为对象构建表面细节。
通过 Performance Advisor 的报告,我们发现着色器成本过高,因此可以通过降低它们的精度进行优化。在 Streamline 中,我们可以使用 Mali 使用变化图表来查看使用 32 位(高精度)或 16 位(中等精度)插值时的循环数。我们可以看到大多数循环使用了 32 位插值。16 位变量的插值速度是 32 位变量的两倍,存储插值结果所使用的着色器寄存器空间也仅为后者的一半,因此我们建议尽可能在片段着色器中使用 mediump(16 位)变化输入。
为了解着色器的情况,我们可以使用 ARM Mobile Studio 的静态离线编译工具来快速分析着色器程序。
首先,你需要从 Unity 提供的编译文件中获取着色代码,然后对该文件运行 Mali Offline Compile:
我们选择分析在敌方 NPC 死亡时产生溶解效果的片段着色器。以下是 Mali Offline Compiler 报告,其中突出显示了一些值得关注的部分:
我们可以看到只有2%的算术运算是在 16 位精度下运行的。所以如果我们将精度从highp降低到mediump,着色器运行效率将更高。这样一来不仅降低了能量消耗和寄存器压力,并且使性能加倍。有些情况下总是需要用到highp,例如位置和深度计算;然而在许多情况下,即便精度降低到mediump,画面上也几乎看不到明显的差异。
该报告大致分析了 Mali 着色器核心中主要功能单元的循环成本。在报告中我们发现运算单元是使用最频繁的。
在着色器属性部分,我们看到该着色器包含只依赖文字常量或统一值的统一计算。这对绘制调用或计算调度中的每个线程产生相同的结果。理想情况下,这种统一计算应该转移到 CPU 上的应用程序逻辑中。
我们还看到着色器能够修改片段覆盖遮蔽,它通过运用discard语句将片段降低到 alpha 阈值以下来确定每个像素中的哪些样本点被片段覆盖。具备可修改覆盖功能的着色器必须使用后期 ZS 更新,这不仅会降低前期 ZS 测试效率,还会降低同一坐标下后续片段调度的效率。因此,我们应尽可能减少在片段着色器中使用 discard 语句和 Alpha-To-Coverage。关于使用 discard 语句的建议,敬请参阅Arm Mali 最佳实践指南中关于使用 discard 语句的建议。
在 Arm Mobile Studio 的 Graphics Analyzer 中能够看到应用程序的所有图形 API 调用,并且可以逐步调用看到整个场景是如何一步步生成的。这大有帮助,可以用来识别那些实际显示尺寸很小,或者距离相机很远,却过于复杂的物体。以下是我们在此款游戏中发现的一些示例:
场景远处角落里的砖块是由几何体建造的,共使用 2064 个顶点。砖块细节在最终呈现的画面中并不十分显眼,因此对其的处理完全是浪费资源。
不仅如此,地砖也存在同样的问题 - 每块地砖有 1170 个顶点,然而即使对象靠近相机,这样复杂的物体也并没有给整体画面带来多少提升。在这里舍弃三角形,转而使用法线贴图来呈现凹凸和边角会更加有好处。此外,我们还能够看到这些对象是使用独立绘制调用绘制而成的。通过将对象批处理或使用对象实例化来减少绘制调用的数量能够提高性能。
我们再来看看另一个例子:场景后面的雕像,每个雕像包含 6966 个顶点。可以看到网格相当复杂,当玩家靠近雕像时视觉效果非常出色,但仅从当前相机位置来看,这些雕像却毫不起眼。当这些对象距离相机如此遥远时,使用网格 LOD 来呈现就能节省大量的算力。
为大量类似的对象降低复杂性可以大大节省几何处理负担,从而减少所需的片段着色量。这样一来不仅会降低片段工作量,增加每秒帧数,还可以减少安装游戏所占用的空间。
我们从不同方面找到了若干个对游戏进行更改从而提高性能的办法。下面是我们选择实施的内容及其具体做法。
固定时间步长间隔与帧速率无关,用于控制何时执行物理计算和 FixedUpdate() 事件。在默认情况下,固定时间步长设置为以每秒 50 帧的帧率运行。尽管 50 帧,甚至 60 帧的帧率在高端移动设备上也能够实现,但更多主流设备的帧数为每秒 30 帧,本文即针对这一数值进行探讨。进入Edit > Project Settings,然后选择Time类别,并将Fixed Timestep(固定时间步长)设置为 0.04。如此一来,物理计算、FixedUpdate() 事件和更新都将确保同步运行。
在 Unity 中对固定时间步长进行调整后,主游戏循环的固定更新部分每帧仅调用一次,平均为 1.5 毫秒。与之前的 12 毫秒相比,这不仅仅是一个巨大改进,也是针对常见性能缺陷的简单解决方案。
在应用程序启动时,内置场景或资源文件夹中引用的所有对象数据都会加载到实例 ID 缓存中。这些资产被视为一个大型资产包,因此总是有元数据和索引信息被加载到内存中。一旦使用了这个包中的资产,就永远不会从内存中卸载。
如果需要降低内存消耗,建议使用 可寻址资源系统来处理资产和资源,这样我们就可以有效地从内存中卸载无用的内容。
在我们的环境中有诸多对象屡次出现。墙壁、地砖和其他环境道具都需要重复使用,从而构建场景。我们可以为对象材质启用GPU实例化来保存绘制调用。GPU 实例化使用少量绘制调用渲染相同的网格,并允许每个实例具备不同的参数,例如颜色或比例。这种修改可以提高 CPU 性能。在下图中能够看到启用 GPU 实例化之前的 Performance Advisor 数据。
接着可以看到在应用程序的同一部分启用了 GPU 实例化的结果,这一增益看似微小却大有用处,离我们 30 帧的目标更近一步。
渲染纹理 是一种向 UI 以及许多其他用例添加 3D 元素的方式。如果有用于渲染纹理的相机,那么当该相机不用于显现画面时请确保将其禁用。我们无需渲染用户看不到的内容。使用 Graphics Analyzer 或 Unity 的 Frame Debugger 确保让这些纹理只有显示在画面中时才会更新。
为了避免一而再地重复创建和销毁相同的对象,给 CPU 增加额外负担,我们可以尝试 建议对象池。建立对象池是一种设计模式,你可以预先创建需要的对象,预先完成 CPU 的工作。然后并不销毁它们,而是重新放回池中,以便下次又需要同类对象时再次使用。这是一种解放 CPU 算力的极好方法,能让 CPU 转而处理游戏中更为重要的任务。
建立对象池后,在 Unity Profiler 捕捉到的画面中,在屏幕上当敌人一波又一波袭来时并没有出现可识别的峰值,对帧速率也没有产生明显的影响。
当网格出现在屏幕上时,GPU 需要花时间渲染网格中大大小小的所有三角形。在镜头和资产可以移动的手游中,这通常会占用大量的 GPU 资源去渲染小到无法在画面中看到的三角形网格。我们可以使用 网格细节级别来解决这个问题。让你的游戏在相机远离资产时利用更简单的网格,通过降低网格复杂性减少 GPU 的渲染负担,同时减少每帧的顶点数,让更大的三角形进行像素处理。这样做不仅提高了效率,也不会有损场景的美观程度。
有关其他资产优化技巧,敬请查看 Arm 的 游戏美工指南 。
如果你知道将会在同一场景中使用某些具有相同材质属性的资产,那么就可以对它们进行批量处理。将它们的纹理数据整合到一个纹理图谱中,对它们进行一次性绘制来节省绘制调用,与多个单独的文件相比,这样压缩后可以节省占用的空间。
在编写自定义着色器或使用 Shader Graph时,你可以选择浮点数精度或是半精度。要尽可能选择半精度,这可令着色器的性能更高,然而你可能需要使用浮点数精度来处理世界空间的位置或深度计算,敬请注意。
当着手为项目规划后期处理效果时,有两个选项可供选择:旧版 Integrated 功能集或新的 Post Processing v2 功能集。以下是使用 Integrated 功能集处理的游戏。
每 3 到 4 帧,我们就会看到垂直同步出现一个峰值,此时系统正在等待帧渲染。相比之下,使用相同的游戏效果,采用 Post Processing v2 功能集可得出如下分析器数据。
这次分析器数据显示出的结果要好得多,因为 Post Processing v2 针对移动设备经过了优化。在你的项目中应用 Post Processing v2 将会获得最好的后期处理性能。
但在追求效果的同时确保性能也同样重要。毕竟这些效果可能成本高昂。在游戏中添加后期处理效果能为项目锦上添花,改善视效。在大众化设备上关闭后期处理效果不仅省电效果显著,还能防止玩家手中的设备过热。
进行其他优化后,我们仍可以看到某些区域存在峰值。通过使用二进制搜索,打开和关闭各项元素,我们最终发现了两点:一是项目在使用后处理效果。这对总时间有所帮助,然而只要我们关闭反锯齿,帧率最终就会趋于稳定。如此一来,即使是在我们用来测试的最低规格设备上也可以保留一部分后期处理。
在优化游戏后,我们再次在 Arm Mobile Studio 上运行来观察有什么改变。此时 Performance Advisor 报告显示我们的平均帧率已经达到了每秒 28.9 帧(之前为 17),并降低了整体片段消耗。游戏中某些部分的片段活动仍然很高,所以我们还要继续优化。有了优质的数据进行指导,相信我们通过分析能够优化这些部分,进一步提高性能。
现在每帧的顶点数远远低于我们的 500,000 顶点预算,可以看到敌方 NPC 被摧毁时顶点数量的规律性下降。
现在,几何体的使用和剔除效率更高,可见图元数量在输入图元数量中所占的百分比要合理得多。如预期中一样,正/背面测试剔除了约 50% 的图元,样本测试剔除的图元低于 10%,也就表明我们减少了极小型三角形的数量。
我们在Unity Profiler、Frame Debugger及Arm Mobile Studio的帮助下找出了几种提高性能、降低CPU和GPU负荷的方法,这里得出的最佳实践完全可以避免未来游戏中出现同样的问题。
当然,优化不能牺牲屏幕图像的质量。下方将在优化前与优化后的游戏外观间做一个比对。
性能测试通常在开发周期的后期进行。 找到进一步优化的契机固然是好,但如果在发布截止日期前没有时间解决问题该怎么办?从一开始就合理设计内容,显然是更加好的办法。围绕网格复杂性、着色器复杂性和纹理压缩来制定内容预算大有裨益,这能够充分促使你的团队针对移动设备进行高效设计。下面这些内容可能对你的团队有用:
只要让大多数游戏和资产都遵循一套最佳实践,就可以在整个开发周期中定期进行性能测试,及早发现问题、解决问题。
使用持续集成系统的团队可以利用 Arm Mobile Studio 专业版提供的自动化性能测试。此版本可以在设备群中的多个设备上运行,省去手动分析的麻烦。报告的数据甚至可以输入至任何与 JSON 兼容的数据库,方便构建可视化仪表盘以及设置警报来监控性能随时间的变化,从而更快地发现问题。
你可以从 Unity 的内置分析器着手。请在说明文档中了解 “怎样分析应用程序” ,或在此处了解 Frame Debugger的使用,查看每一帧的构成。
访问Arm开发者网站来免费下载Arm Mobiel Studio ,参阅Performance Advisor、 Streamline、 Mali Offline Compiler 和 Graphics Analyzer入门指南来 快速上手。
若想获取Unity Profiler和Frame Debugger的使用帮助,请访问我们的 论坛。
若想获取Mali设备或Arm Mobile Studio的使用支持,请访问 Arm的图形与游戏论坛,Arm将积极地为你解答相关疑问。
Is this article helpful for you?
Thank you for your feedback!