Unity优化——脚本优化策略3
大家好,这里是七七,今天又来更新Unity脚本优化篇了,话不多说,直接上主题。
一、注意缓存Transform的变化
Transform组件只存储与其父组件相关的数据。这意味着访问和修改Transform组件的position、rotation和scale属性会导致大量未预料到的矩阵乘法计算,从而通过其父Transform为对象生成正确的Transform表示。对象在Hierarchy窗口中的位置越深,确定最终结果需要进行的计算就越多。
然而,这也意味着使用localPosition、localRotation和localScale的相关成本相对较小,因为这些值直接存储在给定的Transform中,可以进行检索,不需要任何额外的矩阵算法。因此,应该尽可能使用这些本地属性值。
遗憾的是,将数学计算从世界空间更改为本地空间,会使原本很简单的问题变得过于复杂,因此进行这样的更改会破坏实现方案,并引入大量以外的bug。有时,为了更容易地解决复杂的3D数学问题,牺牲一点性能是值得的。
不断更改Transform组件属性的另一个问题是,也会向组件(如Collider、RigidbodyLight和Camera)发送内部通知,这些组件也必须进行处理,因为物理和渲染系统都需要知道Transform的值,并相应地更新。
题外话:如上文所述,由于内存中Transform的充足,这些内部通知的速度在Unity5.4中得到了极大的提高,但我们仍需了解它们的成本。
在复杂的事件链中,在同一帧中多提提换Transform组件的属性是很常见的。每次发生这种情况时,都会触发内部消息,即使它们发生在同一帧甚至同一个函数调用期间。因此,应该尽量减少修改Transform属性的次数,方法是将它们缓存在一个成员变量中,只在帧的末尾提交它们。
二、避免在运行时使用Find()和SendMessage()方法
众所周知,SendMessage()方法和GameObject.Find()方法非常昂贵,应该不惜一切代价避免使用。在有些时候,如场景初始化期间调用Find()是可以原谅的,例如在Awake()或Start()回调期间。即使在这种情况下,它也只能用于获取已经确定存在于场景中的对象,以及只有少量GameObjects的场景。无论如何,在运行时使用这两种方法进行对象间通信会产生非常明显的开销,还可能丢帧。
可以采取多尔方法来解决这个问题,体量比较大,以后找个专题说说。
三、禁止未使用的脚本和对象
场景有时会变得非常繁忙,特别是构建大型的、开放的世界时,在Update()回调这种,调用代码的对象越多,它的伸缩性就越差,游戏也就越慢。然而,如果许多正在处理的内容在玩家的视野之外,或者只是太远而显得不重要,就完全不必要处理它们。这种可能不适合建立模拟大型城市的游戏,因为必须总是处理整个仿真。但它通常适用于第一人称和赛车游戏:因为玩家活动在开阔的区域,而非可视对象可以临时禁用,而不会对游戏过程产生任何明显的影响。下面来介绍两种禁用方式:
3.1通过可见性禁用对象
有时,希望组件或GameObject在不可见时禁用。Unity带有内置的渲染功能,以避免渲染对玩家的相机视图不可见的对象, 避免渲染隐藏在其他对象后面的对象,但这些只是渲染层的优化。它不会影响在CPU上执行任务的组件,比如AI脚本、用户界面和游戏逻辑。我们必须自己控制这种行为。
解决这个问题的一个好方法是使用OnBecameVisible()和OnBecameInvisible()回调。顾名思义,这些回调方法是在可渲染对象对于场景中的任何相机变得可见或不可见时调用的。此外,当一个场景中有多个摄像机时,只有当对象对任何一个相机可见,以及对所有相机不可见时,才会分别调用这两个回调。这意味着上述回调将在预期的正确时间调用;
由于可见性回调必须与渲染管线通信,因此GameObject必须附加上一个可渲染的组件,例如MeshRenderer或SkinnedMeshRenderer。必须确保希望接受可见性回调的组件也与可渲染对象连接在同一个GameObject上,而不是连接到其父或子GameObject上,否则它们也不会调用。
提示:要注意,Unity还计算Scene窗口中对 OnBecameVisible()和OnBecameInvisible()回调隐藏的摄像头数。如果发现在播放模式测试期间,这些方法没有被正确调用,请确保将Scene窗口的摄像机背对所有对象,或完全禁用Scene窗口
为了使用可见性回调开启/禁用独立组件,需要添加下述方法:
void OnBecameVisible() { enabled = true; }
void OnBecameInvisible() { enabled = false; }
为了开启/禁用Component所附加的整个GameObject,可以下面的方式实现方法:
void OnBecameVisible() { gameObject.SetActive(true); }
void OnBecameInvisible() { gameObject.SetActive(false); }
不过,请注意,禁用包含可渲染对象的GameObject或它的父对象之一,就不可能调用OnBecameVisible(),因为现在摄像机没有图形表示来查看和触发回调。应该将组件放在一个子GameObject上,并让脚本禁用它,使可渲染的对象始终可见。
3.2通过距离禁用对象
在其他情况下,如果组件或GameObject离玩家够远,以至于看不见,就可以禁用它们。原神中的原魔就是在玩家走进后才会出现的。
下面的代码是一个简单的协程,定期检查与给定目标的总距离,太远就禁用自己
[SerializeField] GmeObject _target;
[SerializeField] float _maxDistance;
[SerializeField] int _coroutineDelay;
void Start()
{
StartCoroutine(DisableAtADistance());
}
IEnumerator DisableAtADistance()
{
while (1)
{
float distSqrd = (transform.position - _target.transform.position).sqrMagnitude;
if (distSqrd < _maxDistance * _maxDistance)
{
enabled = true;
}
else
{
enabled = false;
}
for (int i = 0; i < _coroutineDelay; i++)
{
yield return new WaitForEndOfFrame();
}
}
}
四、使用距离平方而不是距离
可以肯定地说,CPU比较擅长将浮点数相乘,但不擅长计算它们的平方根。每次使用magnitude属性或Distance()方法要求Vector3计算距离时,都会要求它执行平方根计算,与许多其它类型的向量数学计算相比,这会消耗大量的CPU开销。
然而,Vector3类也提供了sqrMagnitude属性,它提供了同样可作为距离的结果,只是该值时平方。这意味着如果也将需要比较的距离进行平方,就可以执行基本相同的比较,而不需要昂贵的平方根计算。
用这两种方式的结果几乎相同,原因是浮点精度。可能会失去一些使用平方根的精度,因为该值调整为具有不同密度的可表示数字区域;它可以准确地落在一个更精确的可表示数字区域,更有可能落在一个精度较低的数字区域上。结果,比较并不完全相同,但是,在在大多数情况下,它非常接近,不会引起注意,对于这种方式替换的每条指令,性能收益可能相当可观。
如果这个小的精度损失不重要,那么应该考虑这个性能技巧。然而,如果精度是非常重要的,就可能要忽略这个技巧。
注意,此技术可用于任何平方根计算。而不只是用于距离。这是最常见的示例,它揭示了Vector3类的sqrMagnitude属性的中亚行。这是Unity Technologies有意以这种方式向我们展示的一个属性。