【Unity学习笔记】脚本优化小技巧分享

## 前言
Unity游戏程序的优化一直以来都是令人棘手的问题。与排查Bug的过程不同，造成游戏帧率低下卡顿的问题可能分散在程序的各个角落，而每一个小问题单独看却又不够明显难以察觉。因此比起在开发后期再进行优化而困难重重，应该从一开始就注重效率来开发。
Unity优化在于不少方面，本文介绍几个在写脚本时容易被忽视的优化小技巧。

## 1. 使用transform应该先缓存
一个继承自MonoBehavior的脚本可以直接通过**transform**属性访问到自身物体的Transform组件，然而这个行为的效率其实不高。如果在一帧内需要大量对Transform进行操作的话，则应该先建立一个Transform的缓存，对缓存进行操作而不是每次都访问**transform**属性。
我们进行直接访问和通过缓存访问的对比试验。

```csharp:Test.cs
private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("ChangePosition");
    // 为使实验结果明显，反复执行1000000次操作
    for (int i = 0; i < 1000000; i++)
    {
        transform.position = new Vector3(i, i, i);            
    }
    Profiler.EndSample();
}
```
![コメント 2020-01-03 192333.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/baed7b3f-f132-e94b-0b2a-afae7b961075.png)
通过直接访问transform来改变position1000000次，耗时91.52ms。

```csharp:Test.cs
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();
}
```
![コメント 2020-01-03 193055.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/44b5b200-44bb-c4a2-611c-9b9d22c5a5cc.png)
首先建立一个名为myTransform的私有变量，在start中访问transform一次赋值给myTransform。通过访问myTransform来改变position1000000次，耗时55.12ms。
结论是，通过缓存transform的方式可以比直接访问transform的运行效率提高了约40%。这个优化技巧在场景中有大量Transform操作时的效果十分明显。

## 2. 不要频繁访问Camera.main
使用Camera.main虽然可以很方便的访问到主相机，但其内部本质是执行了**FindGameObjectWithTag("MainCamera")**，效率是非常低下的。

```csharp:Test.cs
private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("Camera Operation");
    // 为使实验结果明显，反复执行100000次操作
    for (int i = 0; i < 100000; i++)
    {
        Camera.main.fieldOfView = 60f;            
    }
    Profiler.EndSample();
}
```
![コメント 2020-01-03 202413.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/928d976b-7045-d610-e507-2f48270ce651.png)

通过执行Camera.main获取相机，设置FOV100000次，耗时33.99ms。

```csharp:Test.cs
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();
}
```
![コメント 2020-01-03 202632.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/c6f8a8d1-2edb-6250-0b98-325c992d4b58.png)
创建一个myCamera的变量来缓存Camera.main，通过访问myCamera来设置FOV100000次，耗时仅5.61ms。
结论是，创建一个变量把Camera.main缓存下来，通过访问缓存来相机，效率会提高约83%。

## 3. 创建向量时，尽量使用new Vector()，而不要使用Vector.zero等属性
Unity的向量类中提供的方便创建向量的属性，例如Vector3.zero，Vector.up等属性的效率其实并不高。

```csharp:Test.cs
private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("New Vector");
    // 为使实验结果明显，反复执行1000000次操作
    for (int i = 0; i < 1000000; i++)
    {
        var vector = Vector3.zero;   
    }
    Profiler.EndSample();
}
```
![コメント 2020-01-03 205855.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/fc8975f1-12fe-7142-c657-96c6ec8d9861.png)
使用Vector3.zero来创建向量1000000次，耗时11.70ms

```csharp:Test.cs
private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("New Vector");
    // 为使实验结果明显，反复执行1000000次操作
    for (int i = 0; i < 1000000; i++)
    {
        var vector = new Vector3();   
    }
    Profiler.EndSample();
}
```
![コメント 2020-01-03 210148.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/16436a9f-89c0-a3a2-0650-87614b2b6741.png)
直接使用new Vector3()来创建向量1000000次，耗时仅3.55ms。
结论是使用new Vector()而不是Vector.zero等属性来创建向量，效率提升约70%。

## 4. position和localPosition等同时，尽可能使用localPosition（rotation同理）
为了方便管理Hierarchy，我们经常会创建position和rotation都等于0的父节点，将需要管理的物体放进去作为子物体。这些子物体的localPosition和position在值上是等同的，但在访问position时实际上是执行了将localPosition转换到世界坐标系的操作，效率上比直接访问localPosition低。
![コメント 2020-01-03 220436.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/b2949fc6-f4f2-6d71-9fcf-182f949420e7.png)
先创建一个5层结构，在每一层的物体上执行测试脚本。

```csharp:Test.cs
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();
}
```
根节点的执行情况
![コメント 2020-01-03 221248.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/eeca3c7b-5f4c-ee88-bf23-57f4cbb9c04c.png)
第一层子节点的执行情况
![コメント 2020-01-03 221322.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/382f53c2-9d41-ee94-701e-891688b2205a.png)
第二层子节点的执行情况
![コメント 2020-01-03 221402.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/4f99f3a5-51e6-8d5c-ad67-973e6516322c.png)
第三层子节点的执行情况
![コメント 2020-01-03 221435.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/24e8ae31-3160-eab1-f231-74eb5ee8e612.png)
第四层子节点的执行情况
![コメント 2020-01-03 222740.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265897/8fb9a99d-767f-c472-08b0-9d5707ca95e6.png)
我们可以看到，层级越深的物体访问position时坐标系转换的耗时就越长。

## 总结
1. transform先缓存再使用。
2. Camera.main先缓存再使用。
3. 创建向量使用new Vector()，而不是Vector.zero等属性。
4. 尽可能使用localPosition而不是position，rotation同理。