溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

[Unity 3D] Unity 3D 性能優(yōu)化 (一)

發(fā)布時(shí)間:2020-08-01 20:45:16 來(lái)源:網(wǎng)絡(luò) 閱讀:266 作者:蓬萊仙羽 欄目:開(kāi)發(fā)技術(shù)

聽(tīng)到過(guò)很多用Unity 3D開(kāi)發(fā)游戲的程序員抱怨引擎效率太低,資源占用太高,包括我自己在以往項(xiàng)目的開(kāi)發(fā)中也頭疼過(guò)。最近終于有了空閑,可以仔細(xì)的研究一下該如何優(yōu)化Unity 3D下的游戲性能。其實(shí)國(guó)外有不少有關(guān)U3D優(yōu)化的資料,Unity官方的文檔中也有簡(jiǎn)略的章節(jié)涉及這方面的內(nèi)容,不過(guò)大多都是以?xún)?yōu)化美術(shù)資源為主,比如貼圖的尺寸,模型靜態(tài)及動(dòng)態(tài)的batch以減少draw call,用lightmap替代動(dòng)態(tài)光影,不同渲染模式在不同環(huán)境下的性能等等。鑒于此,加上美術(shù)資源方面的東西本人不是特別了解,所以都撇開(kāi)不談,這里先試著分析分析U3D腳本中常用代碼段的執(zhí)行效率


GetComponent


這是一個(gè)U3D腳本中使用頻率最高的函數(shù)之一,這一族函數(shù)包括GetComponent,GetComponents,GetComponentInChildren,GetComponentsInChildren以及他們的泛型版本,此外GameObject類(lèi)以及Component類(lèi)上的很多屬性也可以歸于這一范疇,比如Component類(lèi)的gameObject屬性,GameObject類(lèi)和Component類(lèi)都有的transform屬性等等這一系列從GameObject實(shí)例以及Component實(shí)例上獲取其他掛載的內(nèi)建組件的屬性接口。

先來(lái)看看GetComponent函數(shù)的幾種重載形式:

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

通過(guò)ILSpy查看UnityEngine部分源碼,發(fā)現(xiàn)泛型形式的GetComponent其實(shí)不過(guò)是在函數(shù)體中對(duì)泛型類(lèi)型T調(diào)用了typeof,然后就直接調(diào)用了非泛型形式的GetComponent,因此在此不對(duì)泛型形式的GetComponent函數(shù)做討論。下面設(shè)計(jì)一個(gè)小實(shí)驗(yàn)來(lái)看看兩種不同GetComponent函數(shù)的效率,以及對(duì)GetComponent的不同使用方式會(huì)帶來(lái)什么樣的影響:


設(shè)計(jì)實(shí)驗(yàn)——實(shí)驗(yàn)執(zhí)行的主要過(guò)程是對(duì)同一個(gè)gameObject連續(xù)獲取同一類(lèi)型的Component 8×1024×1024次,統(tǒng)計(jì)不同方法下的時(shí)間開(kāi)銷(xiāo),單位是毫秒。在實(shí)驗(yàn)用的gameObject上一共掛在了五個(gè)各不相同的組件,所有的實(shí)驗(yàn)操作都是獲取這五個(gè)組件中的第一個(gè)。

方案一,最直接的方式,直接在循環(huán)中對(duì)gameObject調(diào)用GetComponent(Type type)方法;

方案二,同樣直接的方式,直接在循環(huán)中對(duì)gameObject調(diào)用GetComponent(string type)方法;

方案三,在循環(huán)外事先以GetComponent獲取gameObject上的Component并緩存引用,然后在循環(huán)中直接訪問(wèn)緩存的引用;

方案四,利用C#擴(kuò)展方法,對(duì)GameObject類(lèi)添加擴(kuò)展方法,以一個(gè)靜態(tài)字典Dictionary<GameObject, Component>存儲(chǔ)gameObject和gameObject上要取用的Component的鍵值對(duì),然后在擴(kuò)展方法里做字典查詢(xún)以獲得Component;

實(shí)驗(yàn)結(jié)果——方案一約1700ms,方案二約18500ms,方案三約30ms,方案四約1500ms。

(可能有人會(huì)對(duì)方案四抱有懷疑,擔(dān)心字典中g(shù)ameObject數(shù)量會(huì)影響查詢(xún)效率,雖然我可以直接告訴你正常游戲里可能同時(shí)存在的GameObject數(shù)據(jù)量下對(duì)字典查詢(xún)根本沒(méi)有能夠被覺(jué)察到的影響,但還是以數(shù)據(jù)來(lái)說(shuō)明問(wèn)題:

繼續(xù)設(shè)計(jì)子實(shí)驗(yàn),針對(duì)方案四,調(diào)整場(chǎng)景中g(shù)ameObject的數(shù)量,每個(gè)gameObject上都掛載上述實(shí)驗(yàn)里的五個(gè)組件,并且都向字典中注冊(cè),對(duì)每種gameObject數(shù)量的情況都執(zhí)行上述實(shí)驗(yàn)里的8×1024×1024次組件訪問(wèn)。

子實(shí)驗(yàn)結(jié)果——1個(gè)gameObject時(shí)約1500ms,5個(gè)gameObject時(shí)約1500ms,10個(gè)gameObject時(shí)約1500ms,100個(gè)gameObject約1500ms,1000個(gè)gameObject時(shí)約1500ms,10000個(gè)gameObject時(shí)還是約1500ms,此時(shí)向字典中注冊(cè)所消耗的時(shí)間已經(jīng)遠(yuǎn)遠(yuǎn)大于之后進(jìn)行的循環(huán)的消耗。其實(shí)熟悉C#字典表的人根本不會(huì)有疑問(wèn),字典是散列表,查詢(xún)復(fù)雜度O(1)。)


由上述實(shí)驗(yàn)可以得出結(jié)論,如果要獲取一個(gè)gameObject上掛載的某個(gè)組件,在邏輯允許或者架構(gòu)允許的情況下盡量事先緩存這個(gè)組件的引用,這是最高效的做法,開(kāi)銷(xiāo)可以忽略不計(jì);假如情況不允許事先緩存引用,那么在調(diào)用頻率不是很頻繁的情況下可以使用GetComponent<T>()或者GetComponent(Type type)的重載形式;如果確實(shí)調(diào)用比較頻繁,那么最好是自己對(duì)GameObject或者Component類(lèi)進(jìn)行擴(kuò)展,以字典查詢(xún)代替每次的GetComponent調(diào)用,畢竟效率稍微高那么一點(diǎn)點(diǎn)(當(dāng)然了,如果組件是動(dòng)態(tài)的,那么這個(gè)辦法就不適用了,還是乖乖的用GetComponent);而GetComponent(string type)這個(gè)重載如無(wú)必要就不要使用,因?yàn)樗看握{(diào)用時(shí)都必須進(jìn)行類(lèi)型反射,以至于效率只有另外兩個(gè)重載形式的十分之一不到,即便是只能以字符串的形式得知所需組件的類(lèi)型,也可以事先手動(dòng)進(jìn)行類(lèi)型反射,而不是在頻繁的GetComponent時(shí)直接傳遞字符串參數(shù),只有一種情況下不得不使用GetComponent(string type)這個(gè)重載形式,那就是:每一次調(diào)用前都只能以字符串的形式的到組件類(lèi)型,而且每一次調(diào)用前所獲得到的組件類(lèi)型是無(wú)法預(yù)測(cè)的,這中情況下手動(dòng)做類(lèi)型反射跟直接調(diào)用GetComponent沒(méi)有區(qū)別。



看完GetComponent族函數(shù)之后,接下來(lái)就是GameObject類(lèi)和Component類(lèi)內(nèi)置的組件訪問(wèn)屬性。

在實(shí)際腳本代碼編寫(xiě)中,你是否經(jīng)常這樣一長(zhǎng)串代碼就輕易寫(xiě)出來(lái)了:

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

以我們的直覺(jué),GameObject類(lèi)和Component類(lèi)所提供的這些屬性應(yīng)該都是直接訪問(wèn)的事先緩存好的組件引用,因此對(duì)這些屬性的使用便無(wú)所顧忌。但是事情真的是如我們所想的那樣嗎?如果我告訴你,有時(shí)候哪怕是用GetComponent函數(shù)的string參數(shù)形式都會(huì)比使用這些屬性來(lái)的要快,你相信么?還是用實(shí)驗(yàn)數(shù)據(jù)說(shuō)話(huà)吧。


設(shè)計(jì)實(shí)驗(yàn)——對(duì)某gameObject上的Transform組件,采用不同的方法,訪問(wèn)8×1024×1024次。

方案一,實(shí)現(xiàn)緩存gameObject上transform組件的引用,然后所有訪問(wèn)都直接取用緩存的引用;

方案二,在腳本中直接以Component類(lèi)的transform屬性調(diào)用的方式訪問(wèn)(U3D腳本都是從MoniBehaviour類(lèi)派生,而MonoBehaviour又派生自Component類(lèi),所以在腳本中可以直接訪問(wèn)transform屬性,這一點(diǎn)相信很多人都知道);

方案三,在腳本中以gameObject.transform的形式訪問(wèn)組件(注意哦,很多人都有這個(gè)習(xí)慣,覺(jué)得組件是gameObject的組件,所以訪問(wèn)時(shí)都喜歡加上gameObject);

方案四,在腳本中以GetComponent<Transform>()函數(shù)訪問(wèn)組件;

實(shí)驗(yàn)結(jié)果——方案一約30ms,方案二約550ms,方案三約850ms,方案四約1700ms。


吃驚吧?transform屬性訪問(wèn)的開(kāi)銷(xiāo)居然比直接訪問(wèn)引用要大這么多!而且通過(guò)gameObject轉(zhuǎn)一道手之后開(kāi)銷(xiāo)居然又增加了這么多!不過(guò)還好,直接屬性調(diào)用還是比用GetComponent要快的多……別太早下結(jié)論,Transform組件在每個(gè)GameObject實(shí)例上都有,對(duì)它的訪問(wèn)是不會(huì)失敗的,那么如果被訪問(wèn)的組件在GameObject上不存在的時(shí)候呢?比如訪問(wèn)一個(gè)Rigidbody組件,而gameObject上沒(méi)有掛載這樣的組件,這時(shí)有會(huì)怎樣?接著看實(shí)驗(yàn)。


設(shè)計(jì)實(shí)驗(yàn)——嘗試對(duì)某gameObject上的Rigidbody組件進(jìn)行訪問(wèn)8×1024×1024次。

方案一,gameObject上確實(shí)掛載了Rigidbody組件,事先緩存組件的引用,訪問(wèn)時(shí)取用緩存的引用;

方案二,gameObject上確實(shí)掛載了Rigidbody組件,腳本中以Component類(lèi)的rigidbody屬性訪問(wèn)組件;

方案三,gameObject上確實(shí)掛載了Rigidbody組件,腳本中以gameObject.rigidbody的方式訪問(wèn)組件;

方案四,gameObject上確實(shí)掛載了Rigidbody組件,腳本中以GetComponent<Rigidbidy>()訪問(wèn)組件;

方案五,gameObject上沒(méi)有Rigidbody組件,事先緩存組件(當(dāng)然獲取到的是null),訪問(wèn)時(shí)取用引用;

方案六,gameObject上沒(méi)有Rigidbody組件,腳本中以Component類(lèi)的rigidbidy屬性訪問(wèn)組件;

方案七,gameObject上沒(méi)有Rigidbody組件,腳本中以gameObject.rigidbody方式訪問(wèn)組件;

方案八,gameObject上沒(méi)有Rigidbody組件,腳本中以GetComponent<Rigidbody>()訪問(wèn)組件;

實(shí)驗(yàn)結(jié)果——方案一約30ms,方案二約800ms,方案三約1200ms,方案四約1700ms,方案五約30ms,方案六不少于60000ms,方案七不少于60000ms,方案八約1700ms。


更吃驚了吧?這一次的實(shí)驗(yàn),前四組跟上一次實(shí)驗(yàn)差別不太大,但對(duì)rigidbody屬性的訪問(wèn)還是要比transform屬性慢了一點(diǎn),后四組數(shù)據(jù)才是吃驚的根源,在組件不存在的情況下,通過(guò)屬性訪問(wèn)組件居然會(huì)有如此大的額外開(kāi)銷(xiāo)!相比之下,GetComponent方法倒是不在乎組件是否真的存在,開(kāi)銷(xiāo)一如既往。

由于屬性實(shí)現(xiàn)的代碼無(wú)法通過(guò)ILSpy查看,所以在這里我只能用猜的了。首先是,U3D在實(shí)現(xiàn)這些組件訪問(wèn)屬性的時(shí)候,必然做了各種查詢(xún)和容錯(cuò)處理,絕非簡(jiǎn)單的緩存和取用引用那么簡(jiǎn)單,這也是屬性訪問(wèn)比事先緩存引用的訪問(wèn)方式要慢那么多的原因;其次,Transform組件在每個(gè)GameObject實(shí)例上都必然存在,因此transform屬性的實(shí)現(xiàn)比其他組件訪問(wèn)屬性的實(shí)現(xiàn)必然要少那么一些步驟,這就造成對(duì)transform屬性的訪問(wèn)要比其他組件屬性快上一些;最后,當(dāng)組件不存在時(shí),對(duì)組件屬性的訪問(wèn)應(yīng)該是走入了大量的容錯(cuò)處理代碼,這就造成這種情況下屬性訪問(wèn)開(kāi)銷(xiāo)大增。

從這個(gè)實(shí)驗(yàn)又可以得出結(jié)論,我們的腳本代碼里經(jīng)常會(huì)需要訪問(wèn)gameObject引用或者某個(gè)組件的引用,最好的方式當(dāng)然是在腳本Awake的時(shí)候就把這些可能訪問(wèn)的東西都緩存下來(lái);如果需要訪問(wèn)臨時(shí)gameObject實(shí)例的某屬性或者臨時(shí)某組件的gameObject實(shí)例,在能夠確保組件一定存在的情況下,可以用屬性訪問(wèn),畢竟它們比GetComponent要快上一倍,但是如果不能確定組件是否存在,甚至是需要對(duì)組件的存在性做判斷時(shí),一定不要用對(duì)屬性訪問(wèn)結(jié)果判空的方式,而要用GetComponent,這里面節(jié)省的開(kāi)銷(xiāo)不是一點(diǎn)半點(diǎn)。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

un %d
AI