前言
Unity游戏程序的优化一直以来都是令人棘手的问题。与排查Bug的过程不同,造成游戏帧率低下卡顿的问题可能分散在程序的各个角落,而每一个小问题单独看却又不够明显难以察觉。因此比起在开发后期再进行优化而困难重重,应该从一开始就注重效率来开发。
Unity优化在于不少方面,本文介绍几个在写脚本时容易被忽视的优化小技巧。
1. 使用transform应该先缓存
一个继承自MonoBehavior的脚本可以直接通过transform属性访问到自身物体的Transform组件,然而这个行为的效率其实不高。如果在一帧内需要大量对Transform进行操作的话,则应该先建立一个Transform的缓存,对缓存进行操作而不是每次都访问transform属性。
我们进行直接访问和通过缓存访问的对比试验。
private void Update()
{
// 让Profiler监视这一段操作
Profiler.BeginSample("ChangePosition");
// 为使实验结果明显,反复执行1000000次操作
for (int i = 0; i < 1000000; i++)
{
transform.position = new Vector3(i, i, i);
}
Profiler.EndSample();
}
通过直接访问transform来改变position1000000次,耗时91.52ms。
private Transform myTransform;
private void Start()
{
myTransform = transform;
}
private void Update()
{
// 让Profiler监视这一段操作
Profiler.BeginSample("ChangePosition");
// 为使实验结果明显,反复执行1000000次操作
for (int i = 0; i < 1000000; i++)
{
myTransform.position = new Vector3(i, i, i);
}
Profiler.EndSample();
}
首先建立一个名为myTransform的私有变量,在start中访问transform一次赋值给myTransform。通过访问myTransform来改变position1000000次,耗时55.12ms。
结论是,通过缓存transform的方式可以比直接访问transform的运行效率提高了约40%。这个优化技巧在场景中有大量Transform操作时的效果十分明显。
2. 不要频繁访问Camera.main
使用Camera.main虽然可以很方便的访问到主相机,但其内部本质是执行了FindGameObjectWithTag("MainCamera"),效率是非常低下的。
private void Update()
{
// 让Profiler监视这一段操作
Profiler.BeginSample("Camera Operation");
// 为使实验结果明显,反复执行100000次操作
for (int i = 0; i < 100000; i++)
{
Camera.main.fieldOfView = 60f;
}
Profiler.EndSample();
}
通过执行Camera.main获取相机,设置FOV100000次,耗时33.99ms。
private Camera myCamera;
private void Start()
{
myCamera = Camera.main;
}
private void Update()
{
// 让Profiler监视这一段操作
Profiler.BeginSample("Camera Operation");
// 为使实验结果明显,反复执行100000次操作
for (int i = 0; i < 100000; i++)
{
myCamera.fieldOfView = 60f;
}
Profiler.EndSample();
}
创建一个myCamera的变量来缓存Camera.main,通过访问myCamera来设置FOV100000次,耗时仅5.61ms。
结论是,创建一个变量把Camera.main缓存下来,通过访问缓存来相机,效率会提高约83%。
3. 创建向量时,尽量使用new Vector(),而不要使用Vector.zero等属性
Unity的向量类中提供的方便创建向量的属性,例如Vector3.zero,Vector.up等属性的效率其实并不高。
private void Update()
{
// 让Profiler监视这一段操作
Profiler.BeginSample("New Vector");
// 为使实验结果明显,反复执行1000000次操作
for (int i = 0; i < 1000000; i++)
{
var vector = Vector3.zero;
}
Profiler.EndSample();
}
使用Vector3.zero来创建向量1000000次,耗时11.70ms
private void Update()
{
// 让Profiler监视这一段操作
Profiler.BeginSample("New Vector");
// 为使实验结果明显,反复执行1000000次操作
for (int i = 0; i < 1000000; i++)
{
var vector = new Vector3();
}
Profiler.EndSample();
}
直接使用new Vector3()来创建向量1000000次,耗时仅3.55ms。
结论是使用new Vector()而不是Vector.zero等属性来创建向量,效率提升约70%。
4. position和localPosition等同时,尽可能使用localPosition(rotation同理)
为了方便管理Hierarchy,我们经常会创建position和rotation都等于0的父节点,将需要管理的物体放进去作为子物体。这些子物体的localPosition和position在值上是等同的,但在访问position时实际上是执行了将localPosition转换到世界坐标系的操作,效率上比直接访问localPosition低。
先创建一个5层结构,在每一层的物体上执行测试脚本。
private Transform myTransform;
private void Start()
{
myTransform = transform;
}
private void Update()
{
// 让Profiler监视这一段操作
Profiler.BeginSample("SetPosition");
// 为使实验结果明显,反复执行1000000次操作
for (int i = 0; i < 100000; i++)
{
myTransform.position = new Vector3(i, i, i);
}
Profiler.EndSample();
// 让Profiler监视这一段操作
Profiler.BeginSample("SetLocalPosition");
// 为使实验结果明显,反复执行1000000次操作
for (int i = 0; i < 100000; i++)
{
myTransform.localPosition = new Vector3(i, i, i);
}
Profiler.EndSample();
}
根节点的执行情况
第一层子节点的执行情况
第二层子节点的执行情况
第三层子节点的执行情况
第四层子节点的执行情况
我们可以看到,层级越深的物体访问position时坐标系转换的耗时就越长。
总结
- transform先缓存再使用。
- Camera.main先缓存再使用。
- 创建向量使用new Vector(),而不是Vector.zero等属性。
- 尽可能使用localPosition而不是position,rotation同理。