赞
踩
Update() 、LateUpdate() 和其他Unity自带事件函数看起来像简单的函数,但它们具有隐藏的开销。这些函数每次调用引擎代码和托管代码时都需要通信。除此之外,Unity 在调用这些功能之前进行了一些安全检查。安全检查可确保 GameObject 处于有效状态、未销毁等。对于任何单个调用,这种开销不是特别大,但当项目中空Update()的脚本数量增加到数百或数千时可能就会特别浪费。为了避免浪费 CPU 时间,我们应该确保我们的项目脚本中不包含空Update()。
考虑到性能上最少的执行就是最大的节约,可以使用如下伪代码:
- void Update()
- {
- if(!someVeryRareCondition)
- return;
- foobar()
- }
如果我们的游戏具有大量有内容的 Update()脚本时,我们可以通过将这些脚本的Update()内容附加到全局单例中。通过全局单例管理这些 Update、LateUpdate 和其他回调。这还有一个额外的好处,就是允许代码在回调时取消订阅,从而减少每帧必须调用的函数的绝对数量。
- public event Action UpdateEvent;
- void Update()
- {
- UpdateEvent?.Invoke();
- }
如果代码需要频繁运行并且无法由事件触发,但并不需要运行每一帧。在这些情况下,我们可以选择每x帧运行一次代码。如下使用取模运算符来确保 Expensive 函数每3帧运行一次。
- private int interval = 3;
- void Update()
- {
- if(Time.frameCount % interval == 0)
- {
- ExampleExpensiveFunction();
- }
- }
这种技术的另一个好处是,很容易将昂贵的代码分散到单独的帧中,从而避免峰值。在下面的示例中,两个函数分别为每3帧和2帧调用一次,并且从不在同一帧上调用。
- private int interval1 = 3;
- private int interval2 = 2;
- void Update()
- {
- if(Time.frameCount % interval1 == 0)
- {
- ExampleExpensiveFunction();
- }
- else if(Time.frameCount % interval2 == 0)
- {
- AnotherExampleExpensiveFunction();
- }
- }
减少 Update() 中生成的垃圾的另一种技术是使用计时器。这适用于当我们的代码必须定期运行,但不一定是逐帧运行时。
在下面的示例代码中,函数每帧运行一次:
- void Update()
- {
- ExampleExpensiveFunction();
- }
在下面的代码中,我们使用计时器来确保的函数每秒运行一次。
- private float timeSinceLastCalled;
- private float delay = 1f;
- void Update()
- {
- timeSinceLastCalled += Time.deltaTime;
- if (timeSinceLastCalled > delay)
- {
- ExampleExpensiveFunction();
- timeSinceLastCalled = 0f;
- }
- }
对频繁运行的代码进行这样的小更改可以大大减少产生的垃圾量优化性能。
如果我们在代码反复调用返回结果,然后丢弃这些结果,这是非常浪费的,可以通过存储这些结果的引用优化性能。此技术称为缓存。在 Unity 中,通常使用 GetComponent()来访问组件。在下面的示例中,我们在Update()中调用GetComponent()来访问 Renderer 组件,然后再将其传递给另一个函数。此代码没错,但由于重复 GetComponent()调用,因此效率低下。
- private float timeSinceLastCalled;
- private float delay = 1f;
- void Update()
- {
- timeSinceLastCalled += Time.deltaTime;
- if (timeSinceLastCalled > delay)
- {
- ExampleExpensiveFunction();
- timeSinceLastCalled = 0f;
- }
- }
下面的代码只调用 GetComponent()一次,因为该函数的结果被缓存。缓存的结果可以在 Update()中重复使用,而无需再次调用 GetComponent()。
- private Renderer myRenderer;
- void Start()
- {
- myRenderer = GetComponent<Renderer>();
- }
- void Update()
- {
- FooBar(myRenderer);
- }
我们应该检查我们的代码,在以防频繁调用返回结果的函数。通过使用缓存,我们可以降低这些调用的成本。
Find() 和相关功能的API非常强大,但成本很高。这些函数需要 Unity 迭代内存中的每个 GameObject和组件。它们在小型简单项目中影响不是特别大,但随着项目复杂性的增长,使用成本会不断增加。 最好不要经常使用Find()和类似的函数,并尽可能缓存结果。尽可能使用Inspeactot面板设置对象的引用,或者创建专门查找物体的脚本来帮助我们减少在代码中使用Find()。
Camera.main 是一个方便的 Unity API,它返回第一个启用的主摄像机的引用。它看起来像是个变量,但实际上Camera.main与Find()一样,它搜索内存中的所有游戏对象的tag,使用成本非常很高。 为了避免这种潜在的昂贵调用,我们应该缓存Camera.main的结果,或者完全不用它,手动管理对摄像机的引用。
类似Camera.main,Unity还为很多数据类型提供了“简单”常量。但是,鉴于上述情况,必须注意这些常量,通常这些API返回的都是new对象,例如Vector3.zero和Quaternion.identity的属性内容如下:
- get { return new Vector3(0,0,0); }
- get { return new Quaternion(0,0,0,1); }
虽然访问这些属性的成本并不算高,但当它们每帧执行数千次(或更多次)时,就会对性能存在一定的影响,所以对于简单的属性类型,请改用const。const 在编译时会变成内联值类型,即对const变量的引用将替换为值类型,所以不建议长字符串或其他大型数据类型使用const修饰,否则,由于最终二进制代码中会存在重复数据,将导致不必要地增加代码文件的大小。
当const不适合时,应使用static readonly修饰。在项目中开发中,可以将Unity的内置简单属性替换成static readonly变量,使性能略有提升。
- static readonly Vector3 ZeroVector3 = new Vector3(0, 0, 0);
- static readonly Quaternion IdentityQuaternion = new Quaternion(0, 0, 0, 0);
因为 Unity 必须创建实例来管理协程的类,所以调用 StartCoroutine() 会产生少量垃圾。为了减少以这种方式创建的垃圾,任何必须在性能关键时间运行的协程都应提前启动,并且在使用可能包含对StartCoroutine() 的延迟调用的嵌套协程时应特别小心。
协程中的yield语句本身不会创建堆分配;但是,我们在yield语句中传递的值可能会创建不必要的堆分配。例如,下面的代码创建垃圾:
yield return 0;
此代码创建垃圾,因为值0是的int类型的装箱。在这种情况下,如果我们希望简单地等待帧而不导致任何堆分配,最好的方法是使用以下代码:
yield return null;
协程的另一个常见错误是,多次使用new实例化相同值。例如,以下代码将在每次循环迭代时创建并释放 WaitForSeconds 对象:
- while (!isComplete)
- {
- yield return new WaitForSeconds(1f);
- }
如果我们缓存并重用 WaitForSeconds 对象,则产生的垃圾要少得多。下面的代码将此作为示例进行演示:
- WaitForSeconds delay = new WaitForSeconds(1f);
- while (!isComplete)
- {
- yield return delay;
- }
对于位于紧凑循环中的矢量和四元数运算,请记住整数运算比浮点数学运算快,而浮点运算比矢量、矩阵或四元数运算更快。
因此,每当交换或关联运算时,请尝试最小化单个数学运算的成本:
Vector3 x;
int a, b;
// 效率较低:产生两次矢量乘法
Vector3 slow = a * x * b;
// 效率较高:一次整数乘法、一次矢量乘法
Vector3 fast = a * b * x;
创建新集合会在堆上出现内存分配。如果我们发现在代码中多次创建新集合,则应缓存对集合的引用,并使用 Clear()清空其内容,而不是重复调用new。
在以下示例中,每次使用 new 时都会发生新的堆分配。
- void Update()
- {
- List myList = new List();
- PopulateList(myList);
- }
在下面的示例中,仅当创建集合或在后台必须调整集合大小时,才会发生分配。这大大减少了产生的垃圾量。
- private List myList = new List();
- void Update()
- {
- myList.Clear();
- PopulateList(myList);
- }
对函数的引用(无论是引用匿名方法还是命名方法)都是 Unity 中的引用类型变量。它们将导致堆分配。将匿名方法转换为闭包方法(匿名方法在创建时可以访问作用域中的变量)会显著增加内存使用率和堆分配数。
函数引用和闭包分配内存的确切详细信息因平台和编译器设置而异,但如果想解决垃圾回收的问题,则最好在开发期间尽量减少函数引用的使用和闭包。
匿名方法示例
- Action action = () =>
- {
- Debug.Log("这是一个匿名方法");
- };
闭包方法示例
- string str = "这是一个闭包方法";
- Action action = () =>
- {
- Debug.Log(str);
- };
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。