- 性能分析的最佳方法
-
验证场景中是否存在目标脚本
-
验证脚本在场景中出现的次数
-
尽量减少对正在进行的代码的更改
-
最小化内部干扰
-
最小化外部干扰
-
-
缓存组件引用
在Awake、Start等初始化期间缓存组件,不要每次用每次GetComponent
-
使用最快的方法获取组
根据测试GetComponent,在所有GetComponent最快
-
删除空的回调声明
空的Start、Update方法也是会调用的
-
在运行时避免使用Find()和SendMessage()方法
-
静态类
-
单例组件
-
分配对现有对象的引用
-
全局消息系统
像TinyMessenger等,且Unity的相关的框架基本都有自己的消息系统
-
-
禁用未使用的脚本和对象
-
按可见性禁用对象
视锥体剔除回调方法:OnBecameVisible 和 OnBecameInvisible
-
按距离禁用对象
-
-
考虑使用距离平方值
CPU 比较擅长计算浮点数的乘法,但是要计算平方根却相对比较困难。每次我们要求 Vector3 使用magnitude 属性或 Distance方法计算距离时,都会要求它执行乎方根计算(根据毕达哥拉斯定理)。但是,Vector3 类还提供了一个 sarMagnitude 属性,该属性与 distance相同,不同之处在于它计算的是平方值。这使我们能够进行基本相同的比较检查,而无须包含昂贵的平方根计算,只要我们对要进行比较的值进行平方即可。如果A的magnitude 小于 B的magnitude,则A将小于B。
-
避免从GameObject中检索字符串属性
GameObject.tag或者name属性都会引起字符串的复制,从而引起堆内存的分配,但是CompareTag方法不会。
-
更新和协程选择和优化
-
考虑缓存Transform值的更改
- 变换组件 (Transform Component)仅存储相对于其自身父级的数据。这意味着访问和修改变换组件的 position(位置)、rotation(旋转)和 scale(缩放)属性可能会导致很多意外的矩阵乘法计算,从而通过其父对象的变换为对象生成正确的变换表示形式,所以推荐使用localPosition、localRotation、localScale
- 设置脏标示,只有需要修改时才修改Transform属性
-
更快的GameObject空引用检查
gameObject == null会调用原生托管桥另一端的方法,推荐使用System.Object. ReferenceEquals (gameObject, null),当然这个方法产生较好的收益的前提是需要大量的执行gameObject == null此判断。
-
绘制调用 Draw Call
绘制调用就是从 CPU 发送到GPU 的请求,要求它绘制对象。但是,在可以请求绘制调用之前,需要满足几个重要条件。首先,必须将网格和纹理数据从CPU 内存(RAM推送到GPU 显存 (VRAM),这通常是在场景初始化期问发生的。接下来,CPU 必须通过配置选项和渲染功能来准备 GPU,这些都是处理作为绘制调用目标的对象所需的。CPU 和GPU 之间的这些通信任务可以通过基本的图形 API 进行,根据我们所针对的平台和某些图形设置,这些图形 API可以是DirectX 或 OpenGL。这些 API 具有许多可以配置的复杂且相互关联的设置、状态量和数据集,并且可用功能会根据我们所操作的硬件设备而发生巨大变化。在渲染单个对象之前,可以配置的大量设置通常会被浓缩为一个称为渲染状态(Render State)的术语。在更改这些渲染状态选项之前,GPU 将为所 有传入的对象保持相同的渲染状态,并以类似的方式渲染它们。
更改谊染状态可能是一个耗时的过程
-
动态批处理条件
- 所有网格实例必须使用相同的材质引用。
- 仅动态批处理粒子系统和网格渲染器。蒙皮的网格渲染器(用于动画角色)和所有其他可渲染组件类型无法批处理。
- 着色器使用的顶点属性总数必须不超过 900。
- 所有网格实例都应使用均匀的缩放比例,或者所有网格都应使用不均匀的缩放比例,但不能两者兼有。
- 材质的着色器不应取决于多通道。
- 网格实例不得接收实时阴影。
- 在一些联合小组会议上还揭示了若干不成文的要求:
- 每个批处理限制为 300个网格。
- 整个批处理中的索引不得超过 32000个网格。
-
动态批处理应用场景
- 一片大森林,到处都是岩石、树木和灌木丛。
- 建筑物、工厂或宇宙空间站,场景中有许多常见元素 (如计算机、管道等)。
- 游戏中包含许多具有简单几何形状的动态非动画对象和粒子效果(想象一下《几何大战》这样的游戏)。
-
静态批处理条件
- 网格必须标记为 Static(静态)
- 必须为静态批处理的每个网格留出额外的内存,即两倍内存换渲染时间。
- 网格实例可以来自任何源网格,但是它们必须共享相同的材质。
-
静态批处理注意事项
- 节省的绘制调用数无法立即从 Stats(统计)窗口中看到,必须等到运行时。
- 静态对象不应在运行时引入场景。
- 静态批处理的网格无法从其原始变换移动。
- 如果有任何一个静态批处理的网格是可见的,则整个组都将被渲染。
Load Type(加载类型):
-
Decompress On Load(载入时解压缩)
可压缩磁盘上的文件以节省空间,并在首次加载时将其解压缩到内存中,这是加载音频文件的标准方法,应在大多数情况下使用。
-
Compressed In Memory(压缩在内存中)
在加载时将压缩文件直按加载到内存中,在运行时期间,当需要播放时才对其进行解压缩。播放剪辑时,这将牺牲运行时 CPU,但会提高加载速度并减少运行时的内存消耗。因此,此选项最适合用于经常使用的非常大的音频文件,或者如果程序的内存消耗成为瓶颈,而我们愿意牺牲一些 CPU 周期来播放音频剪辑,则也可以选择此选项。
-
Streaming(流媒体)。
它可以通过逐步将文件推入一个小缓冲区,在运行时即时加载、解码和搔放文件。此方法对于特定的音频剪辑将使用最少的内存,并使用最大的运行时 CPU。这带来计后果此使用将导致大量的内存和运行时 CPU 开销。因此,此选项最适合定期播放目秀然气其他实例重種的单实例音场剪辑。也就是说,该设置最活谷在场系的整个生命周開中都需要播放的背景音乐和环境声音效果中使用。
编码格式和质量等级
-
Compressed(压缩格式)
与Compressed格式一起使用的压缩算法将取决于它所针对的平台。例如,独立应用程序、WebGL 和其他非移动平台将使用 Ogg-Vorbis 进行压缩,而移动平合则多使用MPEG-3 (MP3)格式。
-
PCM.
PCM格式是无损且未压縮的音场格式,提供了与橫拟音频的近似。它以较大的文件量换取更高的音频质量,并且最适合用于非常短的声音效果,这些效果要求非常清晰,否则任何压缩都会使体验失真。
-
ADPCM.
ADPCM 格式在大小和 CPU 消耗方面都比 PCM 高效得多,但是压缩会产生大量噪声。如果我们的音效是短暂而混乱的,例如爆炸、碰撞和撞击声,那么在其中夹杂这样的噪声显然没有任何问题,听众将完全感受不到。
最后,Compressed 格式产生的文件要比 PCM 格式小,而声音质量也更低(但是它的质量比 ADPCM 要好得多),但会占用额外的运行时 CPU 使用率。在大多数情况下应使用此格式。此选项使我们可以自定义压缩算法的最终质量级别,以根据需要的文件大小调整质量。使用 Quality(质量)滑块的最佳做法是找到尽可能小的声音质量级别(但不应该让用户注意到质量太差)。要找到每个文件的 “最佳质量级别”,可能需要由用户进行一些实际的测试体验
音频性能增强
- 尽量减少活动音频源数量
- 尽量减少音频剪辑引用 解决方案是利用 Resourees L.oad0和 ResOurces. UnloadAsset0将音频数据仅在播放时粮道在內存中,然后在不再需要用立即释放它。
- 为30声音启用强制单声道选项
- 重新采样以降低频率
- 考虑所有编码格式 如果应用程序没有内存和硬盘消耗方面的负担,则 WAV 格式可用于减少运行时的CPU 成本。因为在每次播放期间,该格式解码数据所需的开销较小。同时,如果有空府的CPU 周期,厕可以通过床缩编码格式节省空间。
- 谨慎使用流媒体 从磁盘流式传输文件应仅限于大型单实例文件,因为它需要运行时磁盘访问权限,这是我们可以使用的最慢的数据访问形式之一。使用此方法的,分层或过渡的音乐剪辑可能合出现重大问题,此时考虑使用 Resources Loado方法才是明智的。
- 通过混合器组应用滤镜效果以减少重复
- 负责任地使用 wwW.audioClip Unity 的 www 类可用于通过网络流式传输游戏内容。但是,访问 www 对象的audioClip 属性将在每次调用它时分配一个全新的 Audio Clip 资源,并且它获取资源的方法与其他 www 资源获取方法类似。一且不再需要此资源,则必须使用 Resources.UnloadAsset方法将其释放。丢弃可用(将其设置为 null)不会自动释放资源,因此它将继续消耗内存。有鉴于此,我们应该只通过一次audioClip 属性获得音频剪辑,以获取资源引用,从此以后仅使用该引用,并在不再需要时释放它。
- 考虑将音频模块文件用作背景音乐 音频模块(Audio Module)文件也称为音轨模块(Tracker Module),是一种节省大量空间而又不会引起明显质量损失的极好方法。Unity 中支持的文件扩展名是.it、.s3m、.xm和.mod。与常见的 PCM 音频格式不同,后者是作为数据的位流读取的,必须在运行时对其进行解码才能生成特定的声音,而音轨模块则包含许多小的高质量 PCM 采样,并像乐谱一样组织整个音轨,定义播放每个样本的时问、位置、音量、音调以及特殊效果。这可以节省很多的文件量,同时又保持高质量的采样。
压缩格式
Texture Type(纹理类型):
Compressed (压缩)、16-bit(16 位)和True Color(真彩色)
纹理性能增强
-
降低纹理文件的大小
-
聪明地使用 MipMap 应该禁用 MipMap 功能的其他候选项还包括:
- 几乎所有 2D 游戏申使用的纹理文件。
- 图形用户界面 (GUI)纹理。
- 总是在相机附近渲染的网格、精灵和粒子效果的纹理示例包括播放器角色本身、它们持有或携带的任何对象以及始终围绕播放器居中的任何粒子效果。
-
从外部管理分辦率的降低 为了使平台自身变得尽可能易用,Unity 做出了不懈的努力,其中就包括允许开发人员将外部工具中的项目文件(例如 PSD 和TIFF 文件)放置到项目工作区中。这些外部文件通常是文件量很大并且分成多个图层的图片。Unity 会根据文件的内容自动生成一个纹理文件供引擎的其余部分使用,这非常方便,因为我们只需要通过 Source Control(源文件控制)维护该文件的单个副本,并且当外部源文件更新时。Unity 副本也会自动更新。 问题在于,Unity 的自动纹理生成和压缩技术从这些文件引入的**混叠 (Aliasing)**可能不如我们用来生成此类文件的工具(如 Adobe Photoshop 或 Gimp)那么强大和有效。Unity 可能会通过混叠来引入伪像 (Artefact),我们可能会养成习惯,即导入比所需分辨率更高的图像文件,以保持预期的质量水平。但是,如果首先通过外部应用程序缩小图像的比例,则泥叠现象可能会少很多。在这种情况下,就可能以较低的分辦率达到可接受的质量水平,同时消耗更少的总体磁盘和内存空间。 我们可以避免由于习惯而在 Unity 项日中使用PSD 和 TIFF 文件(将它们存储在其他位置,并将降低分辨率之后的版本导入 Unity中),或者只是执行一些偶尔的测试以确你不会浪费文件大小、内存和 GPU 显存带宽 (未使用比所需分辦率更高的文件)。如果开发人员愿意花时间比较不同的降低分辦率的版本,那么这虽然会使得在项目文件管理上的便利性降低,但是却可以为某些纹理节省大量成本。
-
调整各向异性过滤水平 各向异性过滤 (Anisotropic Filtering)是一项功能,当以非常浅的(倾斜)角度查看纹理时,可以提高纹理的图像质量。图4-4 所示的屏幕截图显示了在己应用和末应用各向异性过滤的情況下道路上绘制线条的经典示例。如果不使用各向异性过滤,则绘制的线条离相机越远,它们的线条就会变得模糊和失真,而应用了 “各向异性过滤”的视图则会使这些线条更加清晰明了。 可以使用 Aniso Level(各向异性过滤级别)设置对每个纹理手动修改应用于纹理的各向异性过滤的强度,也可以使用 Quality Settings (质量设置)中的 Anisotropic Textures(各向异性纹理〉选项来全局启用/禁用该功能。就像 MipMap一样,这种效果可能代价高昂,有时甚至是不必要的。如果场景中有可以确定永远不会以倾斜角度查看的纹理(例如远处的背景精灵和粒子效果纹理)则可以安全地禁用各向异性过滤以节省一些运行时开销。开发人员还可以考虑根据每个纹理调整各向异性过滤效果的强度,以我到质量和性能之间的平衡点
-
考虑使用纹理集 纹理集 (Adasing)是将许多较小的孤立的纹理组合成一个大的纹理文件的技术,目的是最大程度地减少材质的数量,从而相应地减少绘制调用(详见本书3.1节“绘制调用”和3.3节“动态批处理”)。从概念上讲,该技术与第了 章“批处理的好处”中介绍过的最小化材质使用量的方法非常相似。
纹理集的缺点主要在于开发时间和工作流程成本。要利用纹理集来改造现有项目,需要进行大量的工作,而要弄清楚是否值得进行这项工作可能同样需要大量的工作。此外,我们还需要提防生成对于目标平台而言太大的纹理集文件。
某些设备〈特别是移动设备)对纹理的大小有相对较严格的限制,因为它需要将其拉到 GPU 的最低显存缓存中。如果纹理集太大,则必须将其分解为较小的纹理,以适合目标显存空间。如果渲染器每隔一次绘制调用就需要图集的不同部分中的纹理,那么不仅会造成很多缓存未命中,而且由于纹理不断从显存(VRAM)和低级缓存拉出,可能会出现显存带宽的阻塞。 因此,纹理集显然不是完美的解决方案。如果不清楚是否会带来性能优势,那么我们应该注意不要在其实现上浪费太多时间。 一般来说,我们应该从项目开始就尝试将纹理集应用到中高质量的手机游戏中,并注意不要逾越目标平台对于纹理的限制,然后根据需要对每个平台和每个设备进行调整。另一方面,最简单的手机游戏极有可能在不需要任何纹理集的情况下运行。同时,仅当绘制调用次数超过合理的硬件预期时,我们才应考虑将纹理集应用于高质量的台式机游戏,因为我们希望许多纹理都可以保持高分辨率以实现最高质量。低质量的台式机游戏应该可以避免纹理集,因为绘制调用不太可能成为最大的瓶颈。 当然,无论产品是什么,如果受到绘制调用的 CPU 限制,并且在己经穷尽了许多替代技术的情况下也无法解决,那么纹理集将是下一个实现的最佳技术,因为如果使用得当,它可以节省大量的绘制调用。
-
调整非正方形纹理的压缩率 不建议将非正方形和/或非 2的幂 (Non-Power-Of-Two, NPOT)的纹理导入应用程序中,因为 GPU 通常会要求推送的纹理为正方形,而非2的幂的大小会导致不必要的工作负载(需要处理格式错误的图像纹理大小)。Unity 将自动调整纹理并添加额外的空自空间,以适应 GPU 期望的形状因子,这将导致额外的内存带宽成本,将本质上无用的数据推送至 GPU。
-
稀疏线理 稀疏线理 (Sparse Texture) 称为巨大纹理(Meea-Textures)或平铺纹理(Tiled-Textures ,它提供了一种在运行时有效地从磁盘流式传输纹理数据的方法。相对而音,如果CPU 以秒为单位执行额作,则磁盘将以天为单位进行操作。因此,通常的建议息,成不惜_切代价避免在游戏过程中进行硬盘访问,因为任何此类技术都有可能道成破盘访问量超过可用量的风险,从而导致应用程序停止运行。
但是,稀疏纹理则打破了这一规则,并提供了一些有趣的性能节省技术。稀疏线理的目的是将许多纹理组合成一个巨大的纹理文件,该文件太大而无法作为单个纹理文件加载到图形内存中。除了包含纹理的文件非常大(例如,分辦空为 32768×32768)并且包含相当多的细节 (每个像素32位)之外,这与纹理集的概念是类似的。这个思路是通行动态地手动选择纹理的小部分来 节省大量的运行时内存和内存带宽,并在游戏中需要它们之前将它们从磁盘中拉出。此技术的主要成本是文件大小要求(例如,在上述示例分辨率为 32768×32768 的情况下,文件将占用 4 GB 的磁盘空间!)。这项技术的其他成本则可以通过大量场景准备工作来克服。
-
程序材质 程序材质 (Procedural Maverials)也称为 Substance 村质,是一种在运行时通过将高质量的小型纹理样本与自定义数学公式相结合来生成纹理的方法。程应材质的目的是以初始化期间额外的运行时内存和 CPU 处理为代价,最大程度地减少应用程序的占用空间。
-
减少多边形数量
- 调整网格压缩 Unity 为导入的网格文件提供了 4 种不同的 Mesh Compression(网格压缩)设置:Off(关)、Low(低)、Medium(中)和High<高)。
- 正确使用“己启用读写”标志 Read Write Enabled(己启用读写)标志允许在运行时通过脚本或在运行时由 Unity自动对网格进行更改。在内部,这意味着它将原始网格数据保留在内存中,直到要复制它并动态更改为止。禁用此选项后,一旦 Unity 确定了要使用的最终网格,即可从内存中丟弃原始网格数据。 如果在整个游戏中只使用均匀缩放的网格版本,则禁用此选项将节省运行时内存,因为我们将不再需要原始的网格数据来进一步缩放网格的副本 (顺便说一下,这就是Unity 在动态批处理中,按缩放比例因子组织对象的方式)。因此,Unity 可以尽早丢弃这些不需要的数据,因为直到下一次启动应用程序时,我们再也不需要它了。 但是,如果网格经常以不同的比例在运行时重新出现,则 Unity 需要将该数据保留在内存中,以便可以更快地重新计算新的网格,因此应启用 Read-Write Enabled(己启用读写)标志。禁用它不仅需要 Unity 重新加载网格数据,还需要同时制作重新缩放后的副本,从而导致潜在的性能问题。 Unity 会尝试在初始化时检测此设置的正确行为,但是,当在运行时实例化网格并以动态方式缩放网格时,我们必须通过启用此设置来强制解决该问题。这将提高对象的实例化速度,但是会花费一些内存开销,因为原始的网格数据会一直保留。
-
仅导入/计算所需内容
这似乎是另一个明显的建议,但是网格不仅包含顶点位置数据。网格中可能还有着色器不需要的 Normals(法线)和Tangents(切线),或者也可能自动生成没什么用的Normals(法线)和Tangents(切线)坐标,尤其是在 Smoothing Angle(平滑角度)非常低的情况下。在这些情况下,每个顶点都需要多个法线向量来创建由此产生的多个面的平面阴影样式。
-
考虑烘焙动画
-
让Unity优化网格(Optimize Mesh选项)
-
合并网格(静态/手动)
Unity具有两个物理引擎:3D的Nvidia PhysX和2D的Box2D,但是在Unity上架最新的DOTS后将会使用最新的Unity Physic
-
场景设置
-
缩放比例 我们应该尽可能使游戏世界中所有物理对象的缩放比例保持在接近1:1:1的水平。这意味着默认的重力值为-9.81,世界的单位比例隐含为 1米/单位,因为地球表面的重力为9.81m/s(大多数游戏都在尝试模拟这个情况)。对象大小应该反映隐含的世界比例,因为将它们缩放太大会导致重力似乎使对象移动的速度比预期的要慢得多。反过来说也一样,缩放对象太小会使其掉落得太快并且看起来不太真实。
-
定位 同样,将所有对象保持在接近(0,0,0)的位置将导致更好的浮点精度,从而改善模拟的一致性。Space Simulator(中文版名称《空间模拟器》)和 Free-Running 游戏试图模拟难以置信的大空间,并且它们通常通过秘密传送(或简单地保持)玩家角色居于世界的中心而尽可能至地使用此技术。此时,要么移动其他所有物体以模拟行程,要么将家间体积分隔开,以便始终以按近零的值来计算物理。这样可以确保所有对象都保持接近(0.0.0),以避免在玩家行进很远的距离时出现浮点误差。
-
质量 Unity 说明文档建议将对象的mass(质量)值保持在 0.1 左右,且该值不超过 10,以防止对象产生不稳定性,其网址如下:http://docs.unity3d.com/ScriptReference/Rigidbody-mass.html这意味着我们不应以磅或千克等度量来考虑质量,而应以物体之间的相对值来考虑。我们应努力保持碰撞物体之间质量的比例一致、合理。由于大的动量差和最终的浮点精度损失,使得当碰撞物体的质量比大于 1000 时,最有可能导致不稳定的行为。我们应该尝试确保对象之问的碰撞发生于其质量属性值相似的对象,而具有显著比例差异的对象对则应该使用碰撞矩阵(稍后将对此进行详细介绍)进行剔除。
-
-
正确使用静态碰撞器
物理系统会自动从所有静态碰撞器 (不具有刚休对象的碰撞器)的数据生成数据结构,而不是使用结构管理动态碰撞器 (具有刚体对象的磁撞器)。糟糕的 是,如果在运行时将新对象引入数据结构,则必须重新生成它。这可能会导致严重的CPU峰值。因此,避免在游戏过程中实例化新的静态碰撞器至关重要。
此外,仅移动、旋转或缩放静态碰撞器也会触发此再生成过程,应避免使用。如果我们有碰撞器,但是希望在不对其他物体发生物理反应的情况下移动,则应附加一个刚体以使其成为动态碰撞器,并将 Kinematic(运动》标志设置为 true。该标志可防止对象对由对象间碰撞产生的外部冲力做出反应。这使它的行为类似于静态碰撞器,但是现在它处于可以正确支持移动对象的数据结构中;,因此可以从脚本代码中移动它(在固定更新期间),并将冲力施加到其他对象上。
-
优化碰撞矩阵
-
首选离散碰撞检测
-
修改固定更新频率
-
调整最大允许时间步长
如果 Maximum Alloved Timestep(最大允许时间步长)值被定期命中,则将导致莫些看起来很奇怪的物理行为。刚体对象在空间中似乎会变慢或冻结,因为物理引擎需要在完全解决其整个时间配额之前就保持退出状态。在这种情况下,很明显,我们需要从其他角度优化物理计算。但是至少我们可以确信,该阈值将防止游戏在物理处理峰值期间完全锁定。 开发人员可以通过 Edit(编辑)|Project Settings(项目设置)|Time(时间)|MaximumAllowed Timestep (最大允许时间步长)访问此设置。默认设置是最多消耗 0.333 s,如果该值被突破,则将表现为帧速率明显下降(仅3FPS)。如果开发人员觉得有必要更改此设置,那么很显然此时的物理工作量会遇到一些天问题,因此,建议仅在用尽丘有其他方法均无改善之后才调整该值。
-
最小化投射和包围体检查
-
避免使用复杂的网格碰撞器
-
避免复杂的物理碰撞组件
-
让物理对象睡眠 睡眠狀态的國值可以在Edit (编輯)|Projeet Settings (项目设置)|Physics(物理)|sleep Threshold(睡眠國值)下进行修政。 睡眠中的物理对象存在生成“孤岛”的危险。当大量的刚体对象相互接触并逐渐静止时,就产生了孤岛。想象一下,一堆盒子己经产生并形成了一个很大的孤岛。最终,一旦系统中失去足够的能量,所有的刚体对象都将入睡,它们全部静止。但是,由于它们仍然彼此接触,因此一旦唤醒这些对象之一,它们就会开始连锁反应,唤醒附近的所有其他刚体对象。突然之间,CPU 使用率急剧上升,因为现在有数十个对象重新进入了模拟,并且突然有更多的碰撞需要解决,直到对象再次睡眠。 在运行时更改刚体的任何属性mass(质量)、drag(拖动)、Use Gravity(使用重力)等,也会重新吹醒对象。如果定期更改这些值(例如,在菜一款游戏中,对象大小和质量会随时间交化),则它们保持活跃状态的时间将比平馆更长。在施加力时也是如此,因此,如果使用自定义重力解头方案(洋见 52!节“场跟设置”的“质量”都分中提出的建议),则应尝试避免在每次固定更新时都施加重力,否则该对象将无法睡眠∂
-
修改求解器迭代计数
求解器迭代计数可以在 Edit(编辑)| Project Settings(项目设置)|Physics(物理)|Solver Iteration Count (求解器迭代计数)下进行。在大多数情況下,完全可以接受 6次迭代(的默认值。但是,如果游戏中包含非常复杂的关节系统,那么开发人员可能会希望增加此数量,以抑制任何不稳定(或完全爆炸性)的角色关节行为,而某些项目则就算是减少该数量也没有影响。因此,更改此值后必须执行测试,以查看项目是否仍保持预期的质量水平。
-
优化布娃娃
- 减少关节和碰撞器
- 避免布娃娃之间的碰撞
- 禁止或者删除不活动的布娃娃
-
掌握使用物理引擎的时机 (能不用就不用)
-
细节级别 LOD
-
禁用GPU蒙皮
-
减少曲面细分
前端流程中还有最后一项任务也是需要仔细考虑的:曲面细分 (Tessellation)。通过几何着色器 (Geometry Shader)进行曲面细分可以带来很多乐趣,因为它是一种相对未得到充分利用的技术,它可以真正使游戏的图形效果在一大堆仅使用最常见效果的游戏中脱颖而出。当然,它的代价就是极大地增加前端处理的工作量。 除了改进曲面细分算法或减轻其他前端任务带来的负担,使曲面细分任务有更施展空间外,没有其他简单的技巧可用于改进曲面细分。无论哪种方式,如果前端有瓶颈并且使用了 曲面细分技术,则应仔细检查它们是否消耗了大部分的前端预算。
可以尝试以下两种强力测试:
- 降低分辦率
- 降低纹理质量。
这些变化将减轻管线后端两个重要阶段的工作量,它们分别是填充速率 (Fill Rate)和内存带宽 (Memory Bandwidth)。在现代图形渲染时代,填充速率往往是瓶颈的最常见来源。
-
填充速率
通过降低屏幕分辨率,开发人员可以要求光栅化系统生成明显更少的片段并将它们转置在较小的像素画布上。这将减少应用程序的填充速率消耗,为渲染管线的关键部分提供一些额外的施展空间。因此,如果由于屏幕分辦率降低而使游戏性能突然提高,那么填充速率应该是我们的首要考虑。 填充速率是一个非常宽泛的术语,是指 GPU 绘制片段的速度。但是,这仅仅包括在各种条件测试中幸存下来的片段 (我们可能会在给定着色器中启用这些测试)。片段仅仅是“潜在像素”,如果它未通过任何启用的测试,则将立即丢奔。这可能会极大地节省性能,因为管线可以跳过代价高昂的绘图步骤,立即开始处理下一个片段。 Z测试(Z-Testing)就是这样一个例子,它检查来自较近物体的片段是否己经被绘制到同一像素上。如果是这样,则当前片段将被丢弃;如果不是,则将片段推入片段着色器,并在目标像素上进行绘制,这将消耗填充速率中的一次绘制。现在想象一下,将这个过程与成千上万个重叠的对象相乘,每一次都会生成数百 个或数千个可能的片段。要实现高屏幕分辦率,则每一次和每一帧都会生成数百万或数十亿个片段。显而易见,尽可能多地跳过这些绘制将节省大量渲染成本。
-
过度绘制 通过使用加法 Alpha 混合和非常透明的平面颜色渲染所有对象,可以直观地表示我们有多少过度绘制。当同一像素被多次叠加混合绘制时,高过度绘制区域将更加明亮。这正是 Scene(场景)视图的 Overdraw(过度绘制)着色模式揭示场景过度绘制程度的方式。
-
遮挡剔除 减小过度绘制的最佳方法之一是利用 Unity 的遮挡剔除 (Occlusion Culling)系统。这与视锥体剔除 (Frustum Culling)技术不同,后者将剔除从当前相机视图不可见的对象。此功能在所有版本中始终处于活动状态,并且被此过程剔除的对象也将被遮挡剔除 (Occlusion Culling)系统自动忽略。
-
着色器优化
-
使用移动平台着色器
-
使用低精度数据
-
避免在重排时改变精度
重排(Swizzling)是着色器编程技术,它通过按照我们希望将它们复制到新结构中的顺序列出分量,从现有矢量创建新矢量 (值的数组)。以下是一些重排示例:
float4 input = float4 (1.0, 2.0, 3.0, 4.0); //初始化测试值 float2 vall = input. yz; //重排两个分量 float3 val2 = input.zyx; //按不同的顺序重排3个分量 float4 val3 = input. yyy: //多次重排相同的分量 float sclr = input.wi; float3 val4 = sclr.xxx; // 多次重排标量
-
使用GPU 优化的辅助函数 使用的 Cg 库两数示例包括:abs,用于绝对值;lerp,用于线性插值;mul,用于矩阵乘法;step,用于 step 判断。
-
禁用不必要的功能
-
删除不必要的输入数据
着色器是否真的需要多个通道、透明度、Z写入、Alpha 测试和 Alpha 混合吗?
-
仅公开必要变量
将不必要的变量通过着色器公开给随附的材质可能会很昂费,因为 GPU 无法假定这业值是常量。这意味着无法将着色器代码编译成更优化的形式。
-
降低数学复杂度
-
减少纹理查找 如果着色器正在跨多个纹理执行采样,甚至跨单个纹理执行多个采样时,这很可能会导致内存中的高速缓存未命中。
-
避免条件语句
当该系统遇到条件语句时,它无法独立解析这两个语句。
-
减少数据依赖 编译器将尽最大努力将着色器代码优化为对 GPU 更友好的低级语言,以便在处理其他任务时,它不必等待数据被提取。例如,在着色器中可能会出现以下不够优化的代码:
float sum = input.colorl.r; sum = sum + input.color2.g; sum = sum + input.color3.b; sum = sum + input.color4.a; float result = calculateSomething(sum)
如果能够迫使着色器编译器在写入代码时将此代码编译为机器代码指令,则该代码将具有数据依赖性,由于依赖于sum 变量,因此每次计算需要上一次完成才可以开始。但是,着色器编译器通常会检测到这种情况,并将其优化为使用指令级并行的版本。
-
使用表面着色器,简化优化方式
-
使用基于着色器的LOD
-
-
内存带宽
-
使用更少的纹理数据
-
测试不同的GPU纹理压缩格式
-
最小化纹理采样
-
组织资产以减少纹理交换
此方法又回到了批处理和纹理集。
-
-
关于VRAM限制
-
纹理预加载
常见方法为创建一个具有纹理的隐藏Game Object
-
纹理反复读写
在极少数情况下,会有太多的纹理数据加载到VRAM 中,而所需的纹理却并不存在,GPU 将需要从主内存中请求它,并覆盖现有的纹理数据以腾出空间。随着内存的碎片化,随着时间的流逝,这种情况可能会恶化,并带来以下风险:刚从 VRAM 冲刷掉的纹理需要在同一帧内再次拉出。这将导致严重的内存“反复读写”情况,所以应不惜一切代价避免。 在现代游戏机(例如 PS4、 Xbox One )上,这不再是一个问题,因为它们为CPU 和GPU 共享了一个公共的内存空间。考虑到设备始终运行单个应用程序,并且几乎始终渲染3D图形,因此该设计是硬件级别的优化。但是,所有其他平台必须与多个应用程序共享时间和空间,并且需要能够在没有 GPU 的情况下运行。因此,它们具有独立的CPU 和GPU 内存,开发人员必须确保在任何给定时刻的总纹理使用量保持在目标硬件的可用 VRAM 以下。
-
-
照明优化
-
使用适当着色模式
正向着色为主,延时着色为辅,虽然延时着色只需要通过一遍照明着色器就可以完成大量照明,大大降低总体Draw Call,但是如果场景使用高级功能如阴影、抗锯齿、透明就只能使用正向着色,且延时着色只适合Shader Model3.0的GPU.
-
使用剔除遮罩
-
使用烘培光照贴图
-
优化阴影,硬>软
-
-
为移动设备优化图形
-
尽量减少绘制调用
-
尽量减少材质数量
-
最小化纹理大小和材质数量
与台式机 GPU 相比,大多数移动设备的纹理缓存都非常小。例如,由于 iphone 3G运行的 OpenGLES 1.1 仅使用简单的顶点渲染技术,因此它只能支持 1024×1024 的总纹理大小。而记iphone 3G3、iPhone 4 和iad这一代的产品运行的是 OpenGLES 2.0,它仅支持最大 2048×2048 的纹理。下一代产品则可以支持最大 4096×4096 的纹理。开发人员应仔细检查要针对的设各硬件,以确保它支持要使用的纹理文件大小 (目前市面上在售的Android 设备已经非常多)。当然,下一代设备从來都不是移动市场上最常见的设备。如果希望自己的游戏能够吸引更多的玩家(增加成功的机会),那么开发人员就必须愿意支持较弱的硬件。
-
使用正方形和2的幂纹理
-
在着色器中使用最低精确度格式
-
避免进行Alpha测试
-
-
值类型和引用类型
栈是本着先进后出的数据结构(LIFO)原则的存储机制,它是一段连续的内存,所以对栈数据的定位比较快速, 而堆则是随机分配的空间, 处理的数据比较多, 无论如何, 至少要两次定位。堆内存的创建和删除节点的时间复杂度是O(logn)。栈创建和删除的时间复杂度则是O(1),栈速度更快。 那么既然栈速度这么快,全部用栈不就好了。这又涉及到生命周期问题,由于栈中的生命周期是必须确定的,销毁时必须按次序销毁,从最后分配的块部分开始销毁,创建后什么时候销毁必须是一个定量,所以在分配和销毁时不灵活,基本都用于函数调用和递归调用中,这些生命周期比较确定的地方。相反堆内存可以存放生命周期不确定的内存块,满足当需要删除时再删除的需求,所以堆内存相对于全局类型的内存块更适合,分配和销毁更灵活。
结构体是值类型,字符串是引用类型;字符串的连接操作,会复制一份,所以建议使用String Builder进行优化
-
数据布局的重要性
我们希望将大量的引用类型分组与大量的值类型分组保持分离。如果在值类型中甚至只有一种引用类型(例如结构体),则垃圾收集器会将整个对象及其所有数据成员视为间接可引用的对象。当需要进行标记和扫描时,它必须在继续操作之前验证对象的所有字段。但是,如果将各种类型分成不同的数组,则可以使垃圾收集器跳过大部分数据。
public struct MyStruct{ int myInt: float myFloat: bool myBool; string myString; } MyStruct[] arrayOfStructs = new MyStruct[1000]; // 优化后如下 int[] myInts = new int[1000]; float[] myFloats = new float[1000]; bool[] myBools = new bool[10001; string[] myStrings = new string[1000];
-
关于Unity API
Unity API 中有一些指令会导致堆内存分配,这是我们应该注意的,这实际上包括返回数据数组的所有内容,例如,以下方法将在堆上分配内存:
GetComponents<T> () ; Mesh.vertices; Camera.allCameras;
应该尽可能避免使用此类方法,或者至少应调用一次并进行缓存,这样才不会导致不必要的内存分配。
-
关于foreach循环
Unity C#代码中实现的许多foreach循环在这些调用期间会导致不必要的堆内存分配,因为它们会将 Enumerator对象作为堆上的类而不是栈上的结构进行分配。这完全取决于给定集合的 GetEnumerator方法的实现。事实证明,在Unity 附带的 Mono 版本(Mono 版本2.6.5)中实现的每个单个集合都将创建类而不是结构,这将导致堆分配。这包括但不限于 List 、LinkedList 、Dictionary <K,V>、ArrayList 等。但是,请注意,在典型数组上使用 foreach 循环实际上是安全的!Mono 编译器会将数组上的 foreach 秘密转换为简单的for 循环。
-
协程
开始启动协程时需要花费少量的内存,但是请注意,使用该方法时不会产生更多的开销。如果内存消耗和垃圾回收是重要的问题,则应尝试避免存在太多短暂的协程,并避免在运行时调用 StartCoroutine过多。
-
闭包
闭包 (Closure)是有用但很危险的工具。匿名方法和lambda 表达式并不总是闭包,但它们可以是闭包,这完全取决于该方法是否使用其自身范围和参数列表之外的数据。
-
关于.NET库函数
LINQ和正则表达式消耗大量性能
-
临时工作缓冲区
如果我们习惯于将大型临时工作缓冲区 (Temaporary Work Buffer) 用于一项或多项任务,则应该寻找重用它们的机会,而不是一遍又一遍地重新分配它们,这很有意义,因为这样可以减少分配中涉及的开销以及垃圾回收 (所谓的“内存压力”)值得一提的是,可能需要将这种功能从特定于案例的类提取到通用的上帝类中,该上帝类包含用于多个类重用的较大工作区域。
-
对象池