Unity性能优化
一、Profile的使用
- CPU消耗量
- 渲染信息和GPU信息
- 运行时的内存分配和总消耗量
- 音频源/数据的使用情况
- 物理引擎(2D/3D)的使用情况
- 网络消息传递和活动的情况
- 视频回放的使用情况
- UI性能
- 全局光照统计数据
- 验证目标脚本是否出现在场景中
- 验证脚本的在场景中出现的次数是否正确
- 验证事件的正确顺序
- 最小化正在进行的代码更改
- 减少内部干扰
- 减少外部干扰
- 可以采用IDE调试或Debug.Log,但是注意Unity中日志记录在CPU和内存中都非常昂贵,因此不要Debug很频繁,应该只针对代码中最相关的部分进行有针对性的记录,并且对调试的Debug及时注释。
- Unity的Update()次数即使在相同的硬件上,也可能次数不同,例如在这一秒调用60个更新,而在下一秒有可能则调用59个更新,在下一秒可能调用62个更新,因此,程序最好不要依赖于某个对象Update()的特定调用次数。
- 如果一帧需要很长时间处理,比如程序出现明显的卡顿,那么Profiler可能无法获取结果记录在Profiler窗口中。
- 程序如果启动了垂直同步(Vsync,用于将应用程序的帧率匹配到它将显示到的设备帧率,例如显示器的帧率为60Hz,而程序的渲染循环比60Hz快,则程序会等到,指导输出渲染为止),在Profiler中可能会在WaitForTargetFPS下的“CPU使用情况”区域中产生过多嘈杂峰值,因此在查看监视CPU使用情况时,可以先禁用VSync,具体在
Edit|Project Settings|Quality
中禁用VSync。
- 脚本代码控制Profiler
- 自定义定时和日志记录
1.4.1 Profiler脚本控制
private void DoSomething()
{
Profiler.BeginSample("Test Profiler Sample");
var lt = new List<string>();
for (int i = 0; i < 10000000; i++)
{
lt.Add(i.ToString());
}
Profiler.EndSample();
}
1.4.2 自定义CPU分析
using System;
using System.Diagnostics;
/// <summary>
/// 自定义方法测试定时器
/// </summary>
public class CustomTestTimer : IDisposable
{
private string _timerName;//计时器名称
private int _numTests;//测试次数
private Stopwatch _wathc;//计时器
public CustomTestTimer(string timerName,int numTests)
{
_timerName = timerName;
_numTests = numTests;
if (numTests <= 0)
_numTests = 1;
_wathc = Stopwatch.StartNew();
}
public void Dispose()
{//当引用using()块结束时调用
_wathc.Stop();
float ms = _wathc.ElapsedMilliseconds;
UnityEngine.Debug.LogFormat("{0} 测试完成,总计用时:{1:0.00}ms,每次测试平均用时:{2: 0.000000}ms,一共测试{3}次",_timerName,ms,ms / _numTests,_numTests);
}
}
int numTests = 100000;
using (new CustomTestTimer("Controlled Test",numTests))
{
for (int i = 0; i < numTests; i++)
{
TestFunction();
}
};
private void TestFunction()
{
Debug.Log("123");
}
二、脚本策略
void Update()
{
DoSomething();
}
private float _delayTime=0.2f;
private float _timer=0;
void Update()
{
_timer+=Time.deltaTime;
if(_timer>_delayTime)
{
DoSomething();
_time-=_delayTime;
}
}
void Start()
{
StartCoroutine(DoSomethingCoroutine());
}
IEnumerator DoSomethingCoroutine()
{
while(true)
{
DoSomething();
yield return new WaitForSeconds(_delayTime);
}
}
- 与标准函数调用相比,启动携程会带来额外开销成本(大约是标准函数调用的三倍),同时还会分配一些内存,将当前状态存储在内存中,直到下一次调用它。而且这种开销也不是一次性的,因为协程会不断调用yield,这会一次又一次的造成相同的开销成本,所以需要确保降低频率的好处大于此成本;
- 协程运行独立于MonoBehaviour组件中Update()回调的触发,不管组件是否禁用,都将继续调用携程;
- 协程会在包含它的GameObject变成不活动的那一刻自动停止(无论该GameObject被设置为不活动还是它的一个父对象被设置为不活动),且如果GameObject再次被设置为活动,协程也不会自动重新启动;
- 将方法转换为协程,可减少大部分帧中的性能损失,但如果方法体的单次调用突破了帧率预算,则无论该方法的调用次数再怎么少,都将超过预算,因此这种方法只适用于由于在给定帧中调用方法次数过多而导致帧率超出预算的情况,而不适合由于原方法本身太昂贵的情况;
- 协程较难调试,它不遵循正常的执行流程,且在调用栈上没有调用者。如果使用协程,最好使它们尽可能简单,且独立于其他复杂的子系统。
void Start()
{
InvokeRepeating("DoSomething",0f,_delayTime);
}
-
if(gameObject!=null){
//DoSomething
}
if(!System.Object.ReferenceEquals(gameObject,null)) {
//DoSomething
}
-
GameObject listItem = (GameObject)Instantiate(Resources.Load("Prefabs/UI/Items/PersonListItem"));
listItem.transform.SetParent(m_PersonSelectContnt,false);
GameObject listItem = (GameObject)Instantiate(Resources.Load("Prefabs/UI/AMMT/Items/PersonListItem",m_PersonSelectContnt,false));
private bool _positionChanged;
private Vector3 _newPosition;
public void SetPosition(Vector3 pos)
{
_newPosition = pos;
_positionChanged = true;
}
private void FixedUpdate()
{
if (_positionChanged)
{
transform.position = _newPosition;
_positionChanged = false;
}
}
private void Start()
{
int numTests = 1000000;
using (new CustomTestTimer("向量在中间",numTests))
{
for (int i = 0; i < numTests; i++)
{
Func1();
}
}
using (new CustomTestTimer("向量在最后",numTests))
{
for (int i = 0; i < numTests; i++)
{
Func2();
}
}
}
private void Func1()
{
Vector3 a = 3 * Vector3.one * 2;
}
private void Func2()
{
Vector3 a = 3 * 2 * Vector3.one;
}
private void Start()
{
int numTests = 10000000;
using (new CustomTestTimer("大循环在外",numTests))
{
for (int i = 0; i < numTests; i++)
{
for (int j = 0; j < 2; j++)
{
int k = i * j;
}
}
}
using (new CustomTestTimer("大循环在内",numTests))
{
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < numTests; j++)
{
int k = i * j;
}
}
}
}
三、批处理
- 避免使用大量很小的网格。当不能避免需要使用很小的网格时,可以考虑是否可以合并网格。
- 避免使用过多的材质。尽量在不同的网格之间共用同一材质。
- 在运行时生成(批处理是动态生成的)
- 批处理中包含的对象在不同帧之间可能有所不同,这取决于哪些网格在主相机视图中当前是可见的(批处理的内容是动态的)
- 在场景中运动的对象也可以批处理(对动态对象有效)
- 所有网格实例必须使用相同的材质引用(此处为“材质引用”,如果两个不同的材质但它们设置相同,渲染管线还是不能执行动态批处理);
- 只有ParticleSystem和MeshRenderer组件进行动态批处理。SkinnedMeshRenderer组件(用于角色动画)和其他可渲染的组件类型不能进行批处理;
- 每个网格至多有300个顶点;
- 着色器使用的顶点属性(例如顶点位置、发现向量、UV坐标、颜色信息等等)不能大于900;
- 所有网格示例要么使用等比缩放,要么使用非等比缩放,但不能两者混用;
- 网格实体应该引用相同的光照纹理文件;
- 材质的着色器不能依赖多个过程;
- 网格实体不能接受实施投影;
- 整个批处理中网格索引的总数有上限,这与所用的Graphics API和平台有关,一般索引值在32~64K之间。
- 网格实体必须标记为Static,其副作用即是任何想要使用静态批处理的对象都不能通过任何方式移动、旋转和缩放;
- 每个被静态批处理的对象都需要额外的内存;
- 合并到静态批处理中的顶点数量是有上限的,与Graphics API和平台的不同而不同,一般为32~64K个顶点;
- 网格实例可以来自任何模型,但是它们必须使用相同的材质引用。
四、艺术资源优化
-
-
-
-
-
-
五、内存管理
- 托管域,该域是Mono平台工作的地方,我们编写的任何MonoBehaviour脚本和自定义的C#类在运行时都会在此域实例化对象 ,托管域的内存空间会被自动被垃圾回收管理。
- 本地域,该域我们会间接与之交互。Unity一些底层代码由C++编写,并根据目标平台编译到不同的应用程序中。该域关系Unity内部内存空间的分配,如为各种子系统(如渲染管线、物理系统、UI等)分配资源数据(如纹理、网格、音频等)合内存空间。此外,它还包括GameObject和Component等重要游戏对象的部分本地描述,也是大多数内建Unity类(如Transform、RigidBody等)保存数据的地方。
- 外部库,例如DirectX和OpenGL库,也包括项目中很多自定义的库和插件。
-
-
- 如果提前大致知道字符串的最终大小,那么可以使用StringBuilder类提前分配一个适当的缓冲区来存储或修改字符串。
- 如果不知道结果字符串的最终大小,使用StringBuilder可能不会生成大小合适的缓冲区,因此可以采用string.Format()、string.Join()和string.Concat()。
-
-
foreach(Transform child in transform){
//Dosomething with 'child'
}
for(int i=0;i<transform.childCount;i++){
var child = transform.GetChild(i);
//Dosomething with 'child'
}
六、程序设置
-
Read/Write Enabled
:如果不需要运行时读取图片的像素信息的话,禁用,否则启用后纹理的内存消耗会增加一倍。
-
Generate Mip Maps
:Mipmaps和模型的LOD类似,会根据相机距离远近降低或提升贴图像素,但是会多出三分之一的内存开销,如果不是模型贴图,则可以禁用,此外,UI的贴图基本用不到,可以禁用。
-
Max Size
:视情况而定,在2019.4版本Unity中最大可以达到8192*8192,但一般不要过大,否则贴图单个文件大小过大。
Model
-
Mesh Compression
:压缩比越高模型文件越小,需要根据项目实际效果决定,我们项目目前都将其设为Off
。
-
Read/Write Enable
:如果不需要修改模型时,可以禁用,否则启用后模型内存消耗会增加一倍。但是注意,之前也说过,由于项目中使用了Runtime Editor插件,与该插件需要配合的模型要将此项启用。
-
Optimize Mesh
:默认Everything,可以提升GPU性能。
-
Normals
:如果模型没有法线信息,可以将其设置为None
,减小模型大小。
Rig
-
Animation Type
:如果模型没有动画,将其设置为None
。
-
Optimize Game Objects
:在使用Animator制作动画时,将该项启用,可以将暴露在Hierarchy的子节点移除,极大减少了模型的层级和Children的数量,从而提升运行时的性能。如有挂节点需求,在Extra Transform to Expose
中添加需要暴露的子节点即可。
Quality
-
-
-
-
-
-
-
-
-
Async Upload Time Slice
:该参数设定渲染线程中每帧上传纹理和网格数据所用的时间总量,以毫秒为单位。当异步加载操作时,该系统会执行两个该参数大小的时间切片,默认值为2毫秒。如果该值太小,可能会在纹理/网格的GPU上传遇到瓶颈。如果该值太大,可能会造成帧率陡降。
-
Async Upload Buffer Size
:该参数设定环形缓冲区的大小,以MB为单位。当上传时间切片在每帧发生时,要确保在环形缓冲区有足够的数据利用整个时间切片。如果环形缓冲区过小,上传时间切片会被缩短。该值默认为4MB,可适当提高至16MB。
-
Async Upload Persistent Buffer
:该选项决定在完成所有待定读取工作时,是否释放上传时使用的环形缓冲区。分配和释放该缓冲区经常会产生内存碎片,因此通常将其设置为True。如果需要在未加载时回收内存,可以将该值设为False。
- 关于AUP的其他内容可参考以下网站内容:优化加载性能:了解异步上传管线AUP。
Player Settings
-
Scripting Backend
:可以选IL2CPP
,转成C++代码后性能得到提升,同时也变相提供了C#代码的混淆。
-
C++ Compiler Configuration
:默认选择Release
,如果发布的话,可以改成Master
,这样打包速度虽然会慢一些,但是编译的C++代码会更加优化一些。
-
Prebake Collision Meshes
:启用,用构建的时间换运行时的性能。
-
Keep Loaded Shaders Alive
:启用,因为Shader的加载和解析很耗时,所以不希望Shader被卸载。
-
Optimize Mesh Data
:启用,减少不必要的Mesh数据,降低包的大小。