(统一的3 d)统一的3 d性能优化(一)

听到过很多用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 plaincopy

通过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和gameObject上要取用的Component的键值对,然后在扩展方法里做字典查询以获得Component;

实验结果——方案一约1700ms,方案二约,方案三约,方案四约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()或者GetComponent(Type type)的重载形式;如果确实调用比较频繁,那么最好是自己对GameObject或者Component类进行扩展,以字典查询代替每次的GetComponent调用,毕竟效率稍微高那么一点点(当然了,如果组件是动态的,那么这个办法就不适用了,还是乖乖的用GetComponent);而GetComponent(string type)这个重载如无必要就不要使用,因为它每次调用时都必须进行类型反射,以至于效率只有另外两个重载形式的十分之一不到,即便是只能以字符串的形式得知所需组件的类型,也可以事先手动进行类型反射,而不是在频繁的GetComponent时直接传递字符串参数,只有一种情况下不得不使用GetComponent(string type)这个重载形式,那就是:每一次调用前都只能以字符串的形式的到组件类型,而且每一次调用前所获得到的组件类型是无法预测的,这中情况下手动做类型反射跟直接调用GetComponent没有区别。

(统一的3 d)统一的3 d性能优化(一)