1. 程式人生 > >Unity資源載入入門

Unity資源載入入門



引言

Unity的資源載入及管理,基礎且重要。此篇文章作為近期梳理專案內資源管理器的一個小總結,嘗試儘量用人話將Unity管理資源的關鍵點梳理清楚,個人覺得比較適合像我這樣剛入門且對AssetBundle還不甚瞭解的傢伙。




我理解的資源管理

舉一個不恰當的例子來描述我所理解的資源管理(因為我實在想不出更合適的例子了),想象一個畫面:一個表演者,站在一個臺子後面,面向觀眾,按照規定的劇本,操作著臺子後面不被觀眾看到的箱子,從裡面不斷的取出和放回各種新鮮的玩意兒,一會這麼組合,一會那麼拆散,博觀眾的眼球,最終完成表演。
我沒有當表演者的經歷,雖然我很想嘗試,但想想也覺得這肯定不容易:
1、如果箱子裡的東西都太大,拿起來會很費勁。
2、如果太小呢?恐怕會拿很多次。
3、不用的道具不收?放在臺子上會影響接下來的表演。
4、用過的道具收了吧。萬一收了後面需要的道具,一會兒用的時候還要再費勁拿一次,不拿的話吧還容易導致表演失敗。


帶著問題看文章

是選擇合適的時間取出資源,並在合適的時候釋放它們,儘可能保持較低的記憶體佔用;還是選擇讓資源常駐記憶體,換取更快的讀取和計算速度?如何在時間和空間上做出平衡,才能最大的提升遊戲體驗?我以為這些就是資源管理的目標和意義。可惜這並非易事。這不僅需要結合專案的實際情況,更需要豐富的實戰經驗。

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




無論你是否單身,在Unity的世界裡,你都不愁找不到物件,因為一切都是物件。

無論是紋理音樂還是預製體,在進入Unity的世界後,都變成了各種物件供我們使用,例如紋理轉變為Texture2D或Sprite,音效檔案轉變為AudioClip,預製體變成了GameObject等等。這個由Asset(資原始檔

)轉變為Object(物件),從磁碟進入記憶體的過程,就是例項化。而對資源進行的管理,本質上是對Object的管理。



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

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

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

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




鹹魚都有夢想,何況一個上了歲數的程式設計師呢?但是很可惜,我們一顆龍珠都沒有,為了湊齊這七顆龍珠,我們首先要知道它們分別在哪。一摸左兜,哎?發現了一本《召喚神龍的小訣竅

》,裡面記錄了召喚神龍所必須七顆龍珠的所在位置大小顏色以及如何使用等非常關鍵的資訊。




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

終於,歷經了81難,我們來到了目的地並最終找到了這顆龍珠。費這麼大勁找到的龍珠,當然應該認真記錄下來,於是我們馬上掏出一個黑皮小本本,認真的記下:“第一顆龍珠放在背後小書包的左邊縫有一個機器貓的側兜裡...”。

...

最終,經歷了無數艱難險阻,我們湊齊了七顆龍珠(所以說人只要肯努力,老天就一定回饋你,至少讓你知道你浪費了時間啊)。金光一閃,我們召喚出了神龍... 後面實現了什麼願望我們不談,因為誰沒有點小祕密呢。



現在,讓我們來回顧一下整個過程

1、這條召喚出來的神龍,就好比我們想要例項化的物件,就比如遊戲物件吧,因為它相對複雜些。而這七顆龍珠呢,就好似組成這個遊戲物件所必須的各種元件(Component)、紋理(Texture)、網格(Mesh)等等。

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

重點來了:File GUID 及 Local ID



File GUID

Unity會為每一個加入到Assets資料夾中的檔案,建立一個同級同名的.meta檔案,雖然檔案型別的不同會影響這個.meta的具體內容,但它們都包含一個用來標記檔案身份的File GUID。




例如,如果一個資源引用了另一個外部資源,比如一個Prefab引用了其他指令碼、紋理或Prefab等,則一定會標明引用資原始檔的File GUID。





Local ID

如果說File GUID表示為檔案和檔案之間的關係,那麼Local ID表示的就是檔案內部各物件之間的關係,開啟一個*.Prefab檔案可以很清晰的看到。




一個物件通常是由一個多個物件構成,每個記錄在&符號後面的數字都是一個Local ID,每一個Local ID也表示這它將來也會被例項化成一個物件。也就是說,當一個prefab檔案要例項化成一個GameObject時,它會自動嘗試獲取其內部Local ID所指的那個物件。如果這個所指的物件當前還沒有被例項化出來,那麼Unity會自動例項化這個物件,如此遞迴,直到所有涉及的物件都被例項化。

3、我們可以發現手中沒有龍珠,是因為我們手中的黑色小本本,並沒有記錄龍珠裝在書包的那個位置裡;同樣,Unity通過Instance ID,來獲取或判斷一個物件是否已經被載入完畢。Instance ID由File GUID和Local ID轉換而成,可以簡單理解成是記錄了資源所在記憶體地址的寫著數字的鑰匙牌

每當Unity讀入一個File GUID和LocalID時,就會自動將其轉換成一個簡單好記的數字牌,因為通過File GUID和Local ID定位資源的效率並沒有直接解引用一個地址那麼快。
如果發現這個牌上並沒有掛著一把鑰匙,表示當前這個這個資源還在磁碟中,尚不在記憶體裡(沒有載入);相反,如果這個牌子上有一把鑰匙,表示這個資源已經被載入完畢,你可以快速的找到並使用它。

Unity會在專案啟動後,建立並一直維護一張“對映表”,這張對映表記錄的就是File GUID、Local ID以及由它們轉換而成的Instance ID之間的關係,這樣下次在請求資源時就可以快速的通過檢視鑰匙牌來獲取資源了。

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

(1) 場景載入時,Unity收集了與該場景關聯的資源資訊。

(2) 專案啟動時,Unity收集了所有Resources資料夾下的資源資訊。

(3) 讀取AssetBundle時,Unity獲取了AssetBundle檔案的頭部資訊(Header)。

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

5、當我們費勁千辛萬苦找到龍珠後,記錄在小本本上的7條位置,就好比7個能幫助夠準確定位記憶體位置的Instance ID。想象一下,當我們下次再看到諸如《三顆龍珠召喚小神龍》這樣的小訣竅(另外一個*.prefab),便可直接開啟小本本(查詢對映表中的Instance ID),對著編號及位置從書包裡掏出龍珠(對InstanceID所指的記憶體地址進行解引用),啪啪啪一操作,小神龍這個遊戲物件就能很快被召喚出來了,再也不用去什麼通縣了,可以節省大把時間,想想就覺的美滋滋呢。



AssetBundle

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


AssetBundle的生成

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




每一次呼叫BuildPipleLine.BuildAssetBundles時,將會生成一批AssetBundle檔案,具體數量根據傳遞AssetBundleBuild陣列決定,每一個AssetBundleBuild物件將對應一個AssetBundle及一個同名+.manifest字尾檔案。其中AssetBundle檔案的字尾使用者自行設定,比如".unity3d",".ab"等等;而.manifest檔案是給人看的,裡面有這個AssetBundle的基本資訊以及非常關鍵的資源列表。

除了AssetBundleBuild陣列所定的AssetBundle外,還將額外在output路徑下生成的一對與output資料夾同名的檔案及一個同名.manifest字尾檔案。這個同名檔案可厲害了,它記錄了這批次AssetBundle之間的相互依賴關係。當然.manifest檔案還是給人看的,我們可以用它分析資源間的依賴關係,但是在專案實際執行時,Unity並不會關心它。




可以通過這張圖來看一下每次Build後資源的對應關係,當然這都不如你自己親自Build一次看的清楚。


AssetBundle的載入

根據AssetBundle檔案所在的位置(本地、遠端),AssetBundle有不同的載入方式,在此僅總結最常用的本地AssetBundle檔案載入。

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

1、記錄檔案標記、壓縮資訊、檔案列表的Header部分;

2、記錄資源實際內容的Data部分。

當使用AssetBundle.LoadFromFileLoadFromFileAsync時,在pc平臺及移動平臺上,unity僅會為我們讀取AssetBundle的header部分,並不會將bundle的data部分整個讀入記憶體。

當呼叫上一步生成的AssetBundle物件讀取具體資源時(LoadAsset, LoadAssetAsync, LoadAllAssets),Unity會參考已經快取的檔案列表,找到目標資源在data部分的位置並讀入到記憶體中。

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

為了避免上面Miss的情況,在載入資源時,首先需要將該資源的依賴項全部載入完畢,不過僅需載入依賴資源的AssetBundle檔案。也就是說,我們只要將該依賴AssetBundle的Header部分載入(AssetBundle.LoadFromFile或LoadFromFileAsync)就可以,這樣在真正讀取Asset時,Unity會自動處理好真實依賴的Asset,我們不用操心。
AssetBundle的依賴關係如何讀取呢?載入上面提到的那個很厲害的檔案就可以了。




非常簡單的獲取依賴關係的方法,通常會在專案啟動時將全部依賴關係儲存下來。


AssetBundle的使用

當AssetBundle被成功載入後,呼叫該Assebbundle物件的LoadAsset、LoadAllAssets或對應的非同步版本即可載入資源,也就是例項化物件。如果這個物件已經被載入過,Unity並不會重複載入,還記得之前所說的對映表麼,被載入過的資源就好比掛上了數字牌的鑰匙,直接對地址解引用即可。



AssetBundle的解除安裝

如果說AssetBundle真的有什麼容易出問題的地方,那恐怕就是解除安裝了。
在這裡只說最常用的這個解除安裝方法吧:


public void Unload(bool unloadAllLoadedObjects);

一個被載入過的AssetBundle可以通過呼叫Unload來解除安裝這個Bundle下所有的Asset。但是呼叫這個函式時傳入的引數對解除安裝結果影響甚大
Unity官方對這個函式的講解非常詳細,配圖也非常直觀,因此我只是簡單總結一下。

相同點:

無論傳入引數為 true 或是 false呼叫Unload都可以Destroy當前AssetBundle物件釋放之前從AssetBundle檔案中的Header部分所獲取的資訊。當然,被釋放的AssetBundle物件無法再使用諸如LoadAsset、LoadAllAssets等函式載入資源。

不同點:

unloadAllLoadedObjects == true:

不僅Destroy了AssetBundle這個物件,而且這個AssetBundle下包含的所有物件,只要例項化了,有一個算一個,統統釋放掉。
感覺就像

foreach(Object  asset in assets)
{
  if(asset != null)
  {
      delete asset;
      asset = null;
  }
}

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



它的好處是:不會有重複資源問題的情況發生,每次都處理的乾乾淨淨




unloadAllLoadedObjects == false

僅僅Destroy了AssetBundle這個物件,但是並沒有釋放這個AssetBundle下的任何Asset,因此如果有物件引用了這些Asset,也不會有問題。
它的風險(代價)是:下次再Load這個AssetBundle,並且通過這個AssetBundle重新讀取了這個Asset,會在記憶體中重新建立一份,這樣如果之前的Asset沒有被釋放,那麼現在記憶體中就有兩份Asset了。


這種情況如果頻繁發生,便意味著記憶體中有很多資源將“不受控制”,容易引發記憶體佔用過高的問題,而釋放這種不受控的資源,僅有兩種方式:

1、當沒有物件引用到這些不受控資源時,每次呼叫Resources.UnloadUnusedAssets,回收之。

2、載入場景時,如果載入模式沒有設定為LoadSceneMode.Additive,則會自動呼叫Resources.UnloadUnusedAssets


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

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



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

小B交女朋友時也喜歡送石頭給對方,週一小B認識了一個女孩,並確定了關係,送了一個精心挑選的石頭給她,海誓山盟又云雨一番後,第二天由於感情不和等原因兩人分手了。但是小B家裡是開石材加工場的,他並不關心這塊石頭,”送了就送了吧,至少我經歷了浪漫的愛情“,小B這麼想。並登出了微信消失在茫茫人海中...達1天之久。
週二的時候小B重出江湖,並認識了一個新的女孩,確定了關係,第三天...第四天..啪啪啪...第七天,第二週的時候,江湖上就出現了一個傳說,集齊小B湊齊的七顆石頭,便可以召喚神龍,於是就回到了文章開頭我們提到的那個故事。

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



補充三點

1、移動Unity資源時,要在Unity編輯器內拖動,不要在作業系統下剪下貼上。因為這樣Unity會為這個檔案生成一個新的File GUID及.meta檔案,它會打破之前建立好的關係,讓所有引用過這個檔案的prefab出現miss的情況。

2、實際上在專案build完成後,就已經不存在File GUID和Local ID的概念了,轉而用相對簡單方式建立對映,這也是為什麼我們在專案執行的過程中無法獲取到File GUID的原因,不過原理上它們是一樣的。

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