入門必看:Unity資源加載及管理

Unity的資源加載及管理,基礎(chǔ)很重要。此篇文章作為近期梳理項目內(nèi)資源管理器的一個小總結(jié),嘗試盡量用人話將Unity對資源管理的關(guān)鍵點梳理清楚,個人覺得比較適合像我這樣剛?cè)腴T且對AssetBundle還不甚了解的家伙。

我理解的資源管理

舉一個不恰當?shù)睦觼砻枋鑫宜斫獾馁Y源管理(因為我實在想不出更合適的例子了),想象一個畫面:一個表演者,站在一個臺子后面,面向觀眾,按照規(guī)定的劇本,操作著臺子后面不被觀眾看到的箱子,從里面不斷的取出和放回各種新鮮的玩意兒,一會這么組合,一會那么拆散,博觀眾的眼球,最終完成表演。

我沒有當表演者的經(jīng)歷,雖然我很想嘗試,但想想也覺得這肯定不容易:

1、如果箱子里的東西都太大,拿起來會很費勁。

2、如果太小呢?恐怕會拿很多次。

3、不用的道具不收?放在臺子上會影響接下來的表演。

4、用過的道具收了吧。萬一收了后面需要的道具,一會兒用的時候還要再費勁拿一次,不拿的話吧還容易導(dǎo)致表演失敗。

帶著問題看文章

是選擇合適的時間取出資源,并在合適的時候釋放它們,盡可能保持較低的內(nèi)存占用;還是選擇讓資源常駐內(nèi)存,換取更快的讀取和計算速度?如何在時間和空間上做出平衡,才能最大的提升游戲體驗?我以為這些就是資源管理的目標和意義。可惜這并非易事。這不僅需要結(jié)合項目的實際情況,更需要豐富的實戰(zhàn)經(jīng)驗。

但是在這里,你將不會看到任何可以參考的經(jīng)驗或建議,因為我也不知道啊。

無論你是否單身,在Unity的世界里,你都不愁找不到對象,因為一切都是對象。

無論是紋理、音樂還是預(yù)制體,在進入Unity的世界后,都變成了各種對象供我們使用,例如紋理轉(zhuǎn)變?yōu)門exture2D或Sprite,音效文件轉(zhuǎn)變?yōu)锳udioClip,預(yù)制體變成了GameObject等等。這個由Asset(資源文件)轉(zhuǎn)變?yōu)镺bject(對象),從磁盤進入內(nèi)存的過程,就是實例化。而對資源進行的管理,本質(zhì)上是對Object的管理。

“小當家,這個黃金炒飯是怎么加載出來的?”

簡單介紹一下Unity加載資源的流程

在介紹Unity的資源加載機制之前,先舉一個生活中的例子,來輔助我們了解Unity是如何工作的。

因為我從小熱愛歐洲文學(xué),所以在這就拿我最喜歡的《三國演義》做例子,我們都知道書中多次提到“集齊七顆龍珠,就可以召喚神龍,并幫你實現(xiàn)一個愿望”這種說法。

咸魚都有夢想,何況一個上了歲數(shù)的程序員呢?但是很可惜,我們一顆龍珠都沒有,為了湊齊這七顆龍珠,我們首先要知道它們分別在哪。一摸左兜,哎?發(fā)現(xiàn)了一本《召喚神龍的小訣竅》,里面記錄了召喚神龍所必須七顆龍珠的所在位置、大小、顏色以及如何使用等非常關(guān)鍵的信息。

根據(jù)《召喚神龍的小訣竅》指引,我們知道原來第一顆龍珠藏在了素有小巴黎之稱的北京通縣,可是通縣在哪兒呢?一摸右兜,原來這還有一本1986年出版的《中國地圖》。那就放心了,出發(fā)吧!

終于,歷經(jīng)了81難,我們來到了目的地并最終找到了這顆龍珠。費這么大勁找到的龍珠,當然應(yīng)該認真記錄下來,于是我們馬上掏出一個黑皮小本本,認真的記下:“第一顆龍珠放在背后小書包的左邊縫有一個機器貓的側(cè)兜里...”

...

最終,經(jīng)歷了無數(shù)艱難險阻,我們湊齊了七顆龍珠(所以說人只要肯努力,老天就一定回饋你,至少讓你知道你浪費了時間?。=鸸庖婚W,我們召喚出了神龍... 后面實現(xiàn)了什么愿望我們不談,因為誰沒有點小秘密呢。

現(xiàn)在,讓我們來回顧一下整個過程

1、這條召喚出來的神龍,就好比我們想要實例化的對象,就比如游戲?qū)ο蟀?,因為它相對?fù)雜些。而這七顆龍珠呢,就好似組成這個游戲?qū)ο笏仨毜母鞣N組件(Component)、紋理(Texture)、網(wǎng)格(Mesh)等等。

2、《召喚神龍的小訣竅》就好比我們讀取的這個.prefab文件,它記錄了組成這個GameObject所必須的其他對象以及它們的位置。

重點來了:File GUID 及 Local ID。

File GUID

Unity會為每一個加入到Assets文件夾中的文件,創(chuàng)建一個同級同名的.meta文件,雖然文件類型的不同會影響這個.meta的具體內(nèi)容,但它們都包含一個用來標記文件身份的File GUID。

例如,如果一個資源引用了另一個外部資源,比如一個Prefab引用了其他腳本、紋理或Prefab等,則一定會標明引用資源文件的File GUID。

Local ID

如果說File GUID表示為文件和文件之間的關(guān)系,那么Local ID表示的就是文件內(nèi)部各對象之間的關(guān)系,打開一個*.Prefab文件可以很清晰的看到:

(點擊上圖,可放大查看)

一個對象通常是由一個或多個對象構(gòu)成,每個記錄在&符號后面的數(shù)字都是一個Local ID,每一個Local ID也表示這它將來也會被實例化成一個對象。也就是說,當一個prefab文件要實例化成一個GameObject時,它會自動嘗試獲取其內(nèi)部Local ID所指的那個對象。如果這個所指的對象當前還沒有被實例化出來,那么Unity會自動實例化這個對象,如此遞歸,直到所有涉及的對象都被實例化。

3、我們可以發(fā)現(xiàn)手中沒有龍珠,是因為我們手中的黑色小本本,并沒有記錄龍珠裝在書包的那個位置里;同樣,Unity通過Instance ID,來獲取或判斷一個對象是否已經(jīng)被加載完畢。Instance ID由File GUID和Local ID轉(zhuǎn)換而成,可以簡單理解成是記錄了資源所在內(nèi)存地址的寫著數(shù)字的鑰匙牌。

每當Unity讀入一個File GUID和LocalID時,就會自動將其轉(zhuǎn)換成一個簡單好記的數(shù)字牌,因為通過File GUID和Local ID定位資源的效率并沒有直接解引用一個地址那么快。

如果發(fā)現(xiàn)這個牌上并沒有掛著一把鑰匙,表示當前這個這個資源還在磁盤中,尚不在內(nèi)存里(沒有加載);相反,如果這個牌子上有一把鑰匙,表示這個資源已經(jīng)被加載完畢,你可以快速的找到并使用它。

Unity會在項目啟動后,創(chuàng)建并一直維護一張“映射表”,這張映射表記錄的就是File GUID、Local ID以及由它們轉(zhuǎn)換而成的Instance ID之間的關(guān)系,這樣下次在請求資源時就可以快速的通過查看鑰匙牌來獲取資源了。

4、剛才的例子里,因為沒有龍珠(資源沒有加載),因此我們必須經(jīng)歷一場前往小巴黎的歷險(LoadingAsset),而能夠幫助我們準確定位北京通縣的86版《中國地圖》,可以近似理解成是Unity維護的一套將GUID和FileID解析為數(shù)據(jù)源地址的機制,這套機制中的信息,來自于:

(1) 場景加載時,Unity收集了與該場景關(guān)聯(lián)的資源信息。

(2) 項目啟動時,Unity收集了所有Resources文件夾下的資源信息。

(3) 讀取AssetBundle時,Unity獲取了AssetBundle文件的頭部信息(Header)。

可以理解為:隨著Unity知道更多的信息,這套機制將能夠解析并定位更多的GUID和FileID。

5、當我們費勁千辛萬苦找到龍珠后,記錄在小本本上的7條位置,就好比7個能幫助夠準確定位內(nèi)存位置的Instance ID。想象一下,當我們下次再看到諸如《三顆龍珠召喚小神龍》這樣的小訣竅(另外一個*.prefab),便可直接打開小本本(查詢映射表中的Instance ID),對著編號及位置從書包里掏出龍珠(對InstanceID所指的內(nèi)存地址進行解引用),啪啪啪一操作,小神龍這個游戲?qū)ο缶湍芎芸毂徽賳境鰜砹?,再也不用去什么通縣了,可以節(jié)省大把時間,想想就覺的美滋滋呢。

AssetBundle

AssetBundle(阿賽特邦豆)是Unity官方推薦的資源加載方式,網(wǎng)上對AssetBundle的介紹有很多,且在了解了Unity對資源的加載機制后,其本身沒有什么特別難以理解的地方了,因此在這不過多介紹,僅挑選幾個關(guān)鍵點進行闡述。

AssetBundle的生成

生成AssetBundle有很多種方式,在此僅簡單說一下比較常用的方式,使用BuildPipeline生成AssetBundle文件。

每一次調(diào)用BuildPipleLine.BuildAssetBundles時,將會生成一批AssetBundle文件,具體數(shù)量根據(jù)傳遞AssetBundleBuild數(shù)組決定,每一個AssetBundleBuild對象將對應(yīng)一個AssetBundle及一個同名+.manifest后綴文件。其中AssetBundle文件的后綴用戶自行設(shè)置,比如".unity3d",".ab"等等;而.manifest文件是給人看的,里面有這個AssetBundle的基本信息以及非常關(guān)鍵的資源列表。

除了AssetBundleBuild數(shù)組所定的AssetBundle外,還將額外在output路徑下生成的一對與output文件夾同名的文件及一個同名.manifest后綴文件。這個同名文件可厲害了,它記錄了這批次AssetBundle之間的相互依賴關(guān)系。當然.manifest文件還是給人看的,我們可以用它分析資源間的依賴關(guān)系,但是在項目實際運行時,Unity并不會關(guān)心它。

(點擊上圖,可放大查看)

可以通過這張圖來看一下每次Build后資源的對應(yīng)關(guān)系,當然這都不如你自己親自Build一次看的清楚。

AssetBundle的加載

根據(jù)AssetBundle文件所在的位置(本地、遠端),AssetBundle有不同的加載方式,在此僅總結(jié)最常用的本地AssetBundle文件加載。

我個人將AssetBundle拆分理解為:Bundle加載Asset加載兩部分。因為AssetBundle文件可以從功能上分為兩大塊:

1、記錄文件標記、壓縮信息、文件列表的Header部分;

2、記錄資源實際內(nèi)容的Data部分。

當使用AssetBundle.LoadFromFile或LoadFromFileAsync時,在pc平臺及移動平臺上,unity僅會為我們讀取AssetBundle的header部分,并不會將bundle的data部分整個讀入內(nèi)存。

當調(diào)用上一步生成的AssetBundle對象讀取具體資源時(LoadAsset, LoadAssetAsync, LoadAllAssets),Unity會參考已經(jīng)緩存的文件列表,找到目標資源在data部分的位置并讀入到內(nèi)存中。

如果一個資源引用到了其他資源,則必須要先讀入被引用資源的AssetBundle文件,否則就會發(fā)生引用Miss。這就好似召喚神龍時,通過《召喚神龍的小訣竅》得知第一顆龍珠在北京通縣,但是當打開《中國地圖》時,北京的地方被摳了一個窟窿,我去,這樣我們就無法通過它準確定位龍珠位置了,只有六顆龍珠召喚出的神龍,當然有一部分是Miss嘍。

為了避免上面Miss的情況,在加載資源時,首先需要將該資源的依賴項全部加載完畢,不過僅需加載依賴資源的AssetBundle文件。也就是說,我們只要將該依賴AssetBundle的Header部分加載(AssetBundle.LoadFromFile或LoadFromFileAsync)就可以,這樣在真正讀取Asset時,Unity會自動處理好真實依賴的Asset,我們不用操心。

AssetBundle的依賴關(guān)系如何讀取呢?加載上面提到的那個很厲害的文件就可以了。

(點擊上圖,可放大查看)

非常簡單的獲取依賴關(guān)系的方法,通常會在項目啟動時將全部依賴關(guān)系保存下來。

AssetBundle的使用

當AssetBundle被成功加載后,調(diào)用該Assebbundle對象的LoadAsset、LoadAllAssets或?qū)?yīng)的異步版本即可加載資源,也就是實例化對象。如果這個對象已經(jīng)被加載過,Unity并不會重復(fù)加載,還記得之前所說的映射表么,被加載過的資源就好比掛上了數(shù)字牌的鑰匙,直接對地址解引用即可。

AssetBundle的卸載

如果說AssetBundle真的有什么容易出問題的地方,那恐怕就是卸載了。

在這里只說最常用的這個卸載方法吧:

public void?Unload(bool?unloadAllLoadedObjects);

一個被加載過的AssetBundle可以通過調(diào)用Unload來卸載這個Bundle下所有的Asset。但是調(diào)用這個函數(shù)時傳入的參數(shù)對卸載結(jié)果影響甚大。

Unity官方對這個函數(shù)的講解非常詳細,配圖也非常直觀,因此我只是簡單總結(jié)一下。

相同點:

無論傳入?yún)?shù)為 true 或是 false,調(diào)用Unload都可以Destroy當前AssetBundle對象,釋放之前從AssetBundle文件中的Header部分所獲取的信息。當然,被釋放的AssetBundle對象無法再使用諸如LoadAsset、LoadAllAssets等函數(shù)加載資源。

不同點:

unloadAllLoadedObjects == true:

不僅Destroy了AssetBundle這個對象,而且這個AssetBundle下包含的所有對象,只要實例化了,有一個算一個,統(tǒng)統(tǒng)釋放掉。

感覺就像

foreach(Object ?asset in assets)

if(asset != null)

delete asset;

asset = null;

比如你通過ab.LoadAsset(apple)后,將apple設(shè)置給go_0的一個Renderer,如果這時候ab.Unload(true),那go_0就傻了,咋回事兒啊,圖咋沒了呢?WTF啊。

它的好處是:不會有重復(fù)資源問題的情況發(fā)生,每次都處理的干干凈凈。

unloadAllLoadedObjects == false:

僅僅Destroy了AssetBundle這個對象,但是并沒有釋放這個AssetBundle下的任何Asset,因此如果有對象引用了這些Asset,也不會有問題。

它的風險(代價)是:下次再Load這個AssetBundle,并且通過這個AssetBundle重新讀取了這個Asset,會在內(nèi)存中重新創(chuàng)建一份,這樣如果之前的Asset沒有被釋放,那么現(xiàn)在內(nèi)存中就有兩份Asset了。

這種情況如果頻繁發(fā)生,便意味著內(nèi)存中有很多資源將“不受控制”,容易引發(fā)內(nèi)存占用過高的問題,而釋放這種不受控的資源,僅有兩種方式:

1、當沒有對象引用到這些不受控資源時,每次調(diào)用Resources.UnloadUnusedAssets,回收之。

2、加載場景時,如果加載模式?jīng)]有設(shè)置為LoadSceneMode.Additive,則會自動調(diào)用Resources.UnloadUnusedAssets。

同樣,再舉一個生活中的小例子以闡述這兩種釋放的差異吧:

小A交女朋友時喜歡送心形的石頭給對方,這天小A認識了一個女孩,并確定了關(guān)系,送了一個精心挑選的心形石頭給她,海誓山盟又云雨一番后,第二天由于感情不和等原因兩人分手了。小A是個暖男,他為了女孩能徹底忘記優(yōu)秀的自己并開始一段新的感情,約見了女孩,將之前送給女孩的石頭拿(搬)走了,從此注銷了微信消失在茫茫人海中。

確實,小A喜歡強壯的女孩,因為這樣比較有安全感。

小B交女朋友時也喜歡送石頭給對方,周一小B認識了一個女孩,并確定了關(guān)系,送了一個精心挑選的石頭給她,海誓山盟又云雨一番后,第二天由于感情不和等原因兩人分手了。但是小B家里是開石材加工場的,他并不關(guān)心這塊石頭,”送了就送了吧,至少我經(jīng)歷了浪漫的愛情“,小B這么想。并注銷了微信消失在茫茫人海中...達1天之久。

周二的時候小B重出江湖,并認識了一個新的女孩,確定了關(guān)系,第三天...第四天..啪啪啪...第七天,第二周的時候,江湖上就出現(xiàn)了一個傳說,集齊小B湊齊的七顆石頭,便可以召喚神龍,于是就回到了文章開頭我們提到的那個故事。

沒錯,小A對應(yīng)的就是Unload(true),而小B對應(yīng)的則是Unload(false)。

補充三點

1、移動Unity資源時,要在Unity編輯器內(nèi)拖動,不要在操作系統(tǒng)下剪切粘貼。因為這樣Unity會為這個文件生成一個新的File GUID及.meta文件,它會打破之前建立好的關(guān)系,讓所有引用過這個文件的prefab出現(xiàn)miss的情況。

2、實際上在項目build完成后,就已經(jīng)不存在File GUID和Local ID的概念了,轉(zhuǎn)而用相對簡單方式建立映射,這也是為什么我們在項目運行的過程中無法獲取到File GUID的原因,不過原理上它們是一樣的。

3、盡管一個AssetBundle的Header部分非常小,通常只有幾十KB,但是Unity并不能保證讀入大量AssetBundle的Header部分后資源的加載效率。因此還是按需讀取AssetBundle吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容