Unity 性能优化-代码

1、GameObject本机-托管桥接

与C#对象相比,GameObject和MonoBehaviour是特殊对象,因为它们在内存中有两个表示:一个表示存在于管理C#代码相同系统管理的内存中,C#代码是用户编写的(托管代码),另一个表示存在于另一个单独处理的内存空间中(本机代码)。数据可以再这两个内存之间移动,因此每次移动都会导致额外的CPU开销和 可能的额外内存分配,这种效果一般称为跨越本机-托管的桥接

由以上理论,触发这种额外开销的有以下两种常见情况:

  • 对GameObject空引用检查

    一般我们使用以下方式对GameObject空引用检查:

    if(gameObject!=null){
        //DoSomething
    }
    

    另一种更好地方式是利用System.Object.ReferenceEquals(),其运行速度大约是上边的两倍:

    if(!System.Object.ReferenceEquals(gameObject,null))	{
        //DoSomething
    }
    

    以上方式也适用于MonoBehaviour。

  • GameObject的字符串属性

    从GameObject中检索字符串属性是另一种意外跨越本机-托管桥接的方式。通常使用的两个属性是tag和name,因此使用这两个属性是不好的,然而GameObject提供了CompareTag()方法,它则完全避免了本机-托管的桥接。

    使用gameObject.CompareTag("tag")而不是使用gameObject.tag=="tag"。除此之外,name属性没有对应方法,因此尽可能使用Tag属性。

2、获取组件优化

Unity中获取组件GetComponent()有3个可用的重载,分别是GetComponent(string),GetComponent< T >()和GetComponent(typeof(T))。在这三个方法中,最好使用GetCompnent< T >()重载。

此外,GetComponent()方法也不应该运用在类似Update()逐帧计算中,最好的方法是初始化过程中(Awake或Start等)就获取引用并缓存它们,直到需要使用它们为止。同样的技巧也适用于在运行时决定计算的任何数据,不需要要求CPU在每次执行Update()时都重新计算相同的值,因此可以提前将其缓存到内存中。

3、移除空的回调定义

在MonoBehaviour脚本中常用其周期函数,常用的有Awake()、Start()、Update()、FixedUpdate()等,这些回调函数会在场景第一次实例化时添加到一个函数指针列表中,又因为在所有的Update()回调(包括场景中所有的MonoBehaviour)完成之前,渲染管线不允许呈现新帧,因此当场景中有大量MonoBehaviour脚本时(包含空的Start()或Update()),场景的初始化以及每帧都会严重消耗资源从而影响帧率。因此我们需要在编写脚本时注意删除空的周期函数,例如Start(),Update()等。

4、运行时修改Transform的父节点

Transform组件的父-子关系比较像动态数组,因此Unity尝试将所有共享相同父元素的Transform按顺序存储在预先分配的内存缓冲区中,并在Hierarchy窗口中根据父元素下面的深度进行排序。这种数据结构允许整个组中进行更快的迭代,对于物理和动画等多个子系统有利,但是如果将一个GameObject的父对象重新指定为另一个对象,父对象必须将子对象放入预先分配的缓冲区中,并根据新的深度对所有Transform进行排序。另外如果父对象没有预先分配足够的空间,就必须扩展缓冲区。对于较深、复杂的GameObject结构,这需要一些时间来完成。

通过GameObject.Instantiate()实例化新的GameObject时,想为其设置一个父物体,在我们使用时很多情况会写成类似以下代码:

GameObject listItem = (GameObject)Instantiate(Resources.Load("Prefabs/UI/Items/PersonListItem"));
listItem.transform.SetParent(m_PersonSelectContnt, false);

以上情况在listItem实例化之后立即将Transform的父元素重新修改为另一个元素,它将丢弃一开始分配的缓冲区,为了避免这种情况,应该将父Transform参数提供给GameObject.Instantiate()调用,这调用可跳过这个缓冲区分配步骤,从而提升一部分性能:

 GameObject listItem = (GameObject)Instantiate(Resources.Load("Prefabs/UI/AMMT/Items/PersonListItem", m_PersonSelectContnt, false));

5、减少对Transform的改变

我们在做各种需求的时候,经常会需要改变物体的位置、旋转。而每当我们改变Transform组件的属性时,Transform组件会立刻发出一个OnTransformChanged消息,它不仅发送给当前改变的这个物体,还会发送给它所拥有的所有组件,以及物体的子物体及其组件。在子物体比较多(层级比较多)的情况下,这种递归的开销是值得警惕的。要减少这个开销,可以从两方面入手:

  1. 尽可能减少transform的属性修改。虽然是这样说,但是这通常都是比较困难的。毕竟transform的修改大部分情况下都是必需的。不过我们可以尽可能减少不必要的属性修改;比如计算中的一些中间结果不要set回transform中,而是最后把最终结果set一次。
  2. 尽可能减少子物体的数量。这通常是我们需要重点关注的。场景中的动态物体一般不要设置的层级太深。比如对于带有动画的物体,勾选Optimize Game Object。这会使物体的transform层级大大降低。一般来说都是建议勾选的。只有一种情况不能勾选:你想要在脚本中改变角色的骨骼的位置、旋转等。如果你勾选了,那么物体骨骼的transform是取不到的,即使你设置了Extra Transforms to Expose,可以取到transform,但是这已经不是那根骨骼的transform的引用了,因此,修改它并不能影响骨骼的transform。

6、避免在运行时使用Find()和SendMessage()

SendMessage()GameObject.Find()方法非常昂贵,应不惜一切代价尽量避免使用。Find()会迭代场景中的每个GameObject对象。不过,在场景初始化期间调用Find()有时是可以的,例如在Awake()或Start()中。

7、Vector计算

在进行向量乘法计算时,有一点需要注意乘法顺序,因为向量乘比较耗时,所以我们应该尽可能减少向量乘法运算。可以基于之前CustomTestTimer来做一个实验:

    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;
    }

最终结果如下:

image-20200928112412526

由结果可以看出,以上两个方法结算结果相同,但是Func2却比Func1耗时少,因为后者比前者少了一次向量乘法。所以,应该尽可能合并数字乘法,最后再进行向量乘。

8、循环

在迫不得已需要写多重循环时,应该尽量把遍历次数较多的循环放在内层。做测试如下:

    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;
                }
            }
        }
    }

测试结果如下:

image-20200928114131466

 9、MonoBehaviour

非必要不要继承MonoBehaviour,因为MonoBehaviour中有很多unity定义的属性方法。实例化出来用不到,就是浪费。

10、拆装箱

减少拆装箱操作,相同操作方法用泛型

一种常规易造成拆装箱的写法:

   string str = string.Format("{0}", 1);

上面会造成拆装箱,下面的写法就没问题

  string str = 1.ToString();

注意:string str = string.Format("{0}", i.ToString());依然会有拆装箱

11、string 与 stringBuilder

多次操作字符串使用stringBuilder,对string每次操作都会生成新的对象,从而产生GC垃圾。

stringBuilder如果能确定长度,最好先指定长度。

12、结构体

对于值类型的集合结构使用结构体。占的是栈内存,栈内存效率较高

但栈内存总容量也就是1M,而且在进行形参传递时会进行值拷贝

13、系统自带默认值

Vector3 v3 = new Vector3(0,0,0); 与 Vector3 v3 = Vector3.zero;使用Vector3.zero更好,因为减少了一次new的过程。Vector3.zero全局只有一个。

14、计算向量距离

需求:

if (Vector3.Distance(v1, v2) < showDistance)
{
}

以上需求如果只是判断两个向量距离小于。实际上可以减少求平方根的计算。

首先先看下Distance方法实现

public static float Distance(Vector2 a, Vector2 b)
{
    float num = a.x - b.x;
    float num2 = a.y - b.y;
    float num3 = a.z - b.z;
    return (float)Math.Sqrt(num * num + num2 * num2 + num3 * num3);
}

实际上最后一步中Math.Sqrt可以去掉,从而提升性能。如下:

float num = v1.x - v2.x;
float num2 = v1.y - v2.y;
float num3 = v1.z - v2.z;
distance = num * num + num2 * num2 + num3 * num3;

if (distance < 4) 
{
}

当然可以借助distance = Vector3.SqrMagnitude(v2 - v1);的方法。理论上性能跟我们上面手写是一致的,但是v2 - v1,生成新的Vector3代价要比上面自己声明的 num、num2、num3.代价高的多,实际性能可以还没提升。

15、Camera.main

Camera.main原理后台使用 FindGameObjectsWithTag() ,它会以很高的开销在场景图中搜索具有“MainCamera”标记的 camera 对象。也有说高版本这里做了优化,这个我不太确定,我在Unity2020版本中测试Camera.main依然消耗较高

16、MaterialPropertyBlock

修改材质球属性时,

如下:每次修改颜色都会创建一个材质实例。浪费性能,要改用MaterialPropertyBlock去修改

var _meshRender = GetComponent<MeshRenderer>();
_meshRender.material.color = new Color(1, 0, 1);

17、大量调用List.Contains时。

当场景List个数10000时,如下:效率非常低,循环次数越多计算越慢,这时使用Hashset,效率提升非常明显。

for(int i=0; i<1000; i++)
{
    if(list.Contains(i))
    {
        //todo
    }
}

详细测试工程案例

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一零壹0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值