[Unity 3D] Unity 3D 性能优化-粒子优化

原贴地址:

http://blog.csdn.net/lijing_hi/article/details/11657887


听到过很多用Unity 3D开发游戏的程序员抱怨引擎效率太低,资源占用太高,包括我自己在以往项目的开发中也头疼过。最近终于有了空闲,可以仔细的研究一下该如何优化Unity 3D下的游戏性能。其实国外有不少有关U3D优化的资料,Unity官方的文档中也有简略的章节涉及这方面的内容,不过大多都是以优化美术资源为主,比如贴图的尺寸,模型静态及动态的batch以减少draw call,用lightmap替代动态光影,不同渲染模式在不同环境下的性能等等。鉴于此,加上美术资源方面的东西本人不是特别了解,所以都撇开不谈,这里先试着分析分析U3D脚本中常用代码段的执行效率


GetComponent


这是一个U3D脚本中使用频率最高的函数之一,这一族函数包括GetComponent,GetComponents,GetComponentInChildren,GetComponentsInChildren以及他们的泛型版本,此外GameObject类以及Component类上的很多属性也可以归于这一范畴,比如Component类的gameObject属性,GameObject类和Component类都有的transform属性等等这一系列从GameObject实例以及Component实例上获取其他挂载的内建组件的属性接口。

先来看看GetComponent函数的几种重载形式:

[csharp]  view plain  copy
  1. Component GetComponent(Type type);  
  2. T GetComponent<T>() where T : Component;  
  3. Component GetComponent(string type);  

通过ILSpy查看UnityEngine部分源码,发现泛型形式的GetComponent其实不过是在函数体中对泛型类型T调用了typeof,然后就直接调用了非泛型形式的GetComponent,因此在此不对泛型形式的GetComponent函数做讨论。下面设计一个小实验来看看两种不同GetComponent函数的效率,以及对GetComponent的不同使用方式会带来什么样的影响:


设计实验——实验执行的主要过程是对同一个gameObject连续获取同一类型的Component 8×1024×1024次,统计不同方法下的时间开销,单位是毫秒。在实验用的gameObject上一共挂在了五个各不相同的组件,所有的实验操作都是获取这五个组件中的第一个。

方案一,最直接的方式,直接在循环中对gameObject调用GetComponent(Type type)方法;

方案二,同样直接的方式,直接在循环中对gameObject调用GetComponent(string type)方法;

方案三,在循环外事先以GetComponent获取gameObject上的Component并缓存引用,然后在循环中直接访问缓存的引用;

方案四,利用C#扩展方法,对GameObject类添加扩展方法,以一个静态字典Dictionary<GameObject, Component>存储gameObject和gameObject上要取用的Component的键值对,然后在扩展方法里做字典查询以获得Component;

实验结果——方案一约1700ms,方案二约18500ms,方案三约30ms,方案四约1500ms。

(可能有人会对方案四抱有怀疑,担心字典中gameObject数量会影响查询效率,虽然我可以直接告诉你正常游戏里可能同时存在的GameObject数据量下对字典查询根本没有能够被觉察到的影响,但还是以数据来说明问题:

继续设计子实验,针对方案四,调整场景中gameObject的数量,每个gameObject上都挂载上述实验里的五个组件,并且都向字典中注册,对每种gameObject数量的情况都执行上述实验里的8×1024×1024次组件访问。

子实验结果——1个gameObject时约1500ms,5个gameObject时约1500ms,10个gameObject时约1500ms,100个gameObject约1500ms,1000个gameObject时约1500ms,10000个gameObject时还是约1500ms,此时向字典中注册所消耗的时间已经远远大于之后进行的循环的消耗。其实熟悉C#字典表的人根本不会有疑问,字典是散列表,查询复杂度O(1)。)


由上述实验可以得出结论,如果要获取一个gameObject上挂载的某个组件,在逻辑允许或者架构允许的情况下尽量事先缓存这个组件的引用,这是最高效的做法,开销可以忽略不计;假如情况不允许事先缓存引用,那么在调用频率不是很频繁的情况下可以使用GetComponent<T>()或者GetComponent(Type type)的重载形式;如果确实调用比较频繁,那么最好是自己对GameObject或者Component类进行扩展,以字典查询代替每次的GetComponent调用,毕竟效率稍微高那么一点点(当然了,如果组件是动态的,那么这个办法就不适用了,还是乖乖的用GetComponent);而GetComponent(string type)这个重载如无必要就不要使用,因为它每次调用时都必须进行类型反射,以至于效率只有另外两个重载形式的十分之一不到,即便是只能以字符串的形式得知所需组件的类型,也可以事先手动进行类型反射,而不是在频繁的GetComponent时直接传递字符串参数,只有一种情况下不得不使用GetComponent(string type)这个重载形式,那就是:每一次调用前都只能以字符串的形式的到组件类型,而且每一次调用前所获得到的组件类型是无法预测的,这中情况下手动做类型反射跟直接调用GetComponent没有区别。



看完GetComponent族函数之后,接下来就是GameObject类和Component类内置的组件访问属性。

在实际脚本代码编写中,你是否经常这样一长串代码就轻易写出来了:

[csharp]  view plain  copy
  1. Vector3 pos = gameObject.transform.position;  
  2. gameObject.collider.enabled = false;  

以我们的直觉,GameObject类和Component类所提供的这些属性应该都是直接访问的事先缓存好的组件引用,因此对这些属性的使用便无所顾忌。但是事情真的是如我们所想的那样吗?如果我告诉你,有时候哪怕是用GetComponent函数的string参数形式都会比使用这些属性来的要快,你相信么?还是用实验数据说话吧。


设计实验——对某gameObject上的Transform组件,采用不同的方法,访问8×1024×1024次。

方案一,实现缓存gameObject上transform组件的引用,然后所有访问都直接取用缓存的引用;

方案二,在脚本中直接以Component类的transform属性调用的方式访问(U3D脚本都是从MoniBehaviour类派生,而MonoBehaviour又派生自Component类,所以在脚本中可以直接访问transform属性,这一点相信很多人都知道);

方案三,在脚本中以gameObject.transform的形式访问组件(注意哦,很多人都有这个习惯,觉得组件是gameObject的组件,所以访问时都喜欢加上gameObject);

方案四,在脚本中以GetComponent<Transform>()函数访问组件;

实验结果——方案一约30ms,方案二约550ms,方案三约850ms,方案四约1700ms。


吃惊吧?transform属性访问的开销居然比直接访问引用要大这么多!而且通过gameObject转一道手之后开销居然又增加了这么多!不过还好,直接属性调用还是比用GetComponent要快的多……别太早下结论,Transform组件在每个GameObject实例上都有,对它的访问是不会失败的,那么如果被访问的组件在GameObject上不存在的时候呢?比如访问一个Rigidbody组件,而gameObject上没有挂载这样的组件,这时有会怎样?接着看实验。


设计实验——尝试对某gameObject上的Rigidbody组件进行访问8×1024×1024次。

方案一,gameObject上确实挂载了Rigidbody组件,事先缓存组件的引用,访问时取用缓存的引用;

方案二,gameObject上确实挂载了Rigidbody组件,脚本中以Component类的rigidbody属性访问组件;

方案三,gameObject上确实挂载了Rigidbody组件,脚本中以gameObject.rigidbody的方式访问组件;

方案四,gameObject上确实挂载了Rigidbody组件,脚本中以GetComponent<Rigidbidy>()访问组件;

方案五,gameObject上没有Rigidbody组件,事先缓存组件(当然获取到的是null),访问时取用引用;

方案六,gameObject上没有Rigidbody组件,脚本中以Component类的rigidbidy属性访问组件;

方案七,gameObject上没有Rigidbody组件,脚本中以gameObject.rigidbody方式访问组件;

方案八,gameObject上没有Rigidbody组件,脚本中以GetComponent<Rigidbody>()访问组件;

实验结果——方案一约30ms,方案二约800ms,方案三约1200ms,方案四约1700ms,方案五约30ms,方案六不少于60000ms,方案七不少于60000ms,方案八约1700ms。


更吃惊了吧?这一次的实验,前四组跟上一次实验差别不太大,但对rigidbody属性的访问还是要比transform属性慢了一点,后四组数据才是吃惊的根源,在组件不存在的情况下,通过属性访问组件居然会有如此大的额外开销!相比之下,GetComponent方法倒是不在乎组件是否真的存在,开销一如既往。

由于属性实现的代码无法通过ILSpy查看,所以在这里我只能用猜的了。首先是,U3D在实现这些组件访问属性的时候,必然做了各种查询和容错处理,绝非简单的缓存和取用引用那么简单,这也是属性访问比事先缓存引用的访问方式要慢那么多的原因;其次,Transform组件在每个GameObject实例上都必然存在,因此transform属性的实现比其他组件访问属性的实现必然要少那么一些步骤,这就造成对transform属性的访问要比其他组件属性快上一些;最后,当组件不存在时,对组件属性的访问应该是走入了大量的容错处理代码,这就造成这种情况下属性访问开销大增。

从这个实验又可以得出结论,我们的脚本代码里经常会需要访问gameObject引用或者某个组件的引用,最好的方式当然是在脚本Awake的时候就把这些可能访问的东西都缓存下来;如果需要访问临时gameObject实例的某属性或者临时某组件的gameObject实例,在能够确保组件一定存在的情况下,可以用属性访问,毕竟它们比GetComponent要快上一倍,但是如果不能确定组件是否存在,甚至是需要对组件的存在性做判断时,一定不要用对属性访问结果判空的方式,而要用GetComponent,这里面节省的开销不是一点半点。




IsAlive

U3D的粒子系统脚本接口相信很多人都用过,ParticleSyetem类的一系列接口都有一个bool类型的参数——withChildren,通过这个参数可以直接将相同的判断或者操作应用到一整个通过Transform父子关系树关联起来的ParticleSystem实例集合上。然而,但凡方便的功能,里面就必然有性能陷阱……

以IsAlive这个接口为例(用来判断粒子系统是否所有粒子都已经消亡,一般用在非loop的例子发射器上),看看U3D里是如何实现这个接口的:

[csharp]  view plain  copy
  1. public bool IsAlive()  
  2. {  
  3.     bool withChildren = true;  
  4.     return this.IsAlive(withChildren);  
  5. }  
[csharp]  view plain  copy
  1. public bool IsAlive(bool withChildren)  
  2. {  
  3.     if (withChildren)  
  4.     {  
  5.         ParticleSystem[] particleSystems = ParticleSystem.GetParticleSystems(this);  
  6.         ParticleSystem[] array = particleSystems;  
  7.         for (int i = 0; i < array.Length; i++)  
  8.         {  
  9.             ParticleSystem particleSystem = array[i];  
  10.             if (particleSystem.Internal_IsAlive())  
  11.             {  
  12.                 return true;  
  13.             }  
  14.         }  
  15.         return false;  
  16.     }  
  17.     return this.Internal_IsAlive();  
  18. }  

可以看到,如果传递的withChildren参数为true,那么函数会先尝试调用GetParticleSystems(this)来获取包括下级gameObject在内的所有能找得到的粒子系统组件,然后对这些粒子系统组件依次再调用IsAlive判断。而如果withChildren为false,就仅仅会判断自身。那么自然,开销大小与否,关键就在GetParticleSystems的实现上了。

[csharp]  view plain  copy
  1. internal static ParticleSystem[] GetParticleSystems(ParticleSystem root)  
  2. {  
  3.     if (!root)  
  4.     {  
  5.         return null;  
  6.     }  
  7.     List<ParticleSystem> list = new List<ParticleSystem>();  
  8.     list.Add(root);  
  9.     ParticleSystem.GetDirectParticleSystemChildrenRecursive(root.transform, list);  
  10.     return list.ToArray();  
  11. }  
[csharp]  view plain  copy
  1. private static void GetDirectParticleSystemChildrenRecursive(Transform transform, List<ParticleSystem> particleSystems)  
  2. {  
  3.     foreach (Transform transform2 in transform)  
  4.     {  
  5.         ParticleSystem component = transform2.gameObject.GetComponent<ParticleSystem>();  
  6.         if (component != null)  
  7.         {  
  8.             particleSystems.Add(component);  
  9.             ParticleSystem.GetDirectParticleSystemChildrenRecursive(transform2, particleSystems);  
  10.         }  
  11.     }  
  12. }  

U3D对获取所有下级gameObject实例上的粒子系统组件使用了递归的方式,并在递归结束后返回结果列表时做了一次列表元素复制(List.ToArray()),并且在获取粒子系统组件的时候用的是transform2.gameObject.GetComponent<ParticleSystem>(),而不是transform2.GetComponent<ParticleSystem>(),从上一篇文章里我们已经用实验证实了,前一种方式开销更大。看到这里,我们心里大概已经有谱了,那就是——效率绝对不会高到哪里去,影响性能的地方太多了……还是设计一个小实验来看看这种情况下应该用什么样的方式更好吧:

设计实验——一个两层结构,一个父gameObject挂载一个ParticleSystem组件,两个子gameObject分别挂载一个PariticleSystem组件,采用两种不同的方式对这个组合判断IsAlive各8×1024×1024次。
方案一,直接对父gameObject上的PariticleSystem调用IsAlive(true);
方案二,在循环前,先用GetComponentsInChildren将所有的PariticleSystem存入一个List,循环中对这个List做遍历,对List里每一个ParticleSystem调用IsAlive(false);
实验结果——方案一约3900ms,方案二约65ms。

结果对比很明显。其实,U3D提供的这个接口的意义在于,当不是需要进行那么频繁的调用时,可以用IsAlive(true)来省掉手动获取所有子粒子系统的过程,让代码简洁一些,虽然U3D目前对这个接口的实现有的地方还值得斟酌。ParticleSystem提供的这一族接口(IsAlive只是其中之一,此外还有Play,Pause,Stop等等),如果使用频率不是很高,比如仅仅是初始化或者销毁的时候做一次性调用,那么即便是withChildren参数是true也没有什么大不了的,还能少些很多代码,何乐而不为;但如果需要频繁调用,比如每帧都对粒子系统集合判断IsAlive,这种情况下,一定不能懒惰,该写的东西还是要写的。另外值得注意的一点,IsAlive这一族接口的无参形式,是默认withChildren为true的,使用的时候可别搞错了。

PS,留意一下GetDirectParticleSystemRecursive的实现方式,你会发现它有一个递归条件,就是节点上必须要有PariticleSystem组件,在递归过程中,一旦发现某个节点上没有ParticleSystem组件时,父子关系树上的这一枝就算遍历到头了,再往下即便是还有ParticleSystem存在也会被忽略。因此,如果你面对的ParticleSystem集合就恰好存在这样的断层,那最好还是自己勤快一点,自己动手用GetComonentsInChildren来查找所有的粒子系统组件。




CenterOfMass

在处理物理碰撞时,尤其是OnTrigger族消息,由于要手动计算碰撞点会经常要用到碰撞体的质心。获取质心常用的有三种方式:
1、Collider.bounds.center
2、Collider.rigidbody.worldCenterOfMass
3、Collider.attachedRigidbody.worldCenterOfMass
第一种方式其实是认为碰撞体是均匀几何体,所以取碰撞盒中心作为质心。注意,这里的Collider并不是collider属性,而是表示一个Collider引用,所以不必考虑collider属性访问引起的性能开销(其实在处理碰撞时,Collider引用是能够直接得到的,也不需要做collider属性访问)。

设计实验——针对同一个碰撞体,分别调用以上三种方式各8×1024×1024次
实验结果——第一种约4500ms,第二种约2500ms,第三种约2000ms。

在碰撞不涉及刚体的情况下,要获取质心只能使用性能最差的第一种方式。而有刚体的情况下,自然推荐使用第三种方式。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值