當前位置:
首頁 > 最新 > 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.LoadFromFile或LoadFromFileAsync時,在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吧。

寫在最後

寫在最後,估計能看到的也不多,說些輕鬆些的話,權當是對壓力的一種宣洩。

最近很忙,也很懶惰,但還是逼著自己寫一些東西,無論有沒有技術含量,但總算沒有輸給自己的惰性,起碼一個月一篇的目標算是達成了。

其實,我深知做筆記、寫博客的好處,它會讓你反覆審視那些自以為理解的知識,但結果卻發現原來不懂的太多了,可以順勢查漏補缺。但是,文章畢竟是寫給可能存在的讀者看的,總希望嚴謹些為好。可人非聖賢,總有些知識點不了解、很模糊,結論又禁不起推敲,怎麼辦呢?編嘍~~當然這也是寫博客的好處之一,可以鍛煉編造的能力,當然這也是一門學問,編的好會讓人覺得「我噻大牛哇,啊大神,好崇拜你哦」的錯覺。

以前寫過幾篇文章,總被留言的人說希望附上源碼,其實我本人並不喜歡在博客里插源碼,原因有三:

1、我看其他人的文章時,更喜歡看思路而不是大段的代碼。當然,因為一般我也看不懂。

2、我天生不是拿來主義者,我喜歡理解並嘗試用自己的方式實現,這是我個人的習慣與缺點,但是很遺憾我的年齡已經不小,習慣很難改了。當然,主要是因為我有多次複製粘貼,結果編譯直接咣咣報錯,讓心情很糟糕的體驗。

3、可能是我做程序員不久,所以我並不認為直接讀代碼是最好的交流方式,所以我會始終以我的方式來寫博客,用盡量簡單的語言以及大段的廢話將我對問題的理解描述給你。如果你讀完還是沒讀懂,那說明是我沒做好,我自己對問題理解的就不透徹,我會努力。當然,如果我寫的代碼能運行起來的話,我也願意貼上來啊,打字多TM累啊!


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 偶爾學學Unity 的精彩文章:

TAG:偶爾學學Unity |