[Unity]Optimize Your Mobile Game Performance中文版

写在最前,能力有限,翻了个大概,轻喷。有兴趣和能力的同学还是建议看英文原版。

原版链接

提取码:p9fu

注:标黄部分是由于能力有限,实在难翻,大家可以参考英文原文做一个理解。

顺便吐槽下:如果直接复制整个word文档在博客发布页,图片会一直卡在“正在上传中...”,只能一张一张去复制。


分析工具Profiling

Unity自带工具:

Profiler

开启Profiler,并运行时执行Profiler:

Build时勾选Development.Build、Autoconnect.Profiler

扫描二维码关注公众号,回复: 16765808 查看本文章

特定场景修改帧数:

Unity>Preferences>Analysis>Profiler>Frame Count 

默认300,可以最大提高至2000,以应用于不同的调试优化场景。(同时这个数值越高,越耗费cpu以及内存) 

Profiler视图:

Hierarchy 

它支持根据耗时来对采样进行排序。同时也可以计算计算当前帧函数调用的次数和托管堆内存(GC.Alloc)

Timeline 

它显示了特定帧时间的可视化分解。可视化显示不同活动进程以及跨线程之间的关联。使用它来确定时CPU绑定还是GPU绑定。

在开始对你的项目进行性能分析前,先保存Profiler .data 文件。然后待做出相应优化操作之后,将修改前的.data文件和修改后的进行对比:配置-优化-对比。如此反复,以优化性能。

分析和优化要尽早的、频繁的

要在项目初期、项目出问题时,尽早的,时刻的进行性能分析及优化,而不是在快上线时。

不要盲目地优化

不要去猜测和假设是xx原因影响的游戏性能,要及时地使用Unity Profiler和相应平台的性能分析工具,去准确发现和定位,究竟是什么原因造成的性能缓慢的问题。

Profile on the target device 

Unity Profiler 无法收集移动平台引擎所有的部分,不过iOS和Android都有自己平台对应的性能优化工具。

iOS:Xcode/Instruments
Android:Android Studio/Android Profiler 

某些平台独有的性能分析工具:

比如,Arm Mobile Studio,Intel VTune,Snapdragon Profiler。

Profile Analyzer 

这个工具可以收集多个Profiler的数据,然后对优化前和优化后的数据进行对比和定位,从而对我们感兴趣的某一帧去加以分析和优化。算是对Profiler的一种补充和扩展,可以看到更详细的帧数据。这个工具可以从Unity的Package Manager可以获取到。

每帧特殊时间预算

理论上30fps(frames per second )每帧耗时为33.33ms(1000 ms / 30 fps )。60fps每帧耗时为16.66 ms 。

但是在移动平台上,不能一直使用这个时间,因为会导致设备过热,CPU、GPU降频。

确定是GPU绑定还是CPU绑定

Gfx .WaitForCommands标识:渲染线程已就绪,主线程阻塞

Gfx .WaitForPresent标识:主线程已就绪,GPU阻塞

关注设备温度

过热会直接影响性能。

在低端设备上测试性能

向下兼容,在最低兼容的目标设备上测试性能

推荐观察顺序:渲染—>内存(GC)—>代码脚本


内存(Memory)


Unity使用自动内存管理来管理用户生成的代码和脚本。较小数据块,像值类型的局部变量,被分配在栈上。较大数据块和持久数据存储,分配在托管堆上。

垃圾回收器(GC)回定期识别和释放未使用的堆内存。当垃圾回收操作自动运行时,会在堆内存中检查所有对象是否被使用,而这一操作会导致游戏卡顿或者性能缓慢。

优化内存的使用意味着我们要知道什么时候分配和释放内存,并将垃圾回收带来的影响降至最低。

参考Unity托管堆介绍:

Unity - Manual: Managed memory

Memory Profiler 截图比较

Memory Profiler 的使用

这个单独的组件可以对内存托管堆(managed heap memory )进行截图(可以在Package Manager中预览和下载),从而帮你对片段(fragmentation )和内存泄露等问题进行精准定位。

在Tree Map视图中点击一个变量可以定位到一个在内存中保存的本地对象。你可以在这里看到常见的内存消耗的问题,比如非常大的纹理或重复的assets资源。

学习如何使用Memory Profiler提高内存使用效率:

https://www.youtube.com/watch?v=I9wB4Cvgz5g

或者参考Unity官方文档:

Memory Profiler | Memory Profiler | 0.2.10-preview.1
 

减少垃圾回收的影响 (GC)

Unity使用的是贝姆垃圾收集器(Boehm-Demers-Weiser garbage collector),它会停止运行你的程序代码只有当它执行结束之后才会恢复程序的正常运行。which stops running your program code and only resumes normal execution when it has finished its work

贝姆垃圾收集器参考文档:A garbage collector for C and C++

需要注意某些不必要的堆分配,它们可能会导致GC峰值:

字符串(Strings):

在C#里,字符串类型是引用类型,并不是值类型。

减少不必要的string类型的创建和操作。

尽量避免解析基于字符串的数据文件,比如JSON和XML;可以把数据储存在ScriptableObjects或者用MessagePack 、Protobuf等格式来代替JSON、XML。

Unity自带函数的调用:

需要注意的是一些函数会创建堆分配。

缓存一个数组的引用,而不是在循环中重复的对其进行操作。

此外,尽量使用已有的方法去实现我们想要的需求。比如,使用GameObject.CompareTag(string)方法来判断物体的Tag,而不是直接去比较一个字符串和GameObject.tag(因为这样会返回一个新创建的string类型的垃圾)

装箱(Boxing):

避免值类型变量和引用类型变量之间的传递。因为这个操作会产生一个临时的对象,带来潜在的垃圾。比如强制将值类型转换为对象类型(int i = 123;object o = i)。

协程(Coroutines):

尽管yield不会产生垃圾,但是创建一个新的WaitForSeconds对象时会产生垃圾。所以尽量缓存和重复使用WaitForSeconds对象,而不是在每次yield时去创建它。

语言集成查询和正则表达式(LINQ and Regular Expressions):

当性能出现问题时应尽量避免使用LINQ和Regular Expressions,因为这两者底层都在进行装箱操作,会产生垃圾。

可能的话,定时回收垃圾

如果确定在某个特殊的时间点回收垃圾不会对你的游戏造成影响,你可以手动调用System.GC.Collect来进行垃圾回收。可参考《理解自动管理内存》来知道你可以在什么时候做些什么是对自己有利的:https://docs.unity3d.com/Manual/performance-memory-overview.html

使用增量垃圾回收器incremental GC来缓解垃圾回收工作负载

对于程序的运行来讲,增量垃圾回收器是多个的、阻塞时间更短的(much-shorter interruptions),在许多帧上分发执行的,以此来缓解垃圾回收的工作负载。而不是单个的,长阻塞的(long interruption)。

如果垃圾回收影响到了应用的性能,可以开启这个选项,看看是否可以显著地降低垃圾回收峰值。可以通过Profile Analyzer来验证此举对你的应用程序性能是否是有提升的。

 使用增量垃圾回收器来减少GC峰值


自适应性能(Adaptive Performance)

有了Unity和三星的自适应性能后,你可以通过监测设备的温度和电量来确保你可以有备无患地进行相应的调整。比如当用户玩了很长时间

之后,你可以动态地降低游戏的细节层次(LOD),来确保你的游戏可以流畅地运行。自适应性能允许开发者在保持游戏效果的同时,以一个可控的方式去提高游戏的性能。

你可以通过自适应性能的相关APIs去调整你的应用程序,同时它也提供了一些自动模式。在这些模式里面,自适应性能通过以下几个关键性的指标来确定游戏的设置:

-基于先前帧的期望帧速率(Desired frame rate based on previous frames)

-设备温度水平(Device temperature level)

-设备接近热事件(Device proximity to thermal event )

-CPU或GPU绑定的设备Device bound by CPU or GPU

这四个指标metrics决定了设备的状态自适应性能通过调整优化相应设置来减少性能瓶颈这是通过提供一个称为索引器Indexer的整型值用来描述设备的状态。(These four metrics dictate the state of the device, and Adaptive Performance tweaks the adjusted settings to reduce the bottleneck This is done by providing an integer value, known as an Indexer, to describe the state of the device)

需要注意的是自适应性能只支持三星设备 

你可以通过选择Package Manager > Adaptive Performance > Samples来查看由Package Manager提供的例子来学习更多自适应性能相关的内容。每一个例子都受特定的变量(scaler)所影响,所以你可以看到不同的变量是如何影响到你的游戏的。我们也建议通过学习回顾最终用户文档(End User Documentation)来学习更多的自适应性能的配置和如何正确的使用其API。

Adaptiveperformance Samples

Using Adaptive Performance samples | Adaptive Performance | 2.1.1

End User Documentation

Adaptive Performance user guide | Adaptive Performance | 2.1.1


编程和代码架构Programming and code architecture

Unity的PlayerLoop包含与游戏引擎核心相交互的功能。这个树状结构包含许多系统——处理初始化相关、每帧的update。你所有游戏相关脚本的运行都依赖PlayerLoop。

当你在profiling视图内去看的时候,你会发现你项目里的所有用户代码,都在PlayerLoop下方(Editor相关的脚本在EditorLoop下)。

用户脚本、设置、渲染相关内容会非常显著地影响到每帧的计算和最终渲染在屏幕上的时间 

你可以通过以下的提示和技巧来优化你的脚本。

充分理解Unity PlayerLoop

首先确保你已经理解了Unity在每帧循环时代码的执行顺序。每个Unity的脚本里面的一些函数都会按照既定的顺序去执行。你应该清楚地理解和明白在一个脚本里,AwakeStartUpdate以及其他的一些方法在整个被创建的生命周期之内的区别。

关于脚本整个生命周期内函数的执行顺序,可以参考“脚本生命周期流程图”:

Unity - Manual: Order of execution for event functions

避免每帧去执行大量的代码

一段代码是否真的需要每帧都去执行,这是一件值得深思熟虑且慎重的事情。可以将不必要的代码从UpadateLateUpdateFiexdUpdate里面移出来。可以将必须要每帧执行的代码放在这几个方法里面,但是尽量避免放入一些并不需要频繁去更新的逻辑。可能的话,只在发生变化的时候,去执行相应的逻辑。

layerLoop生命周期图

如果确实需要使用Update函数,可以考虑每n帧来执行你的代码。这是一种常见的通过应用时间分割(time slicing)方法来多帧分散代码工作负载的技术。下面这个例子,就是每3帧执行一次ExampleExpensiveFunction方法:

private int interval = 3;

void Update()

{

    if (Time.frameCount % interval == 0)

    {

          ExampleExpensiveFunction();

    }

}

避免在Start/Awake函数里写大量的逻辑

当你的第一个场景被加载时,每一个object上的这些函数都会被调用:

 Awake

— OnEnable

— Start

避免在你的程序第一帧被渲染出来之前,在这些函数内写大量耗费时间的逻辑。否则,你将会耗费大量不必要的加载时间。

可以参考以下链接来了解第一个场景加载时的详细过程:

Unity - Manual: Order of execution for event functions

避免空的Unity方法事件

即使是一个空的MonoBehaviours,它也会占用请求资源,所以你应该删除空的Update或LateUpdate等方法。

如果要使用这些方法来测试某些功能,请使用预处理指令(preprocessor directives):

#if UNITY_EDITOR

Void Update()

{

}

#endif

这样的话,你就可以随意地使用Update来测试你的功能,同时不用担心打包时会产生不必要的开支。

删除Debug Log

大量的Log(尤其是在Update,LateUpdate,FixedUpdate里的)会影响程序的性能。可以在打包前禁用不必要的Log信息。

可以用一个条件属性(Conditional Attribute)和一个预处理指令来很方便的实现这个需求。比如像下面这样自定义一个类:

public static class Logging

{

  [System Diagnostics Conditional(“ENABLE_LOG”)]

  static public void Log(object message)

  {

      UnityEngine Debug Log(message);

  }
}

 

添加一个预处理指令来区分你的脚本 

使用你自定义的类来打印log。当你需要禁用所有log信息时,只需要在Player Settings里删除这个预处理指令,你所有的log信息分分钟就全部消失了。

使用hash值来代替string参数

Unity底层并没有使用string类型的名称来处理比如Animator,Material,Shader等属性。为了更好的效率,所有的属性名称使用的都是一个hash IDs,这些IDs事实上都是用来处理这些属性的。

无论在什么时候在Animator,Material或Shader上使用Set或者Get方法,请使用参数为整型值的方法来替代参数为字符型的方法。因为参数为字符型的方法会将string类型转为hash类型然后再把hash ID传递给参数为整型的方法。(The string methods simply perform string hashing and then forward the hashed ID to the integer-valued methods)

使用Animator.StringToHash来获取Animator的属性名称,使用Shader.PropertyToID来获取材质球或者Shader的属性名称。

选择正确的数据结构

每帧都会有成千上万的数据不停地在迭代,而你所选择的数据结构可能会在数以万计地迭代时,对性能造成不小的影响。所以你的集合中究竟应该使用列表(List),数组(Array),还是字典(Dictionary),这个选择变得尤为重要。在C#中你可以参考MSDN guide to data structures这个通用教程,来选择合适自己的正确的数据结构。

避免在运行时添加组件components

在运行时调用AddComponent函数时会带来不小的消耗。因为不管在什么时候,当你在运行时去调用这个方法,Unity都会去检查当前Object上面是否有重复的component或其他必须的(required)component。

通常情况下,直接去实例化一个已经绑定好我们所需要的component的预制体,这样可以节省更多性能方面的开销。

对游戏物体Gameobjects和组件components进行缓存

GameObject.FindGameObject.GetComponent,和Camera.main(在2020.2之前的版本)方法非常耗费性能,所以应该避免在Update中去调用这些方法。你可以在Start的时候,去调用这些方法,并缓存这些实例。

比如下面这个重复调用GetComponent的反面例子:

void Update()
{
 	Renderer myRenderer = GetComponent<Renderer>();
 	ExampleFunction(myRenderer);
}

相反的,你可以只调用一次GetComponent方法,然后对其返回值进行缓存。然后便可以在Update中重复使用这个返回值,而不用每次去调用GetComponent方法。

private Renderer myRenderer;
void Start()
{
 	myRenderer = GetComponent<Renderer>();
}
void Update()
{
 	ExampleFunction(myRenderer);
}

使用对象池

对象的实例化(Instantiate)和销毁(Destroy)通常是一件会带来垃圾并产生垃圾回收峰值(spikes)的缓慢的进程。我们可以使用对象池去预加载一些可能会被重复使用或者可回收的对象,而不是频繁地去实例化和销毁这些对象(比如从枪里发射的子弹)。

在这个例子中,对象池创建了20个可被重复使用的PLayerLaser实例

在CPU比较空闲时去实例化一些可被重复使用的实例。然后将这些对象储存在一个集合内。当游戏运行的时候,我们仅仅需要从中取出一个当前可以使用的对象,然后去启用(enable)它。在我们不需要它的时候去禁用(disable)它,并将其返回对象池,而不是去销毁它。

这样做可以减少项目中内存的托管分配(managed allocations),进而避免垃圾回收带来的问题。

 对象池中所有的PlayerLaser对象都是未激活的且随时可以被发射的

可以参考这个文档来学习如何创建一个简单的对象池系统:

Introduction to Object Pooling - Unity Learn

使用ScriptableObjects

将固定的值或者设置储存在ScriptableObjects里,而不是MonoBehaviourScriptableObject是一个只需要设置一次的项目内的资产(asset)。它不能直接和GameObject产生关联。

ScriptableObject里创建一个字段去存储你的数值或者设置,然后在你的MonoBehaviour里面去引用他们。

在这个例子中,名为Inventory的ScriptableObject保存着各种GameObject的设置 

使用这些ScriptableObject内的字段可以尽可能的避免你每次从Monobehaviour实例化对象时,产生些不必要的重复的数据。

可以通过《Introduction to ScriptableObjects》这个视频教程,来了解ScriptableObjects是如何帮助优化我们的项目。同样你也可以通过下面这个文档来学习了解相关知识:

Unity - Manual: ScriptableObject


项目配置(Project configuration)

下面这些项目设置会影响到你的手机性能。

降低或者禁用加速计频率(Accelerometer Frequency)

Unity每秒会多次刷新你手机的加速计(Accelerometer)(Unity pools your mobile’s accelerometer several times a second)。为了更好的性能表现,如果你的游戏并不需要Accelerometer,可以选择禁用这个属性,或者降低它的频率。

如果你的游戏没有用到Accelerometer Frequency,请确保你禁用了这个属性

禁用一些不必要的Player或者Quality设置

Player Settings里对不支持的平台(platforms)禁用Auto Graphics API以减少生成过多shader变种(variants)。假如你的应用不支持那些老旧的CPU,那么请禁用目标平台的架构(Target Architectures)。

Quality设置里,禁用那些不需要的Quality levels。

禁用不必要的物理效果(physics)

假如你的游戏不需要用到物理效果,那就取消勾选自动模拟(Auto Simulation)和自动同步转换Auto Sync Transforms。这些设置将会降低你应用程序的运行效率,有弊无利。

选择正确的帧率

移动端游戏的帧率必须在设备的电量以及热量之间做一个平衡或者取舍。而做为一种妥协,你可以考虑将你的游戏限制在30帧,而不是60帧。在移动端,Unity默认的设置是30帧。

你可以在运行时通过Application.targetFrameRate来动态地调整你的帧率。比如,你甚至可以在某些缓慢或者静态的场景里,将帧数限制在30帧以下。然后在玩家操作时提高你的帧率。

避免过于复杂的层级结构(hierarchies)

简化你的层级结构!假如你的GameObjects不需要嵌套层级,那么尽量简化它们的嵌套关系。在你的场景里较小的层级结构得益于多线程刷新TransformsSmaller hierarchies benefit from multithreading to refresh the Transforms in your scene)。而复杂的层级结构将会带来更多的层级转换,同时产生很多不必要的计算量和更多的垃圾回收。

可以参考层级优化(Optimizing the Hierarchy)和Unite talk主题演讲来对Transforms有一个更好的了解和联系。

变换(Transform)一次而不是两次

同样的,当我们去移动Transform的时候,可以用Transform.SetPositionAndRotation来同时刷新物体的位置和旋转。这样可以避免由于两次刷新Transform而带来的不必要的开销。

假如你需要在运行时去实例化一个GameObject,一个简单而有效的优化方法是,在实例化时直接设置它的父物体,和它的位置,旋转信息:

GameObject Instantiate(prefab, parent);

GameObject Instantiate(prefab, parent, position, rotation);

可以参考Scripting API来了解更多的实例化Object相关的信息。

如果开启了垂直同步(Vsync)

移动端不支持渲染半帧(half-frames)。即使你在Editor(Project Settings > Quality)内禁用了垂直同步,垂直同步在硬件级上还是被启用的。假如CPU刷新速度跟不上的话当前帧将会被挂起进而有效地降低你的帧率(If the GPU cannot refresh fast enough, the current frame will be held, effectively reducing your fps)。


资源(Assets)

资源管理操作(asset pipeline)会极大程度地影响你应用的性能。一个经验丰富的技术美术(technical artist)可以帮你的团队去定义和要求资源的格式,参数和导入设置。

不要依赖默认设置。请使用不同平台重写标签来对纹理,几何网格等资源来进行对应地优化。不当的设置将会产生更大的打包尺寸,更多的打包时间,和更多的内存占用。考虑使用一些预置功能来对特定的项目制定一个基准设置以达到最优化设置(Consider using the Presets feature to help customize baseline settings for a specific project to ensure optimal settings)。

可以参考这个文档来更好地练习对美术资源的管理:

https://docs.unity3d.com/Manual/ImportingAssets.html

或者在Unity Learn网站上学习《移动应用上3D美术资源的优化》(3D Art Optimization for Mobile Applications

正确地导入纹理

纹理资源很可能占用大量的内存空间,所以纹理的导入设置是非常值得去考究的。通常情况下,你可以参考如下的建议:

更小的Max Size在效果可以接受的情况下,使用最低的设置。这么做可以无损且非常高效地降低你的纹理内存(texture memory)。

使用二次幂POT):Unity要求移动端纹理尺寸为二的次幂(POT)的压缩格式(PVRCT或ETC)。

图集的使用将多个纹理打成一个纹理可以减少draw call,提升渲染效率。可以使用Unity SpriteAtlas或者第三方插件Texture Packer来打包图集。

禁用Read/Write Enabled选项当这个选项被启用时会在CPUGPU内复制两份内存地址会导致纹理所占内存大小的翻倍(When enabled, this option creates a copy in both CPU- and GPU-addressable memory, doubling the texture’s memory footprint)。在大多数情况下,禁用这个选项。假如你需要在运行时去生成纹理,可以通过Texture2D.Apply这个方法来强制生成,然后将makeNoLongerReadable参数设置为true

禁用不需要的Mip Maps有些纹理在屏幕上的大小尺寸始终不会改变,比如2D sprites和UI图形,这些是不需要MipMaps的(在3D物体的纹理上开启这个选项,以确保相机位置发生改变时,它的显示效果不同)。

正确地导入设置可以帮助优化你的打包尺寸

压缩纹理

下面这个例子中两个模型使用了相同的纹理。但是左边的纹理设置在内存占用上几乎是右边的8倍,但是在显示效果上,并没有太大的差别。

未压缩的纹理将会占用更多内存

在iOS和Android上使用自适应缩放纹理压缩(Adaptive Scalable Texture Compression(ATSC))。绝大多数游戏的开发都会针对最低设备适配支持ATSC压缩(The vast majority of games in development target min-spec devices that support ATSC compression)。

以下特殊情况:

处理器为A7及以下的iOS设备(比如iPhone5,5S等)使用PCVRTC格式

2016年之前的Android设备使用ETC2((Ericsson Texture Compression)格式

如果PVRTC或者ETC压缩质量不够高,或者你的平台设备不支持ASTC格式,可以尝试使用32-bit的纹理来代替16-bit。

更多关于不同平台纹理压缩格式相关的信息可以参考以下文档:

Unity - Manual: Recommended, default, and supported texture formats, by platform

调整网格导入设置

像纹理、网格等资源如果导入时相关设置未加斟酌,将会增加大量的内存占用。最小化网格资源带来的内存占用:

压缩网格主动压缩网格可以减小硬盘空间的占用(但是运行时的内存不受影响)。需要注意的是网格的压缩和不同参数会影响网格最终的效果,你需要做的是通过测试不同的压缩等级,来找到一个适合你模型的等级。

禁用读写(Read/Write):开启这个选项之后会在内存中复制两份网格出来,一份保存在系统内存中,一份保存在GPU内存中。在大多数情况下,请禁用这个选项(在Unity 2019.2及更早的版本中,这个选项是默认打开的)。

禁用rigs和融合变形(BlendShapes):假如你并不需要骨骼动画和融合变形(BlendShapes)动画,不管在什么时候,请尽量地去禁用这些选项。

如果可能请禁用法线和切线如果你确定网格的材质不需要法线和切线,请禁用它以节省一些不必要的开销。

检查你的网格面数(polygon counts)

模型的精度越高,意味着更大的内存和GPU的使用率。你的背景需要50万的几何面数去渲染么?(Does your background geometry need half a million polygons?)考虑在DDC package中减少你的模型数(Consider cutting down models in your DCC package of choice)。删除一些相机视角渲染不到的面数。使用纹理和法线贴图来表达模型细节层次,而不是用高精度的网格。

使用AssetPostprocessor来自动化你的导入设置

AssetPostprocessor允许你在导入资源时运行脚本。它允许你在导入模型,纹理,音频等资源之前或之后去自定义一些设置。

使用可寻址资源系统(Addressable Asset System)

可寻址资源系统(Addressable Asset System)提供了一个便捷的方式用来供管理你的资源,可以通过“地址”(address)或者别名(alias)去加载AssetBundles。这个统一(unified)的系统,可以从本地或者远程(CDN)异步加载资源。

假如你将非代码资源(模型,纹理,预制体,音频,甚至一个完整的场景)打包成一个AssetBundles你可以把他们当作单独的资源从下载的内容中获取(you can separate them as downloadable content (DLC))。

然后使用Addressable 给你的移动应用创建一个小的初始化构建云端内容交付功能Cloud Content Delivery可以让你将游戏内容托管交付至云端然后玩家从云端获取他们游戏的实时进度(Then, use Addressables to create a smaller initial build for your mobile application.Cloud Content Delivery lets you host and deliver your game content to players as they progress through the game.)。

在可寻址资源系统(Addressable Asset System)中使用地址(adress)加载资源 

点击了解可寻址资源系统(Addressable Asset System)是如何管理资源的。


图形和GPU的优化

Unity决定每一帧必须去渲染哪些物体,然后生成draw call。一次draw call是去调用图形绘制API绘制图形(比如三角形),而批处理(batch)则是将一组draw call放在一起去执行。

随着项目越来越复杂,你需要特定的流程(pipeline)去优化GPU上面的工作负荷。通用渲染管道Universal Render Pipeline (URP)目前使用的是单通道向前渲染的方式为你的移动端带来更高的图形效果延迟渲染技术可能会在将来的版本中得已实现)(The Universal Render Pipeline (URP) currently uses a single-pass forward renderer to bring high-quality graphics to your mobile platform (deferred rendering will be available in future releases))。同样的在主机和PC上面基于灯光和材质的一些效果也可以应用拓展至手机和平板(The same physically based Lighting and Materials from consoles and PCs can also scale to your phone or tablet)。

下面这些教程可以帮助你提升你的图形效率

批处理draw call(Batch your draw calls)

把将要一起绘制的物体合批处理可以最小化因为绘制物体产生的draw call数量。这样做可以减少CPU在渲染物体中产生的消耗,从而提升性能。在Unity中使用以下技术可以合并多个物体从而使draw call降低:

动态合批(Dynamic batching):对于较小的网格,Unity可以在CPU上对顶点进行分组,转换,然后一次性绘制他们。请注意:仅仅在你有足够多的低多边(low-poly)形网格时使用这个功能(顶点属性低于900,顶点数低于300)。动态批处理不会合并比这些顶点数多的物体,所以启用它会消耗CPU的时间,因为CPU需要每帧去查找较小的网格。

静态合批(Static batching):对于一些静态的物体,Unity可以对那些使用了相同材质的物体进行批处理,减少draw call。静态合批效率优于动态合批,但是它会使用更多的内存空间。

GPU实例化(GPU instancing):假如你有大量的完全一样的物体它会使用图形硬件技术来批处理这些物体(If you have a large number of identical objects, this technique batches them more efficiently using graphics hardware)。

SRP Batching:在你的Universal Render Pipeline Asset下面的Advanced菜单中,开启SRP Batcher。这个选项可以加速你的CPU的渲染时间,具体取决于你的场景。

利用这些批处理技术来调整你的GameObjects 

使用帧调试器(Use the Frame Debugger)

帧调试器(Frame Debugger)显示了每帧是如何由不同的draw call构成的。这是一个可以监测你的shader属性,帮你分析你的游戏是如何渲染的非常有用的工具。

可以通过以下文档来了解Frame Debugger有哪些新功能:

Working with the Frame Debugger - Unity Learn

避免使用过多的动态光

相较于传统的前向渲染器(forward renderer),通用渲染管线(URP)减少了大量的draw call数量。避免在你的移动应用中添加过多的动态光。可以考虑在动态网格中使用自定义shader效果或者光照贴图(light probes),或者在静态网格中烘焙光照。

可以通过这个对比表,来查看实时光照在通用渲染管线(URP)和内置渲染管线(Built-in Render Pipeline)中的不同之处:

Feature comparison table | Universal RP | 10.4.0


帧调试器将每帧分成单独的步骤

禁用阴影

阴影投射将会在MeshRenderer和灯光(light)中被禁用。所以不管什么时候,可以的话,请禁用阴影,以减少draw call数量。

你可以将一个使用了模糊纹理的简单的网格或者面片(quad)放在角色的下方,来伪装一个阴影。或者使用自定义shader来创建一团阴影。

禁用阴影投射(Cast Shadows)来减少draw call

将光照烘焙至Lightmaps

使用全局光(Global Illumination (GI))给你的静态几何体(geometry)添加绚丽的灯光。使用 Contribute GI属性来标记物体,就可以用Lightmaps的形式储存高质量的光照信息。

在运行时对已烘焙的阴影和灯光进行渲染时,不会对性能造成影响。更好的CPU和GPU可以加速全局光的烘焙。

开启Contribute GI

调整Lightmapping设置(Windows > Rendering > Lighting Settings)和Lightmap尺寸来限制内存占用大小

可以参考这个引导手册和这篇关于光照优化的文章来帮助你在Unity中着手使用Lightmapping。

使用灯光层级(Light Layers)

对于使用了多种复杂灯光的场景来说,可以使用不同层级来区分你的所有物体,然后通过遮罩剃除(culling mask)功能将不同的灯光所带来的影响限制在不同的范围内。

层级功能可以将不同的灯光所带来的影响限制在不同的范围内

在移动物体上使用光照探针(Light Probes)

光照探针储存着场景中空白空间所烘焙的光照信息并提供了高质量的光照效果(直接和间接的)。他们使用了球谐函数(Spherical Harmonics),计算速度比动态光照要快很多。

光照探针在后台照射动态物体(Light Probes illuminate dynamic objects in the background)

使用细节层次LOD

随着物体的移动,LOD功能可以为它们切换更简单的网格,使用更简单的材质和shader,有助于提高GPU的性能。

通过在Unity Learn网站学习《Working with LODs》课程来了解更多细节。

一个使用了LOD Group的网格实例 

 

相同模型在不同分辨率下的网格顶点差异

使用遮挡剔除(Occlusion Culling)功能来移除隐藏的物体

被隐藏在别的物体后面的一些物体,可能依旧在被渲染,同时耗费资源。使用遮挡剔除功能来忽视(discard)他们。

相机视野外的视锥剔除是自动化的,遮挡剔除是一个烘焙进程。只需要将你的物体设置为静态遮挡器(Static Occluders)或遮挡(Occludees),然后通过Window > Rendering > Occlusion Culling对话框去烘焙它。尽管这并不适合所有的场景,但在大部分情况下使用剔除(culling)可以提高性能。

点击Working with Occlusion Culling教程了解更多相关信息。

注意移动端原生分辨率

手机和平板变得越来越先进,分辨率也变得越来越高。

使用Screen.SetResolution(width, height, false)来降低输出的分辨率以此提高一些性能。配置多个分辨率用来平衡游戏的画面和性能。

限制相机数量

每个相机都会带来一些性能开销,不管它是否在执行一些有意义(meaningful)的工作。仅使用渲染所必需的相机组件(Only use camera components necessary for rendering)。在一些低端设备上,每个相机可能会占用CPU1ms的时间。

尽量避免复杂的着色器shader

通用渲染管线(URP)已经提供了一些自发光和无光照shader,并且针对移动平台做了优化。尽量减少shader的变化,因为它会对运行时内存的使用产生巨大的影响。假如通用渲染管线提供的shader没办法满足你的需求,可以使用Shader Graph来自定义shader。点击这里查看如何使用Shader Graph可视化地生成shader。

使用Shader Graph创建自定义shader

减少overdraw和透明混合(alpha blending)

避免绘制不必要的透明的或者半透明的图片。overdraw和透明混合会严重影响移动端平台的性能(Mobile platforms are greatly impacted by the resulting overdraw and alpha blending)。不要重叠几乎透明的图片或者特效。你可以使用 RenderDoc图形调试器来检查你的overdraw。

减少使用屏幕后处理

全屏的屏幕后处理效果会极大程度地降低游戏性能。在制定美术效果时请谨慎使用屏幕后处理特效。

在移动应用中使用简易的后处理效果

谨慎使用Renderer.material

在脚本中使用Renderer.material会复制并返回一份新的材质球(material)。这将会影响已经进行了批处理的内容,包括材质球。假如你想访问批处理对象的材质球,请使用Renderer.sharedMaterial来代替Renderer.material

优化SkinnedMeshRenderers

渲染蒙皮网格(skinned meshes)是非常消耗性能的。确保你每个使用了SkinnedMeshRenderer的物体都是真的需要它。假如你的GameObject只在某些时间需要用到动画(animation),可以在静态的动作下使用BakeMesh函数去冻结蒙皮网格,然后在运行时切换至比较简化的 MeshRenderer(If a GameObject only needs animation some of the time, use the BakeMesh function to freeze the skinned mesh in a static pose, and swap to a simpler MeshRenderer at runtime)

减少反射探针(Reflection Probes)的使用

Reflection Probe可以创建一个真实的反射效果,但是这可能会造成一个非常大的消耗。可以使用低分辨率的立方图(cubemaps),遮罩剃除(culling mask),和纹理压缩(texture compression),来提高运行时效率。 


用户界面(User interface)

UnityUI(UGUI)通常也是产生性能问题的一个来源。Canvas组件上的UI元素生成和更新网格,给GPU发送drawcall指令。这个过程往往比较耗费性能,所以在使用UGUI时,请注意以下几点:

精简你的Canvas

假如你有一个包含成千上万元素的巨大的Canvas,更新单独的UI元素时将会强制更新整个Canvas,很可能会产生CPU峰值。

可以利用Unity支持多个Canvas的这个功能。然后基于刷新频率的不同,来对你的UI元素进行分类。将静态UI元素放在一个Canvas下,同时创建些比较小的子Canvas(sub-canvases),用来存放那些动态的UI元素。

确保每一个Canvas上的所有UI元素使用相同的Z值,材质球,和纹理贴图。

隐藏不可见的UI元素

在你的游戏中,可能会有一些UI元素只是偶尔的显示那么一下(比如血条,只在玩家受伤的时候出现一次)。假如这些不可见的UI元素依旧是活跃状态(is active),那么它还是会产生draw call。所以直接禁用(disable)所有不可见的UI元素,直到再次显示时再启用(re-enable)。

如果你需要隐藏Canvas,那么请禁用Canvas组件,而不是禁用整个GameObject。这样可以避免网格和顶点的重绘。

减少使用GraphicRaycasters禁用Raycast Target

像触摸和点击屏幕等输入事件的检测,需要GraphicRaycaster组件。这是通过检测屏幕上每一个输入点,来检测是否在UI的RectTransform上。

在hierarchy面板中,移除Canvas上默认的GraphicRaycaster组件。然后在需要交互的元素(比如button,scroll rects等)上单独的添加GraphicRaycaster组件。

禁用 Ignore Reversed Graphics选项,它默认是启用的

同样的,在所有不需要Raycast Target的text,image上禁用这个选项。在一个由复杂的元素组成的UI上面,任何细小地改动,都会引起一些不必要地计算。

如果可能,请禁用Raycast Target

谨慎使用Layout Groups组件

Layout Groups更新效率比较低,所以谨慎使用这个组件。如果你的内容并不是动态的,那么不要使用这个组件,可以用锚点比例布局来代替它(Avoid them entirely if your content isn’t dynamic, and use anchors for proportional layouts instead)。或者当Layout Group对UI布局设置完成之后,在代码里去禁用它。

假如你真的需要Layout Groups (Horizontal, Vertical, Grid)组件来管理你的动态元素,那么不要嵌套他们以提升性能。

Layout Groups会影响性能,尤其是嵌套使用时

尽量少的使用庞大的列表(List)和网格(Grid)视图

过于庞大的列表和网格视图是十分耗费性能的。假如你需要制作一个庞大的列表或者网格视图(比如一个由数以百计的商品组成的库存页面),可以复用一个小的UI元素对象池,而不是为每一个商品新建一个UI元素。可以参考这个例子来深入了解。

避免大量重叠元素

大量UI元素分层(layering)过多(例如在纸牌游戏中大量纸牌堆叠)会产生overdraw。自定义你的代码以便在游戏运行时去动态的将UI元素合并到更少的层级和批次。

使用多种分辨率(resolution)和纵横比(aspect ratio)

目前很多移动设备使用的分辨率和屏幕尺寸完全不同,为不同的设备提供不同版本的UI(alternate versions of the UI)以便每一台设备都有最好的用户体验。

使用设备模拟器(Device Simulator)来预览大部分已支持的设备上UI的显示效果。你也可以在XCodeAndroid Studio上创建虚拟设备。

使用设备模拟器预览各种屏幕格式

当使用全屏UI时隐藏其他所有东西

假如你的暂停页面和开始页面覆盖住了场景中所有的东西,那么在场景中禁用3D相机对其渲染。同样的,禁用所有隐藏在最顶部Canvas后面的所有Canvas元素。

考虑当屏幕显示全屏UI的时候,降低刷新率Application.targetFrameRate,因为这个页面并不需要60fps去更新。

Canvas的Render Mode选择World Space和Camera Space时记得分配相机

如果没有给Event Camera或者Render Camera赋值的话,Unity会强制将Camera.main赋值给他,这样会产生不必要的性能代价。

如果允许的话,Canvas的Render Mode可以考虑使用Screen Space–Overlay,这个模式不需要单独指定相机。

当使用Space Render Mode时,请确保给Event Camera指定了对应的相机 


音频Audio

尽管音频资源通常并不是性能的瓶颈,但是你可以通过优化它来节省内存。

尽可能地使用单声道音频

假如使用3D空间音频,最好将你的音频设置为单声道或者启用Force To Mono选项。一个用于定位空间的多声道音频会在运行时被强行转换成单声道音源,这个过程将会增加CPU和内存的占用。

尽可能地使用原始的未经压缩的WAV格式文件做为你的资源

假如你使用了一些压缩格式(例如MP3,Vorbis),Unity会在打包时对其进行解压-二次压缩。而这两个过程将会导致最终的音频效果不尽人意。

压缩音频降低比特率

通过压缩来降低音频大小和内存占用率:

——尽量使用Vorbis格式(或者对于一些不会循环播放的音频,使用MP3格式)

——对于一些简短的,且频繁使用的音频,可以使用 ADPCM格式(比如脚步声,射击声)。这个压缩格式和未经压缩的PCM相比,在播放时的解码速度更快。

移动端音效资源的采样率最多不要超过22050hz。使用更低的音频设置通常不会特别影响最终的音频质量,可以用你的耳朵去感受一下。

针对AudioClip的导入设置来进行优化

选择合适的加载类型

这个设置因资源大小而异。

——较小尺寸的音频(小于200kb)应该选择Decompress on Load。这会将音频解压至原始的16-bit PCM格式数据,将会增加CPU和内存的占用,所以这个选项只适用于简短的音频。

——中等尺寸的音频(大于等于200kb)应该选择Compressed in Memory

——较大尺寸的音频(背景音乐)应该设置为 Streaming。否则,整个资源将会一下子加载到内存中。

从内存中卸载静音的音频(AudioSources)

实现按钮这个功能时,不要直接将音量调整为0。你可以销毁(DestroyAudioSource组件,将它从内存中卸载,前提是玩家不会经常去切换(静音/非静音)这个选项。


动画(Animation)

Unity的动画系统是非常先进的。如果可能的话,使用以下设置来对移动端动画进行限制。

使用通用型而不是人型(Use generic versus humanoid rigs)

默认情况下,向Unity导入动画模型时会被设置为通用型(Generic Rig),但是当使用的是角色动画时,开发者通常会将它设置为人型(Humanoid Rig)。

相较于通用型,人型动画将会多增加30-50%的CPU计算时间,因为它每帧都要计算反向动力学和动画重定向,尽管你并没有使用到它。假如你并不会使用到人型(Humanoid Rig)动画里的这些属性,那么请选择将动画设置为通用型(Generic Rig)。

避免过度使用动画控制器Animator

Animator主要是用来驱动人型角色的,但是经常被用来控制单个值的变化(比如一个UI元素的透明通道)。过度使用Animator,特别是与UI元素进行结合的使用。尽可能的在移动端上使用旧版动画(legacy Animation)组件。

可以考虑使用补间动画或者使用第三方插件来实现简单的动画效果(比如DOTween)。

Animator可能会很耗费性能


物理(Physics)

Unity内置的物理系统(Nvidia PhysX)在移动设备上非常的耗费性能。参考以下技巧可以帮你节约一些帧数。

优化你的设置

在PlayerSettings里,尽可能的检查一下Prebake Collision Meshes这个选项。

启用Prebake Collision Meshes

确保设置过你Physics settings (Project Settings > Physics)。尽可能简化Layer Collision Matrix

禁用Auto Sync Transforms,启用Reuse Collision Callbacks

修改物理设置来提升性能 

密切关注Profiler上的Physics module,以了解性能问题

简化碰撞盒(Simplify colliders)

网格碰撞盒(Mesh collider)是很耗费性能的。用更简单的碰撞盒来代替复杂的碰撞盒来趋近原始的形状(Substitute more complex mesh colliders with simpler primitive or mesh colliders to approximate the original shape)

使用原始的或者简易的网格碰撞盒

用物理方法移动刚体(Rigidbody)

MovePosition或者AddForce这些类方法来移动刚体。直接移Transform组件将会导致物理世界的重新计算,这在比较复杂的场景中是相当耗费性能的。在FixedUpdate中而不是在Update中移动物体。

修改Fixed Timestep

项目设置里默认的Fixed Timestep是0.02(50Hz)。按照你的目标帧率去修改这个设置(比如30fps则设置为0.03)。

否则,假如你在运行时掉帧,那就意味着Unity会在一帧里多次调用FixedUpdate方法,很可能会产生由庞大的物理带来的性能问题(potentially creating a CPU performance issue with physics-heavy content)。

Maximum Allowed Timestep这个设置限制了当掉帧时,可以进行可多少次物理计算和FixedUpdate事件。降低这个值意味着在性能下降时,物理和动画将会变得缓慢,但是可以减少每帧他们所带来的影响。

修改Fixed Timestep来适配你的目标帧率,降低Maximum Allowed Timestep来减少性能问题

可视化的物理调试器(Visualize with the Physics Debugger)

使用Physics Debug窗口(Window > Analysis > Physics Debugger)来排查并解决碰撞和差异问题。这个窗口可视化颜色编码的形式显示了GameObject可以和哪些对象发生碰撞。

物理调试器很直观地显示出了你的物理对象可以和哪些对象发生交互碰撞

更多相关信息,可以参考Unity官方文档:Physics Debug Visualization
 


工作流和协同合作(Workflow and collaboration)

在Unity中打包一个工程是一件很庞大的事情,通常会涉及到很多开发者。确保你的工程设置对于你的团队来讲已经是最优解。

使用版本控制

每个人都需要使用某种类型的版本控制。确保你的编辑器设置(Editor Settings)的Asset Serialization Mode已经设置为Force Text

假如你正在使用一个第三方版本控制工具(比如Git),那么请确保Version Control设置中的Mode已经设置为Visible Meta Files

Unity已经内置了一种标记语言(YAML,一种可读的,序列化语言)工具用来合并场景和预制体。更多相关信息,可以参考Unity官方文档:Smart Merge

版本控制工具是团队工作中所必不可少的东西。它可以帮助我们来定位bug,找到出问题的版本。遵循良好的开发流程,比如使用分支(branch)和标签(tag)来管理项目的版本和发布。

Plastic SCM,Unity官方推荐的适用于游戏开发的一款版本控制工具。

分割较大的场景

太大,且单个的场景不太利于协作开发。可以将你的关卡分割成一些较小的场景以便美术人员和设计人员更加方便地去协作开发,同时这样做也会将产生冲突的可能性降至最低。

在运行时,你可以使用SceneManager.LoadSceneAsync通过添加LoadSceneMode.Additive参数来加载场景。

移除未使用的资源

需要注意一些与第三方插件和库捆绑在一起的未使用的资源。其中包括一些用来测试的资源和脚本,假如你没有手动移除它们,它们可能会被打入包内。应在最初的时候就移除那些没有使用到的资源。

使用Unity Accelerator加速分享

Unity Accelerator是一个协作(Collaborate)服务的代理和缓存,它可以让你更快的共享Unity编辑器的内容。假如你的团队是在一个本地网络下工作的,你不需要重新构建你的工程部分,显著地减少了下载时间。当使用Unity Teams Advanced时,加速器也可以共享资源。


Unity成功案例Unity Integrated Success

讲的似乎是和Unity官方合作的成功案例。

有兴趣的可以访问以下地址了解:

Unity Integrated Success

联系表格


总结(Conclusion)

你可以通过Unity Blog,或者在Unity社区Unity学习论坛上使用#unitytips标签来获取更多额外的优化方面的知识,经验技巧,和相关新闻。

性能优化是一个很广泛的话题。了解你的移动设备硬件是如何工作的,以及它的性能局限性。为了找到一个对与你的项目来说是最佳的有效的解决方案,你需要去了解并学习Unity官方的类、组件、算法、数据结构和你对应平台的性能分析工具。

当然了,你的创造力同样也不可小觑。

猜你喜欢

转载自blog.csdn.net/qq302756113/article/details/123872531