天天看點

玩轉Unity資源,對象和序列化(上)

這是一系列文章中的第二章,覆寫了Unity5的Assets,Resources和資源管理

本文将從Unity編輯器和運作時兩個角度出發,主要探讨以下兩方面内容:Unity序列化系統内部細節以及Unity如何維護不同對象之間的強引用。另外還會讨論對象與資源的技術實作差别。

譯注:除非特别說明,下文中所有的“資源”均指代“Asset”。

本文内容是了解在Unity中如何高效加載和解除安裝資源的基礎。正确的資源管理對縮短加載時間并減少記憶體占用來說至關重要。今天先為大家分享上半部分内容。

在了解Unity如何確定萬無一失地管理資料之前,首先要知道Unity是如何識别并序列化資料的。首先第一點,要正确區分資源(Asset)和對象(UnityEngine.Objects)。

資源(Asset)是硬碟中的檔案,存儲在Unity工程的Assets檔案夾内。例如,紋理(Texture),材質(Material)和FBX檔案等,它們都是資源。一些資源的資料格式是Unity原生支援的,例如材質。有些資源則需要轉換為原生的資料格式後才能被Unity使用,例如FBX檔案。

UnityEngine.Object,或者說以大寫字母O開頭的Object——對象,代表序列化資料的集合,表示某個資源的具體執行個體。它可以是Unity引擎使用的任何類型的資源,例如網格,Sprite,音頻剪輯或動畫剪輯。所有的對象(Object)都是UnityEngine.Object基類的子類。

幾乎所有的對象(Object)類型都是内建的,其中有兩種比較特殊的類型。

ScriptableObject為開發者提供了一套便捷的系統,供開發者自定義資料類型。這些類型可以被Unity直接序列化或反序列化,并在Unity編輯器的檢視器視窗中進行操作。

MonoBehaviour提供了連結MonoScript的容器。MonoScript是一種内部資料類型,Unity用它儲存對某個特定程式集和命名空間中特定腳本類的引用,MonoScript本身不包含任何實際的可執行代碼。

資源(Asset)與對象(Object)是一種一對多的關系,即一個資源檔案可能會包括多個Object。

所有UnityEngine.Objects都可以引用其他的UnityEngine.Objects。這裡“其他的Object”可能存在于相同的資源檔案中,或需要從其他資源檔案導入。例如,一個材質Object通常有一個或多個紋理Object的引用。這些紋理Object一般是從一個或多個紋理資源檔案中導入的(例如PNG或JPG檔案)。

序列化後,這些引用由兩部分資料組成:檔案GUID和本地ID。檔案GUID用于識别資源(Asset)檔案中目标資源(Resource)的存儲位置。而本地唯一(1)的ID負責識别單個資源檔案中的Object,因為一個資源檔案可能會包含多個Object。

檔案GUID存儲于.meta檔案中。Unity會在首次導入資源檔案時生成.meta檔案,并和資源檔案一起存儲在相同的目錄中。

上述的識别和引用系統可以使用文本編輯器檢視:

建立一個全新的Unity工程,更改編輯器設定,将Edit - Project Settings - Editor中的Version Control設為Visible Meta Files,并将Asset Serialization設為文本。

建立材質并向工程中導入一個紋理。将材質賦給場景中的一個立方體,儲存場景。

使用文本編輯器打開這個材質對應的.meta檔案。在檔案頂端附近會有一行被标示為“guid”,該行定義了材質資源檔案的檔案GUID。

如需檢視本地ID,使用文本編輯器打開材質檔案,材質Object的定義大緻如下:

在上面的例子中,前面有&符号的數字就是材質的本地ID。如果這個材質的Object位于一個檔案GUID為“abcdefg”的資源檔案中,則該材質Object的唯一識别符就是檔案GUID“abcdefg”和本地ID“2100000”的組合。

在Unity中,為什麼要使用檔案GUID和本地ID這套系統呢?答案是為穩定服務,也是為了提供一套靈活的、無關具體平台的工作流程。檔案GUID提供了檔案存儲位置的抽象,這樣一個檔案GUID就對應一個具體的檔案,這個具體的檔案存儲在什麼位置也就無關緊要了。是以我們才能随意移動這個檔案而不破壞所有相關Object對這個檔案的引用。

任何資源(Asset)檔案中都可能含有(或通過導入産生)多個UnityEngine.Object資源(Resource),是以需要一個本地ID來對其中的Object做明确區分。

如果與資源檔案相關聯的檔案GUID丢失,則所有對該資源檔案中的Object的引用都會被破壞。這就是必須保證.meta檔案具有和資源檔案相同的檔案名并存儲在同一目錄下的原因。注意Unity會重新生成丢失或被删除的.meta檔案。

Unity編輯器負責維護一張檔案路徑與檔案GUID之間關系的映射表。隻要資源檔案被讀取或導入,這個映射關系就會被建立,映射會将資源的具體位置和資源的檔案GUID進行關聯。Unity編輯器處于打開狀态時,假設一個檔案的.meta意外丢失,并且該資源檔案的路徑沒有改變,編輯器可以保證這個資源會被配置設定到相同的檔案GUID。

如果在Unity編輯器處于關閉狀态時丢失.meta檔案,或資源檔案被移動但沒有移動對應的.meta檔案時,所有對資源檔案中的Object的引用都會丢失。

正如前面深入了解資源與對象中所說的一樣,不能被Unity直接支援的資源類型必須經過導入才可以使用——使用資源導入器來完成。這些導入器是自動調用的,您也可以使用AssetImporter在腳本中調用API及其子類。例如,在導入單獨的紋理資源例如PNG和JPG時,TextureImporter API提供了導入時要使用的相關設定的通路。

導入過程最終的産物是一系列UnityEngine.Object。在Unity編輯器中,這些對象會具體表現為父資源下的多個子資源,例如作為Sprite Atlas導入的紋理材質,其下屬會有多個嵌套的Sprite。每一個對象都會使用相同的檔案GUID,因為它們的源資料都存儲在同一個資源檔案中。它們在紋理資源中的具體區分工作則使用本地ID來完成。

導入過程中會将源資源轉換為比對Unity編輯器中標明的目标平台的格式。導入過程可能會牽涉一些重量級操作,例如紋理壓縮。如果每次打開Unity編輯器時都要執行這些操作,那效率就太低了。

為了解決這一問題,我們将資源導入的結果緩存在Library檔案夾中。具體就是,導入程序的結果将會存儲在以資源檔案GUID頭兩位作為名稱的檔案夾中。這些檔案夾位于 Library/metadata/ 目錄下。各個不同的對象會被序列化後存儲在一個二進制檔案中,檔案使用資源檔案的GUID來命名。

這對所有資源都是一樣的,不僅僅是非原生資源。隻不過Unity原生支援的資源不需要對其進行轉換或序列化處理。

上半部分的内容主要介紹了資源(Asset)和對象(UnityEngine.Objects)的差別,以及檔案GUID和本地ID二者的關聯和差異。下半部分将為大家介紹第三種ID:對象的執行個體ID,并看看這些ID對資源在記憶體與顯存中的加載和解除安裝分别有着怎樣的作用。

在檔案中,本地ID是唯一的。即在一個資源檔案中,裡面包含的本地ID都是不重複的。

在内部,這種緩存被稱為PersistentManager。實際的轉換工作在在Unity的C++ Remapper類中進行,Remapper類沒有提供任何C# API調用接口。

運作時建立資源的示例是在腳本中建立Texture2D對象:var myTexture = new Texture2D(1027, 768);

程式運作時對象并沒有被解除安裝卻被從記憶體中移除的情況通常會發生在Unity失去了對圖形内容的控制的時候。例如,當手機應用被挂起并被強制在背景運作。這種情況下,手機作業系統通常會将所有的圖形資源從GPU顯存中強行解除安裝。之後APP再回到前台運作時,Unity不得不重新向GPU上傳需要的材質、着色器和網格資料,以便恢複場景的正常渲染。

到此整個Unity内部資源管理與對象引用及序列化的内容就結束了,希望看完本文的你對如何合理配置設定Unity項目結構都有了比較清晰的概念。

繼續閱讀