行业资讯 分类
[翻译]Unity游戏优化最佳实践发布日期:2024-05-06 浏览次数:

帮助你发行游戏的Unity专家贴士

原文链接:

Unity Games Optimization Best Practices v5
本文翻译自Unity官方发行的电子书《Unity Game Optimization Best Practices》。本文主要介绍了使用Unity作为游戏引擎,从游戏策划到实际研发的最佳实践。其中包括了策划、工作流程、调试、资源管理、代码架构、物理、动画、GPU性能、UI这九大主题。译者水平不足,翻译错漏及不足之处请在下方评论中指出,谢谢 。—— CJT

几乎所有游戏都是携着一个创新的灵感和一个工作室制作最好的游戏的希望降生于世的。这是一个伟大的事业,而且它可能带来很大的回报。但是在发行一款完整的以及优化好的游戏的旅途上有着无数挑战。

考虑到这一点,来自Unity的Integrated Success Services(ISS)的专家们准备了这个指南——围绕9个重要的研发领域——以帮助你更好地理解并避免常见地内存、性能和平台问题。


研究特性需求和目标平台

在启动你的项目之前,先透彻地研究你的特性需求和目标平台。确保所有的目标平台都支持你的需求(例如,在低端移动设备不支持实例化渲染)。一定要考量清楚你准备做的可能的变通方法或是妥协。

同样的,为每个目标平台定下最低要求。并为你的研发及QA团队寻找到多个设备,因为在开发过程中将会用到不少的目标设备。这样你就可以迅速地测量并调整现实的性能表现和每一帧的预算,并且可以在整个开发过程中保持对它们的监控。

定下内存和性能预算

当你建立好了目标配置需求及支持的功能,确定好内存和性能的预算。这可能会很棘手,且在开发过程中你可能会需要重新定义和调整它们,但是带着一个合理的计划开工要比没有任何计划并将你喜欢的东西一股脑儿丢进你的项目中去要好得多。

一开始,定下你的目标帧数和理想的CPU性能预算。对于移动平台,别忘了过热可能会发生并降低CPU和GPU的频率,所以在你的规划中为它们预留空间。

从你的CPU预算开始,尝试去决定对不同系统需求,渲染、特效、核心逻辑等等,你想花费多长时间。

内存预算可能会很难决定。Asset是内存开销大户,而且是你(作为开发者)无法控制的。举例来说,你打算给asset花多少内存?贴图、模型和音效将会吃掉一大部分,而且你不小心的话很容易失去控制。避免过大的贴图,根据它们在屏幕上可视的大小以及目标平台的分辨率将它们大小控制在合适的范围。类似的,根据情况确保模型有着合适的顶点和三角形数。一个只在远处出现的模型不需要高精度的模型。

最后,思考一下对于你项目中的系统你可以给多少内存。举例来讲,你可能实现了一个特定的系统来预先计算大量的数据以减少每次更新时CPU的计算量,但这合理吗?这就是那个消耗了不成比例巨大内存的系统吗?或许它是对于性能来讲最重要的系统,因而这些代价都是值得的。这都是你需要规划好的问题。

建立打包和QA的步骤

建立好一个打包和QA的步骤至关重要。本地打包和测试在某个时间点前都十分有用,但这同时也很消耗时间及容易出错。对于这个问题有多种解决方案可以供你考虑(例,Jenkins就很流行)。你可以选择配置自己的打包专门用机,或者使用一种云服务来减小维护自己的打包机器的开销。你也可以考虑Unity TeamsCloud Build功能。花点时间评估并选择一种适合自己需求的方案。

先计划好如何将功能发布到你的发布版本是个好主意。与版本控制一起,考虑你想怎么样打包和验证你的开发分支。作为打包过程中一部分的自动化测试可以找到许多问题,但不是全部问题。如果你确实有自动化测试运行,记得收集测试结果,以便在不同版本间比较测试的表现。这将帮助你更快地发现退化。你也需要在你的流程中加进一些手动质量检测。只要它们被验证完了就可以合并分支到发布版本中。

用Unity Cloud Build为你的项目创建自动化打包

? 考虑从头再来

在完成你项目的原型之后,认真地考虑是否要从头开始你的研发过程。在原型阶段作出的决策通常以速度为优先,且很有可能原型项目中包含了许多的“hack”因而并不是一个适合你开展工作的牢固基础。


大部分的开发者都想把他们的时间花在创造和生产上,而不是持久地与工作流程的毛病搏斗。然而,在开发过程中糟糕的选择可能会导致低效和团队的失误,影响最终产品的质量。要减少这种情况,对于常见的任务找方法以精简优化它并使其更健壮。这将使你的团队因问题导致的损失的减到最少。

自动化重复的手动工作

重复化的手动工作往往是自动化(例如,通过自定义脚本或是编辑器插件)的首要选择。一段相对较小的时间和工作量可以转化成整个团队在整个项目生命周期中累计节省的大量时间。自动化工作也剔除了人为失误的风险。

特别是,确保你项目的打包过程是全自动并可以一键完成的,要么是在本地要么是在一个Continuous Integration服务器上。

使用版本控制

所有的开发人员都应该使用一种版本控制,Unity也有多种内置的解决方案

Unity编辑器中的版本控制设置

确保你项目的Editor Setting中的Asset Serialization Mode设为了Force Text,这应该就是默认的值。

Unity同样也有一个内置的专门为合并场景和Prefab的YAML(一种人类可理解,数据序列化的语言)工具。确保把这个同样也设置好。更多信息,请参见这里SmartMerge

如果你的版本控制方案支持提交hook(例如,Git),你可以用它来执行某些特定的标准。更多信息请参见Unity Git Hooks

你应该将所有开发工作和你的主分支隔离开来以保证你的项目总是有一个可以稳定运行的版本。使用分支和标签来管理milestone及发布。

对团队执行一个良好的提交信息规定。清晰、有意义的提交信息将帮助在开发过程中定位问题,而空白或无意义的信息只会平添麻烦。

最后,优秀的版本控制可以协助你迅速找到一个问题是什么时候开始出现的。以Git为例,它有一个bisect功能让你选择一个好的版本和一个有问题的版本然后使用“分而治之”的方法来检查中间版本并测试和标记它们是好或坏。花些时间学习你的版本控制系统有哪些功能并找到那些可能在开发过程中有用的。

利用好Cache Server

在Unity编辑器中切换目标平台,尤其是大型项目,可能会非常慢,所以我们推荐使用Unity Cache Server。如果你需要多项目或版本的Unity的支持,你可以运行多个cache server,乃至使用不同的端口。

小心插件

如果你的项目包含了大量的插件和第三方库,因为许多插件都内置了测试用资源及脚本,很有可能这些未被使用的资源被一起打包进了你的游戏。

如果你正在使用Asset Store的资源,检查哪些依赖被添加进了你的项目。举例来讲,你可能会惊讶地发现你的项目中有多种不同的JSON库。

最后,将你不需要的插件中的资源剔除,以及项目原型阶段留下的那些旧资源及脚本。

注重于Scene和Prefab的合作

思考清楚你打算让团队如何合作相当重要。大型、单一的Unity Scene并不适合团队工作。我们建议你将关卡拆分成多个相对较小的场景以方便美术和设计师在最小化冲突的情况下共同制作同一个关卡。在运行时,你的项目可以通过SceneManager.LoadSceneAsync()接口以及传进mode=LoadSceneMode.Additive来叠加地加载场景。

Unity 2018.3和之后的版本有Improved Prefabs可以支持Prefab的嵌套。 这可以允许将Prefab内容拆解成更小的分开的对象以让不同的团队成员在最小化冲突的风险下独立地工作。就算如此, 也会出现团队成员需要对同一个资源开展工作的情况出现。确保你们对如何处理这个问题达成了共识。这个可能仅仅需要一个内部沟通政策从而团队成员通过一个Slack频道、邮件等相互通知。如果你的工作流程支持的话,你也可以使用版本控制系统的文件check-in/check-out机制来解决这个问题。


经常调试你的项目

别让你项目的问题累积起来——试着频繁调试你的项目,而不是仅在项目计划的后期或者说当你的项目表现出了性能上的问题时。对你的项目典型的“性能表现”有个良好的感觉可以帮助你更容易地发现性能问题。如果你看到了一个新的问题,请尽快调查。

一些重要的调试技巧

  • 不要基于假设来优化你的项目,务必根据在调试过程中的发现来优化。
  • 共同使用Unity以及平台专用的调试工具一起来搞清楚你的项目到底发生了什么。
  • 在Unity编辑器中调试你的项目有一定作用,但一定要在目标平台本身上进行调试。
  • 进一步调查可以考虑使用自动化测试来识别各种关联。同样的,可以考虑保存调试结果再通过类似Profile Analyzer这类工具进行手动比较。

Unity工具

Unity自带的调试工具通常需要在你项目的开发版本运行。所以尽管性能表现和最终的非开发版本不完全一致,它应该可以提供一个相对较好的全局表现,只要你选择了Release而不是Debug设置。

? 用Unity Profiler查看项目的关键部分

经常用Unity Profiler来查看你项目的关键部分。来查看你项目的关键部分。

在你的脚本中增加有意义的Unity Profiler采样器来在不用进行Deep Profiling的情况下为你提供更直观的信息。Deep Profiling会极大影响性能而且只在Unity编辑器中或者是支持Just-In-Time Mono编译的平台(例如,Windows,macOS和Android)运行。

别忘了除了可以重新排序Profiler工具的每条记录(比如CPU、GPU、物理等)你还可以添加和移除它们。在Unity 2018.3和之后的版本中,移除那些目前不需要的记录将减小在目标平台上运行时CPU的占用。在以前的Unity版本中,在运行时所有调试信息都会被收集,因而增加了Profiler的性能消耗。

在Profiler的Hierarchy视图中,按Total列来排序以定位CPU消耗最大的区域。这将帮助你集中到需要调查的部分。

用Unity Profiler的Hierarchy视图调试最消耗性能的代码

类似的,按GC Alloc列来排序以揭露产生托管内存分配的区域。以尽可能减小内存分配为目标,尤其是那些经常甚至于每帧都会发生的,并集中解决可能导致托管内存堆迅速增长并触发垃圾回复的内存分配高峰。将托管内存堆(managed heap)的大小(在Profiler的Memory记录中显示为Mono)保持的尽量地小。因为使用的内存越多,它就会越发碎片化,从而导致垃圾回收极其昂贵。

不像只注重于Unity主线程的Hierarchy视图,Profiler的Timeline视图将提供给你一个宝贵的关于多个线程的总览,包括Unity主线程和Rendering线程,Job System线程和用户线程(如果添加了合适的Profiler采样器)。

用Unity Profiler的Timeline视图调试多个线程

用Profiler Analyzer来对付退化测试(regression testing)

Profile Analyzer可以让你从Unity Profiler中导入数据——或是目前捕获的数据或是保存在文件中的先前的记录——并允许你进行多种分析。除了可以看到分解成平均值、中位数和峰值消耗的调试标记,你还可以比较数据集。这相当有用,比如说在退化测试中当你比较改变前后收集到的数据。

比较前后的Profiler数据来查看性能改进了或是退步了

记录并可视化内存消耗

Memory Profiler(可以在Package Manager中的Preview package里找到)是一个可以记录并可视化部分(不是全部)应用的内存使用的强大工具。它的Tree Map视图对于立即可视化常见资源类型的内存消耗,如贴图和模型,以及其他的内存管理类型如字符串和项目特有的类型,特别有用。

检查资源消耗的区域以排查潜在的问题。比如说,异常巨大的贴图可能是错误的导入设置或过高的分辨率所导致的。重复的资源也可能在这里被发现。不同资源有相同的名字是可能的,但是相同名字和相同大小的多个资源就可能是重复的资源且需要调查清楚。错误的AssetBundle分配是该问题潜在的一个原因。

检查渲染和批合并的流程

Frame Debugger工具对于检查渲染流程和批合并的部分相当有用。比如说该工具将告诉你为什么一个合并被打断了,这为你优化你的内容指明了方向。

当你看到一帧是如何创建起来的,其他的问题可能也会显露出来。比如说多次渲染相同的内容(例,在实际项目中重复的摄像机曾被发现)以及渲染完全被遮挡了的内容(例,在一个全屏幕2D UI后继续渲染一个3D场景)。

平台/供应商特有的工具

尽管Unity的调试工具提供了海量的功能,花费时间学习高效地使用你目标平台的原生调试工具也非常值得。

例如:

这些工具包提供了更加强大的调试选项,比如说基于采样和基于指令的CPU调试,完全原生的内存调试,和包括shader调试在内的GPU调试。通常你将对你项目的非开发版本来使用这些工具。

你将可以测量所有CPU核心、线程和函数的性能,而不只是那些有Unity Profiler标记的。

内存调试(例,在iOS中通过Instruments' Allocation和VM Tracker工具)能揭露不会在Unity的Memory Profiler中显示的内存大户,例如第三方插件的原生内存调用。


资源管线非常重要。你项目的大部分内存占用将来自贴图、模型以及音效这些资源,因此非最佳的设置将会让你流很多血。

为了避免这种情况,建立好一个美术资源的工作流程,来将美术资源制作成正确的规格至关重要。如果可能的话,从一开始就带一个经验丰富的技术美术一起讨论,让他帮助定义这个步骤。

首先,为你将使用的格式和规格定下清晰的指导方针。

Unity可以导入许多类型的资源文件

你的项目中所有的资源都正确地设置的导入设置是非常重要的。确保你的团队明白他们工作的资源的设置,并执行规定来确保你所有的资源有一致的设置。

同样的,不要对所有平台仅仅使用默认的设置。使用平台特定的设置来优化资源(例如设置不同的最大分辨率或者是音频质量)。

你可以考虑使用AssetPostprocessor API来自动设置好新资源的导入设置,就像这个项目

相关阅读:美术资源最佳实践指南

正确地设置好贴图的导入设置

为贴图资源设置好导入设置

贴图通常是所有资源里面消耗内存最多的,所以请确保你的导入设置是正确的。

请永远不要勾选Read/Write Enabled选项,除非你真的需要在脚本中读取像素信息。该选项将在CPU内存和GPU内存中各保存一个副本,因而将总体的内存开销翻倍。好消息是,你可以通过Texture2D.Apply() API然后定义参数makeNoLongerReadable=false来强行使贴图资源丢弃CPU内存上的副本。

当需要mipmap的时候才启用它。通常来讲在3D场景中使用的贴图需要mipmap,但在2D中则没有这个需求。当不需要这个选项却仍勾选它的话会增加约1/3的贴图内存占用。

能用压缩就用,尽管不是所有贴图都能保持足够的视觉上的清晰度,尤其是UI贴图。了解清楚对于你的目标平台可选压缩格式的优缺点,并作出明智的选择。举例来讲,在iOS平台上用ASTC通常最终的效果比PVRT更好,但是这个格式不被没有A8或更新的芯片的低端设备支持。有些开发者选择将不同压缩版本的资源放进项目,允许他们对特定设备使用最高质量的资源。

尽量多使用Sprite Atlas来将贴图组合。这将改善批合并以及减少draw call数目。根据你使用的Unity版本,可以使用Unity SpritePackerUnity SpriteAtlas或者类似Texture Packer这种第三方的工具来进行这个操作。

通过禁用Mesh选项来收复内存

为模型资源设置好导入设置

同理,当你需要通过脚本来读取网格数据的时候才勾选Read/Write Enabled选项。该选项直到Unity 2019.3之前都是默认勾选的,而且就留着让它保持勾选的状态相当常见。对于许多项目你可以通过禁用该选项收复内存的失地。

如果你的项目用到了不同的模型文件(例如FBX)来导入可见的网格和动画,请确保将源文件中的网格丢弃。否则,这些网格数据仍会被打包进最终的输出中,浪费内存。

为音频选择最佳的压缩格式

对于大部分的Audio Clip,Load Type设置中默认为Compressed In Memory会比较好。确保你为每一个平台都选择了合适的压缩格式。主机有其自己的自定义格式,而其他平台Vorbis是一个不错的选择。在移动平台上,因为Unity并不支持硬件解压缩,所以在iOS上使用MP3并没有什么优势。

为每个目标平台选择合适的采样率。举例来说,移动设备上的一段短音效不应超过22050Hz,而实际上大部分的音效都可以在最终效果差别极其细微的情况下用一个更低的数值。

确保较长的音乐或环境音效的Load Type设置为Streaming,不然整个资源会一次性地加载进内存中。

将Load Type设置为Decompress On Load会占用一定的CPU及内存来将音频解压缩成原生的16-bit PCM音频数据。该选项通常只对于经常同时发生的短音效,如脚步声或剑相撞声,有必要使用。

当可能的时候,使用原始(无损)未压缩的WAV文件作为你的资源。如果你使用压缩格式(譬如MP3或Vorbis)的音频文件,Unity会解压缩它并在打包过程中再次压缩。这两次有损的转换步骤会降低最终的音频质量。

如果你计划在你的项目中将声音放置在3D空间中,你应该将它们改为单通道或者打开Force To Mono设置。这是因为放置在某个位置的多通道音频在运行中会被压缩成单通道,因而增加了CPU的消耗和内存的开销。

用AssetBundles,而不是Resource文件夹

使用AssetBundles而不是将资源一股脑地放在Resource文件夹内。放在Resource文件夹中的资源会被打包进一个内部的AssetBundle中,接着这些的文件头信息会在启动时加载。因此将大量资源放进Resource文件夹的结果就是启动时间变长。

为了避免重复的资源,显式地将所有资源放进AssetBundles中,尤其是那些有依赖的。比如,假设有两个材质用了同一张贴图。如果这张贴图没有被放进一个AssetBundle中而那两个材质被放进了两个不同的AssetBundle,那么这张贴图将会被复制一份然后分别随着那两个材质放进AssetBundle。显式地将该贴图放进AssetBundle来避免这种情况的发生。

你可以使用AssetBundle Brower工具来设置和追踪AssetBundle中的资源和依赖关系,AssetBundle Analyzer工具来高亮显示重复的资源以及潜在的非最佳设置。

为把资源整合进AssetBundle中建立你自己的解决方案,因为放之四海而皆准的解决方案并不存在。许多人选择按逻辑来组合AssetBundle,例如,对于一个赛车游戏,你可以将每款车型所用到的所有资源放入一个单独的AssetBundle中。

但是,对于那些支持patching的平台要小心。因为压缩算法的应用,对资源一个很小变更都会导致整个AssetBundle的二进制数据改变。这意味着不能生成一个高效的patch delta。因此,如果你的项目用了很小数目的大AssetBundle,这将会成为一个大问题。为了patching,使用相对较小的AssetBundle而不是仅使用几个大的。


通过遵守这些最佳实践以及作出明智的架构决策,你将确保更高的团队效率和游戏发售后更好的用户体验。

相关阅读:如何获得更好的编写脚本的体验

避免抽象类

过度设计和抽象的代码可能会令人难以理解,尤其是你团队的新进成员,而且这通常会让分析和讨论变更更加困难。这类型的代码通常会促生更多代码,导致更长的编译时间(包括有更多IL去翻译的IL2CPP build),而且最终的代码可能会更慢。

理解Unity Player Loop

确保你对Unity的帧(玩家)循环有着足够的理解。例如,知道Awake,OnEnable,Update和其他函数是什么时候被调用的相当重要。你可以在Unity文档中找到更多信息。

默认的Fixed Timestep设置通常会导致多余的CPU计算

Update和FixedUpdate之间的差别极其重要。FixedUpdate方法被项目的Fixed Timestep值控制。该值默认为0.02(50Hz)。这意味着Unity保证FixedUpdate方法每秒调用50次,因而一帧中有多次调用是可能的。如果你的帧数下降了,这个问题可能会更加严重,因为一帧中FixedUpdate调用的次数变多了。这将可能导致我们平时称之为“死亡螺旋”的恶性循环发生,因为它有时会展现出严重故障的形态。

物理系统更新是FixedUpdate是一部分,所以有大量物理内容的游戏可能会在这个地方挣扎。一些项目也使用FixedUpdate来运行各种游戏系统,因此请小心地检查及避免这类性能问题的发生。由于这些原因,我们强烈推荐你将项目的Fixed Timestep值设置成与你的目标帧数高度接近的数值。

选择合适的帧数

选择一个合适的目标帧数。举例来讲,移动项目常常需要在流畅的帧数和电池消耗及过热降频之间取得一个平衡。以60 FPS运行一个大量占用CPU和/或GPU资源的游戏会导致更短的续航时间和更快的过热。对于大部分项目来说,以30 FPS为目标是一个完全合情合理的妥协。你也可以考虑通过在运行中修改Application.targetFrameRate属性动态调整帧数。

在Unity 2019.3 beta开始支持的全新的On-Demand Rendering功能可以让你降低渲染的频率同时保证其他系统,如输入系统,不受影响。

在脚本或动画中没有将帧率考虑进去很常见。不要假设更新时间是一个常量,在Update中用Time.deltaTime并在FixedUpdate中用Time.fixedDeltaTime

避免同步加载

当加载场景或是资源时常常会出现性能曲线上的高峰,这通常是因为把加载放在Unity主线程中同步执行。因此,将你的游戏设计成健壮的异步加载来避免这些性能高峰。使用AssetBundle.LoadFromFileAsync()AssetBundle.LoadAssetAsync()而不是AssetBundle.LoadFromFile()AssetBundle.LoadAsset()

将你的项目设计成异步的还有其他的好处。用户交互可以保持流畅。与服务器的认证和交换过程可以与场景和资源的加载同时进行,因而减少整体的启动和加载时间。完全异步的设计还意味着将来将你的项目移植到全新的Addressable Asset System会更加简单。

使用预分配对象池

当对象频繁创建和摧毁的时候(合适例子是子弹或是NPC),使用回收重用的预分配对象池。尽管每次重用重置对象的特定组件可能会消耗一定的CPU,这仍然比新建一个对象要节省性能。这次方式同样极大地减少了你项目中托管内存分配的次数。

尽可能减少使用标准behavior方法

所有自定义的行为都继承于一个定义了Update()、Awake()、Start()以及其他方法的抽象类。如果一个行为不需要用到这些方法(完整的列表可以在Unity官方文档中找到),请完全移除它而不是留着一个空函数体。否则这个空函数将仍被Unity调用。这些方法会导致小量的CPU开销,主要是因为从原生代码(C++)调用托管代码(C#)的消耗。

这对于Update()方法来讲特别麻烦。如果你的项目里有大量对象都有Update()方法,这些CPU的开销可能会上升到对性能有影响的地步。考虑使用“管理者模式”:一个或多个管理者类实现一个Update()方法来负责所有对象的更新。这会极大减少原生代码和托管代码之间的通信次数。更多细节请看这篇博客文章

也一样决定你项目中那些系统需要每帧更新。游戏系统通常可以运行在更低的频率,轮流更新。一个简单的例子就是两个系统在每两帧交替更新。

我们亦推荐使用时间分片,既一个负责大量对象的系统将更新工作分摊到多个帧中,在每一帧中仅更新一部分对象。这种设计同样可以帮助减少CPU负担。

这个更高级的形态可能是你实现一个复杂的“更新预算管理系统”。你项目中的各个系统都被分配了一个每帧最大的时间预算,然后每个系统实现一个基于标准接口的管理类来在给定时间内执行尽可能多的工作。这个方法可以在整个项目的生命周期中非常好地帮助管理CPU的高峰负担。遵循这种设计模式的系统也可以变得更加灵活。

记得缓存昂贵的API结果

尽可能地缓存数据。常见的API调用如GameObject.Find(),GameObject.GetComponent()以及存取Camera.main可能会非常昂贵,所以避免在Update()方法中调用它们。正确的做法是在Start()中调用它们并保存调用的结果。

避免运行中的字符串操作

避免在运行中执行包括连接在内的字符串操作。这些操作将导致大量托管内存分配,从而导致垃圾回收的高峰。只有在字符串确实改变了的情况下才重新生成字符串(例如玩家得分),而不是每帧都更新。你也可以使用StringBuilder类来显著减少内存分配的次数。

避免不必要的debug日志

非必要的debug日志经常会导致项目性能的高峰。像Debug.Log()这类API会继续产生日志,甚至是在非开发build中,这往往使开发者大吃一惊。为了避免这种情况,请考虑将这些API像Debug.Log()的调用封装进你自己的类中,在方法上使用Conditional属性[Conditional("ENABLE_LOGS")]。如果没有定义这个Conditional的属性,那么这个方法和它的所有调用都会被丢弃。

不要在重点代码部分使用LINQ查询

尽管因为其强大的功能和易用性,使用LINQ查询很有诱惑力,请避免在关键部分(例如,常规更新)中使用它们。因为它们可能会生成大量内存分配以及占用大量CPU资源。如果你必须要使用它们,保持警惕并限制它们只在偶尔的情况下使用,譬如关卡初始化,只要它们不造成不必要的CPU高峰。

用不分配内存的API

Unity有一些API会产生托管内存分配,比如Component.GetComponents()。像这种返回一个数组的API是在内部分配内存的。有时候它们会有不产生托管内存分配的替代者,这时永远要使用不产生托管内存分配的。许多Physics API有全新的不产生托管内存分配的替代品,例如,用Physics.RaycastNonAlloc()而不是Physics.RaycastAll()

避免静态数据解析

项目经常会处理存储在像JSON或XML这种可读格式文件里的数据。这不仅从服务器下载的数据中很常见,在处理内置静态数据中也很常见。这类解析可能会很慢且往往产生大量托管内存分配。正确的做法是使用ScriptableObject以及自定义编辑器工具来处理游戏中内置的静态数据。

相关阅读:用Scriptable Object来架构你游戏的3个好办法


主要因为在一帧中可能多次调用FixedUpdate(),在游戏中物理系统往往过分消耗性能。然而,你也需要注意其他的潜在可能影响性能的问题。

当可能时,启用在Player Settings中的Prebaked Collision Meshes选项来在打包过程中生成运行中使用的物理网格数据。否则,这些数据将在加载资源时生成。且拥有大量物理网格的对象会在生成中占用Unity主线程的大量CPU资源。

Mesh Collider的消耗可能会很大,因此根据实际情况使用一个或多个简单的内置碰撞体来近似原来复杂的形状。

设置

在Unity编辑器中配置物理设置

考虑禁用自Unity 2017开始支持并一直默认开启的Auto Sync Transform选项,以加强向后兼容性。该选项将确保自动化变换任何修改并立即同步更新其内部的Physics对象。然而,在Physics模拟进入下一帧之前将这完成并不总很重要,而它却占用了一帧中大量的CPU时间。禁用该选项将推迟进行同步时间但总体来说节省一定的CPU时间。

如果你使用了碰撞回调,请启用Reuse Collision Callback选项。这将在回调中重用一个内部Collision对象以避免在每次回调中分配内存。同样的,在使用回调函数的时候请小心不要在其中做复杂的计算。因为在大量碰撞事件的复杂场景这些开销将会快速叠加。在你的回调函数中添加Unity Profiler标记可以让它们可见并让你更容易追踪判断它们是否是性能问题。

最后,确保你的Layer Collision Matrix是最优的,既只有需要碰撞的层被勾选。


因为开发者经常喜欢大量使用Animator,在项目中动画这一特性往往惊人的昂贵。

Animator主要为人形角色所设计的,但经常被用来制作单一一个数值的动画(例如,一个UI元素的alpha通道)。尽管由于其状态机流程使用Animator很方便,它实际上相对比较低效。在内部测试中的结果显示,运行在如低端iPhone 4S这样的设备上时,Animator的性能在有约400条曲线时才胜过Legacy Animation。

对于更简单的使用例(比如改变一个alpha值或是大小),考虑使用更轻量的实现方式如自己实现一个功能类或是使用一个类似DOTween的第三方库。

最后,如果你是手动更新Animator的请小心,因为它们通常是使用Unity的Job System并行处理的,手动更新会把它们强制放在主线程中执行。


我们建议使用GPU调试来显示顶点、片段和计算着色器占用的GPU性能。这可以让你调查最昂贵的绘制调用并找到最昂贵的着色器,提供了大量的潜在优化可能。

相关阅读:

优化图形性能表现

最优化角色建模

Shader调试和优化贴士

用Xcode的GPU Capture工具在iOS上调试shader

小心overdraw和alpha blending

通常移动平台会被alpha blending和overdraw严重影响。大量的GPU渲染时间被大面积但很难注意到的overlay或是有大量零alpha像素的多层alpha-blended sprites特效占用的情况并不少见。

也避免绘制不必要的透明图片。对于那些有大量透明区域的图片(如全屏vignette特效图片),请考虑使用自定义的mesh来避免渲染那些透明像素。

保持shader简洁,精简shader变体

在移动平台上,让着色器越简单越好。用你能实现的最轻量的自定义着色器,而不是用Standard着色器。对于低端的目标平台,使用简化版本的特效或者直接禁用它。

请试着将着色器变体数量控制在最低水平,鉴于大量的着色器变体会影响性能以及消耗大量运行时内存。

不要依赖于大量Camera组件

请避免使用多于你实际需求的Unity Camera组件。举例来讲,现在不难见到项目使用多个摄像机来创建UI层。无论是否做了有意义的工作,每一个Camera组件都会造成一定的开销。在性能强大的目标平台上这可能微不足道,但是在低端或移动平台上每个Camera组件可能消耗最高1ms的CPU时间。

考量静态和动态合并

对共享同一个材质的环境网格启用Static Batching。这允许Unity合并它们以极大地减少绘制调用数量及渲染状态变更,同时可以利用对象剔除。

通过调试来决定你的项目是否在使用Dynamic Batching时表现更好,通常情况下都不是。对象足够相似并必须符合严格且相对简化的条件才可以动态合并。Unity的Frame Debugger可以协助你看到对象未能成功合并的原因。

别忘了正向渲染和LOD

当你使用正向渲染(forward rendering)的时候避免使用太多动态光源。每一个动态光源都会为每个受光的物体增加一个新的渲染步骤。

也别忘了可以使用Level of Detail(LOD)的时候就用。当物体移动到远处的时候,使用简化的材质着色器与模型以大幅改善GPU的表现。


Unity UI(也被称为UGUI)经常是项目性能问题的来源。更多细节请查阅Unity UI优化贴士

考虑使用多分辨率和宽高比

Unity UI让建立一个可以适应不同分辨率和宽高比屏幕调整位置和缩放UI很简单。然而,一种设计不总适合所有平台,所以创建多种版本的UI(或者说部分UI)来让每个设备上都有最佳的体验。

务必要在各种支持的设备上测试你的UI以确保用户体验是最佳且一致的。

配置一个Unity UI Canvas

避免使用少量的Canvas

别将你的UI元素放在一个或者几个大Canvas中。每个Canvas都对其所有元素维护一个网格。当一个元素改变的时候,这个mesh会被重新创建。

将UI元素放入不同的Canvas中,最好是根据其更新频率来分组。将动态元素和静态元素分开将避免重建静态网格数据的消耗。

注意Layout Group

Layout Group是另一个常见的性能问题源,尤其是嵌套使用它们的时候。当在Layout Group中的UI Graphics组件改变了的时候,例如说当ScrollRect移动的时候,UGUI会递归向上查找场景的层级直到找到一个没有组件父母节点。该layout和里面的所有东西随后都会被重建。

尽量避免使用Layout Groups,尤其是当你的内容并非动态的时候。为避免Layout Group仅仅用来初始化随后不会改变的布局内容,考虑添加一些自定义代码来在内容初始化之后禁用那些Layout Group组件。

List和Grid视图可能会很昂贵

List和Grid视图是另一个常见的UI模式(例,背包或商店界面)。在这种情况下,当可能有上百个物品而一小部分可见,请勿为所有物品创建UI元素,那样的会非常昂贵。请实现一种模式以重用元素并在它们移动出一边时将它们放回另一边上。一位Unity工程师已经提供了一个Github项目使用例。

避免大量重叠的元素

由多层重叠的元素构成的UI并不少见。一个合适的例子可能是卡牌游戏中的一个卡片Prefab。尽管这种方式可以允许许多设计上的自由度,大量的像素overdraw可能会极大的影响性能。它甚至可能导致更多次数的绘制分批。请研判你是否能将多个层级元素合成为更少(乃至一个)的元素。

思考你如何使用Mask和RectMask2D组件

Mask和RectMask2D组件在UI中常常用到。Mask利用渲染目标的模板缓冲来决定是否绘制像素,几乎将成本全部放在GPU上。RectMask2D在CPU上运行边界检查来丢弃在蒙版外的元素。拥有大量RectMask2D组件的复杂的UI,尤其是嵌套时,可能会占用可观的CPU性能来进行边界检查。小心别过多使用RectMask2D组件。或者是你项目GPU负载低于CPU负载时,考虑切换到Mask组件以平衡总体的负载。

组合你的UI贴图以改善合批

确保将UI贴图尽可能地合成成一张大贴图以改善合批。将逻辑上属同一类的贴图合成一张大贴图合情合理,但也要小心大贴图不要过大以及/或者过于稀疏地填充。这是一个常见的内存浪费来源。

同时也要确保可以压缩大贴图的时候将其压缩。通常来讲UI贴图是无需压缩的,因为压缩会产生瑕疵,但这也取决于实际情况。

在绝大部分情况下,UI贴图是不需要mipmap的。所以请确保其在Import Setting中是禁用的,除非你确实需要它(比如说世界空间中的UI)。

当你添加一个新的UI窗口或屏幕时请小心

当添加一个新的UI窗口或是屏幕时经常会引发项目中的问题。这有许多潜在的原因(例如,因为根据需求加载资源以及实例化一个有大量UI组件复杂结构的纯粹开销)。

也尝试减少UI的复杂程度,考虑缓存相对来讲经常使用的UI组件。禁用并启用它然而不是每次都摧毁并重实例化它。

当不需要时禁用Raycast Target

记得禁用UI Graphic元素的Raycast Target选项,如果它们不需要接受输入事件。许多UI元素都不需要接受输入事件,比如在按钮上文字或者不可交互的图片。然而,UI Graphic组件的Raycast Target选项时默认开启的。复杂的UI可能会有大量非必须的Raycast Target,所以禁用它们可能会节省不少的CPU处理时间。

对不需要输入事件的元素禁用Raycast Target

我们希望本文中的最佳实践和贴士可以帮助你的项目建立一个健壮的结构并且你使用正确的工具和设计、开发、测试和发行的工作流程。当你需要更进一步以及来自最后期限的压力增大,请务必去寻找其他公开的Unity资料。

? 更多信息

你可以在Unity Blog,推特的#unitytips标签,Unity社区论坛Unity Learn上找到更多优化技巧,最佳实践和新闻。

?♂? 联系Unity ISS

需要个性化的服务?考虑一下Unity Integrated Success Services。ISS不只是一个支援包。有着一位专门的Developer Relations Manager(DRM),一位Unity专家迅速融入你的团队,你的项目将得到极大地加强。你的DRM将确保提供给你专门的技术和操作专家以解决问题和保证你的项目发售前后都运行流畅。请填写我们的联系表格以联络我们。


平台注册入口