[unity-15] Unity性能優化三

聽到過很多用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函數的幾種重載形式:

Component GetComponent(Type type);
T GetComponent<T>() where T : Component;
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類內置的組件訪問屬性。

在實際腳本代碼編寫中,你是否經常這樣一長串代碼就輕易寫出來了:

Vector3 pos = gameObject.transform.position;
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,這裏面節省的開銷不是一點半點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章