使用 Unity 开发安卓游戏时,如何追踪性能问题

2015年08月26日 14:06 0 点赞 0 评论 更新于 2025-11-21 17:46

原文翻译自 codeandux.com,原标题为 “How I tackled my performance issues developing an Android game in Unity”。

前言

两周前,我开始使用 Unity 开发一款名为 SkyBlocks 的 Android 游戏,该游戏已在 Google Play 上架,大家有时间可以下载体验。在开发过程中,我遇到的最大问题就是性能问题。于是,我开始逐步分析导致性能问题的原因,并寻找相应的解决办法。

Sky Blocks 游戏机制

SkyBlocks 游戏类似于倒过来的俄罗斯方块与太空入侵者的结合体。游戏玩法是将方块摆成一行,此时该行方块会移至游戏面板的最上方,但不会像俄罗斯方块那样完全消失。玩家有 60 秒时间尽可能多地摆出方块行。UFO 会入侵“地面”(游戏面板下方),并试图破坏玩家搭建的一切。一旦 UFO 穿过防御,就会开始破坏地球,当地球血量降至 0 时,游戏结束。虽然游戏听起来简单,但开发起来颇具挑战,不过也十分有趣。

不要忘记做设计

在开发过程中,永远不要忘记先进行设计。开始开发 SkyBlocks 时,我对游戏的具体模样和实现方式并不清晰,也未深入思考如何处理相关问题。由于之前使用 JavaScript 和 HTML5 开发过俄罗斯方块,我直接将代码复制粘贴到 C# 中,并修改了一些小 BUG,如旋转时的碰撞检测方式,但未考虑从 2D 到 3D 的区别。

每次更新游戏时,我不再一次性绘制整个游戏面板,而是将每一行创建为一个 GameObject,并使用简单的立方体渲染网格中已锁定的块。每次网格更新时,我都需要销毁所有块并重新创建。在电脑上,游戏运行效果尚可。然而,我忽略了游戏面板(网格)有 10 行 20 列,最多可能有 200 个立方体需要不断渲染、销毁和重建。若有必要,行数还会增加。并且每个立方体都有自己的引用资源,这意味着每个立方体都需要调用一次绘图。铺满一个游戏面板大约需要渲染 150 - 200 个块,这就需要大约 200 次绘图调用。

如果在移植代码前进行了设计,我就能预见到游戏无法长时间稳定运行,从而避免浪费大量时间。

解决问题

解决问题的最佳方法是先勾勒整体思路,再逐步深入。需要思考各个部分如何协同工作,以及它们各自的功能。在 Sky Blocks 项目中,主要部分包括游戏面板、防御线和 UFO。

  • 游戏面板:主要用于控制游戏,上面有移动的块和已锁定的静止块。
  • 防御线:由 10 个立方体组成的静止线。
  • UFO:是一个可移动到防御线上方的组合网格。

减少绘图调用次数

在游戏开发过程中,我发现绘图调用次数过多,导致游戏在 Android 设备(如三星 Galaxy S4)上运行越来越慢。为了提升游戏性能,减少绘图调用成为关键任务。我在网上查找相关资料,思考绘图调用对性能的消耗、引发绘图调用的原因以及减少调用的方法。

在性能提升方面,我虽未能设计出完美的实验方案,但找到了一个在 CPU 和 GPU 上都能运行的方案。部分实验中性能提升不明显,且将游戏绑定到 FPS 上的大部分实验会使游戏运行变慢。经过分析,主要原因是所有立方体使用单独的素材进行渲染。

为减少游戏面板上的绘图调用,我采取了以下措施:

  • 减少对象和素材数量:实现了一个功能,将原本可能分开绘制的 200 个立方体改为绘制一整块网格。
  • 替换纹理和着色器:用顶点颜色替换单色纹理,并将素材着色器改为在网上找到的无光源顶点颜色着色器。通过这些操作,将游戏面板上的多次绘图调用减少为仅一次。

对于防御线,我将所有素材修改为相同的无光源顶点颜色。虽然没有将它们作为一个网格进行渲染,但沿用了每 10 个立方体一条防御线的策略。由于素材已共享,之前多次的防御线绘图调用也减少为一次。

遗憾的是,我没有对调整前的游戏进行截屏,且不想恢复到之前的解决方案,因此无法展示调整前后的区别。以下是优化后的游戏截屏,可让大家对游戏有一个直观的了解。

活动块原本每个包含 4 - 5 个独立的立方体,且每个立方体都有素材引用,每个块至少需要四次绘图调用才能创建。而现在,顶端的两个被锁定的块仅使用了一次额外的绘图调用。这些块作为活动块时,由原始立方体组成,每个立方体至少需要一次处理才能创建。

防御线使用相同的模式,由 10 个使用顶点颜色和共享素材的立方体组成。实际上,Unity 会自动将完整的防御线添加到“通过批处理保存”,无需在一个网格中绘制。

UFO 较为灵活,每个 UFO 被分成上、中、下 3 个独立的网格。由于我希望 UFO 随机出现并部分活动,每个 UFO 的每个部分有 3 - 4 个素材,一个 UFO 原本大约有 12 - 17 次绘图调用,但实际达到了 17 - 30 次。屏幕上同时出现 2 - 3 个 UFO 时,绘图调用次数可达 50 - 100 次。

为减少 UFO 的绘图调用,我在网上找到一个可将所有网格合并成一个的脚本,但该脚本无法很好地处理素材,因此我只能使用一种颜色的 UFO。经过调整,我可以使用至少 2 种不同的颜色和一个纹理。虽然放弃了多彩的 UFO,但将绘图调用次数减少到了约 10 次。

并非所有绘图调用的减少都能使游戏运行更快。对于一些更灵活、微妙的部分,如果愿意牺牲一定的灵活性来减少绘图调用,也是可行的。目前,我已将游戏运行期间的平均 150 - 200 次绘图调用减少到 75 - 90 次。

对于 UFO 射出的激光,我也解决了绘图调用问题。所有激光都有素材引用,全力射击时每个激光会有 30 - 40 次绘图调用。我使用相同的无光源顶点颜色着色器,并为网格分配顶点颜色,将所有激光的绘图调用减少到仅一次,即使 UFO 连续射击也不受影响。

目前,整个游戏的绘图调用已降低到 30 - 45 次,游戏运行更加流畅。其他绘图调用主要由 UI 引起,我原本打算减少 UI 对象数量来提高速度,但目前的效果已让我满意。

减少绘图调用的重要规则是使用尽可能少的素材,尽量使用共享素材而非引用素材,这有助于减少绘图调用。

确保你只加载了一次资源

在我的代码中,使用了资源加载功能,但 Unity 没有对加载结果进行缓存,导致相同资源被多次加载,这消耗了大量性能。例如,我曾多次将相同的素材加载到激光武器上,游戏在电脑上运行正常,但在 Android 设备上表现不佳。

为解决这个问题,我避免了重复加载资源,并删除了项目中不必要的资源加载代码。同时,创建了一个静态字典,以资源名称为键,资源为值。在使用资源时,先检查字典中是否已存在该资源,若存在则从缓存中获取,否则进行加载。建议大家在开发中也尝试使用这种方法加载资源。

尽可能避免实例化

我原本没有意识到对象实例化会消耗大量性能,在代码中几乎随处进行实例化操作,以为其性能消耗与创建新类引用类似。但实际上,程序在 CPU 上实例化和销毁对象都需要花费时间。例如,UFO 攻击时会创建大量激光,每个激光的实例化和销毁间隔很短,大约 20 - 40 个对象的实例化和销毁耗时 1.5 秒。减少绘图调用虽有效果,但我才发现 UFO 出现时实例化消耗了大量性能。

为解决这个问题,我创建了有序的对象池。在场景中创建了一个名为 ProjectilePool 的空对象,并在代码中创建了一些新的 Projectiles。不再在 Projectiles list 中查找可用的 Projectile,而是在 ProjectilePool 中查找。若找到可用的 Projectile,则重新设置其位置和状态,使其可以被重复使用。若未找到,则像以前一样创建一个新的 Projectile,并将其添加到 ProjectilePool 中并使其不活动。通过这种方式,将 UFO 攻击期间的 CPU 使用率降低到了 25% - 30%,游戏运行更加流畅。

总结

绘图调用

绘图调用是影响游戏性能的重要因素。为减少绘图调用,可采取以下措施:

  • 减少素材数量:使用尽可能少的不同纹理,尝试将更多纹理整合到地图集中。
  • 使用共享素材:避免使用单独引用的素材,尽量让多个对象共享同一素材。
  • 调整着色器:如使用无光源顶点颜色着色器,减少不必要的参数。
  • 合并网格:在必要时可牺牲一定的视觉效果,将多个网格合并为一个。

实例化

实例化操作速度较慢,应尽量避免。可在初始化时加载尽可能多的对象,使用时直接引用。也可使用对象池,循环使用旧对象,减少实例化次数。

需要注意的是,绘图调用和实例化并非影响游戏性能的唯一因素。绘图调用主要消耗 CPU 和 GPU 资源,而实例化主要消耗 CPU 资源。如果游戏中有大型复杂模块或大量处理任务,仅减少绘图调用可能无法显著提升游戏速度。因此,需要仔细检查代码,找出性能瓶颈并进行优化。

在我的游戏中,实例化、销毁和 Web 请求是主要的性能问题。

其他补充信息

Sky Blocks 下载地址

https://play.google.com/store/apps/details?id=com.Shinobytes.SkyBlocks

无光源顶点着色器

http://pastebin.com/RMm5a4Zv

减少绘图调用的重要性

“虽然绘图调用可能成为性能瓶颈,但帧频才是关键。如果帧频良好,就无需过于担心绘图调用。绘图调用对性能的影响程度很大程度上取决于硬件状况和每一帧的处理任务。” —— Daniel Brauer, Unity Technologies

实例化素材与共享素材

  • 实例化素材:可改变任何属性,仅为特定类实例化一次。每次实例化可能触发一次绘图调用,但实例化后修改属性不会创建新实例,仅修改当前实例。
  • 共享素材:使用相同的着色器和其他属性,Unity 可对素材进行分组并批处理使用该素材的所有对象。使用无光源着色器后,我未在代码中修改素材属性,所有素材都被一起批处理。

我将发布一篇更详细的文章,介绍针对不同对象提高性能的方法,包括更多代码示例。最后,祝大家开发愉快!

作者信息

洞悉

洞悉

共发布了 3994 篇文章