优化移动游戏性能:来自Unity顶级工程师的Physics、UI和音频设置小贴士

Unity的Integrated Success团队向来以帮助客户解决最复杂的技术问题为目标。本次,我们邀请到了这支高级软件工程师团队来与大家分享一些移动端游戏优化的专业知识。
作为一支掌握引擎核心代码的团队,Accelerate Solutions致力于为广大Unity客户提供收益最大的项目优化方案。团队的日常工作就是深入了解客户项目,分析并找出有待优化的部分,再思索如何提高其速度、稳定性和效率。
我们的工程师在分享中可谓是倾囊相授,如此多的锦囊妙计无法在一篇博文完整地讲完。因此,我们决定将这些山一般的知识编纂成一本完整的电子书(请在此处下载),并编写一个新博客系列,重点介绍其中70多个可操作性较强的技巧。
在本文属系列第二篇,我们将话题聚焦于UI、Physics和音频上的性能优化方法。第一篇关于剖析、内存和代码架构的博文可在此处查看——我们将在下一篇博文中专门讨论资源、项目配置和图形方面的优化,请继续关注。
想要立即看完所有内容?那就来这里免费下载完整版电子书吧。
让我们马上进入正题!
Unity内置的Physics物理模拟(Nvidia PhysX)在移动设备上很可能会耗费过多机能,而下方提示将指导你如何挤出更高的帧率。
请尽可能多地使用PlayerSettings下的Prebake Collision Meshes(预烘焙碰撞网格)。

找到项目的Physics设置(Project Settings>Physics),并尽量简化Layer Collision Matrix(碰撞层级矩阵)。
禁用Auto Sync Transforms(自动同步变换),启用Reuse Collision Callbacks(重复使用碰撞回调)。


网格碰撞体的性能开销一般比较高昂,因此用原始或简化后的网格碰撞体来代替复杂的碰撞体是较好的选择。

使用类似MovePosition或AddForce等方法来移动Rigidbody对象。直接在Transform组件上进行平移会让引擎重新计算整个物理空间,如果恰巧场景还非常复杂,则整个流程会耗费大量的机能。物理对象的移动方法应防在FixedUpdate而非Update下。
Fixed Timestep在默认情况下为0.02(50 Hz),请根据你的目标帧率将其调成合适的值(如30 fps应为0.03)。
如果运行时出现了掉帧,这可能暗示了Unity在一帧上调用了多次FixedUpdate,且执行了过于繁重的物理运算,消耗了过多的CPU性能。
Maximum Allowed Timestep(最大时间步步长)可在掉帧出现时限制物理运算和FixedUpdate的运行时长。更低的数值表示物理运算和动画会在掉帧时被限制,从而降低其对帧率的影响。

Physics Debug(物理模拟调试)窗口(Window>Analysis>Physics Debugger)可帮助你排除有问题的碰撞体或不准确的物理运算,它会以一定的颜色来显示出可相互碰撞的GameObject。

关于工具的更多信息请参见Unity文档的Physics Debug Visualization。
Unity UI(UGUI)是性能问题中的常客。UGUI的Canvas组件负责为UI元素生成和更新网格,并向GPU发出绘制调用,而这些运作的性能消耗较大,因此我们在使用时要记住以下要点。
如果某个大型Canvas包含了上千个元素,单个UI元素的更新也会导致整个Canvas更新,造成不必要的CPU占用。
这时我们可利用UGUI的多画布功能,根据更新频率来划分UI元素。将相对静止的UI元素放在一个画布上,将需要时常更新的动态元素放在小型的子画布上,
然后统一画布内所有UI元素的Z轴坐标、材质和纹理。
部分UI元素可能只会间断地出现(譬如角色受伤时出现的血条),就算这个元素变得不可见,它仍可能会发出绘制调用。因此我们需要具体地禁用所有不可见的UI组件,仅在需要时重新激活。
如果你只需隐藏画布,可以禁用Canvas组件,不必禁用整个GameObject,从而避免引擎再度绘制网格和顶点。
类似屏幕触摸或点击等输入事件需要使用GraphicRaycaster组件处理。组件会循环检测屏幕的每个输入点,检查其与UI的RectTransform是否有重叠。
但我们不必将GraphicRaycaster放在默认Canvas层级的顶层,而可以将其添加到需要互动的元素(按钮、滚动条等等)上。

此外,不一定每个UI文本都要为Raycast Target(射线目标)。在不必要的位置禁用这个选项可让UI运算进一步简化,这在复杂的UI上意味着不小的性能收益。

由于Layout Group的更新效率较低,所以非动态内容完全可以不使用这项功能,转而借助锚点来实现比例化布局。或者,你也能用自定义代码在布局生成完毕后禁用Layout Group组件。
如果你确实需要在动态元素上使用(水平、垂直、网格式)Layout Group,请避免嵌套使用。

大型List和Grid视图会占用大量性能。如果你需要创建一个大型的List或Grid视图(比如一个包含数百个物品的物品栏),可以考虑建立一个UI库,重复使用库中的元素而非为每个物品创建一个元素。请在这个GitHub项目样例中查看实例。
将大量的UI元素堆在一起(比如卡牌游戏中的卡堆)会造成过度绘制。我们可使用自定义代码,在运行时将堆叠的多个元素合并成几个或几批较小的元素。
目前移动设备的分辨率和屏幕尺寸五花八门,而制作不同分辨率的UI、让其完美适配设备也就很有必要了。
Device Simulator(设备模拟器)支持在引擎中模拟UI在各个设备上的表现。你也可以使用XCode和Android Studio来创建虚拟设备。

如果暂停或开始菜单覆盖了整个场景,我们可以禁用渲染场景的摄像机,同样地,隐藏在顶层画布背后的元素也可被禁用。
你甚至可以在UI显示期间使用Application.targetFrameRate来降低帧率,因为UI不需要以高帧率刷新。
如果Event或Render Camera字段为空,则Unity会强行在其中加入Camera.main,造成不必要的性能浪费。
我们可以使用在画布的RenderMode中使用Screen Space - Overlay,来省略掉摄像机的使用。

虽然音频通常不会产生性能瓶颈,但一定的优化仍然可以省下些许内存。

Unity会在导入时解压所有的压缩格式(如MP3或Vorbis),在构建时再度进行压缩,这就导致了音频经受两次压缩,最终音质将不可避免地受损。
当然,我们的确能使用压缩技术来降低音频大小和内存使用:
- Vorbis格式适用于大部分音效(不用循环播放的音效可使用MP3)。
- ADPCM格式适用于短促且经常出现的声音(如脚步声、枪声)。相较于未压缩的PCM格式,该格式减小了文件体积,还能在播放时快速解码。
移动设备上的声效最高频率应为22,050赫兹,较低的频率虽然对最终音频的影响不大,但你最好用自己的双耳来做判断。
Load Type设置应根据音频的体积而异。
- 短音频(< 200 kb)可以设为Decompress on Load(加载时解压),这时音频会被解压成原始的16位PCM音频数据,产生一定CPU和内存占用,所以音频不宜过大。
- 中等音频(>=200kb)可保持Compressed in Memory(在内存中压缩)。
- 长音频(背景音乐)应设为Streaming(流播放),避免将整个音频资源一次性加载到内存中。
如果游戏中有静音按钮,静音行为不能只是简单地将音量设置为0。假设玩家无需经常性地开关静音,我们可以调用Destroy方法来将AudioSource组件从内存中移除。
我们将在下一篇博文中讨论图形和资源方面的优化。当然,如果你立即学习所有Unity工程师们提出的技巧和窍门,可在此处下载全本电子书。

我们希望帮助每位Unity创作者发挥出自己项目的最大潜力,如果你有任何想要深入发掘的优化课题,请在评论中给我们留言。