Unity學習筆記 - Assets, Objects and Serialization

Assets和Objects編程

    Asset是存儲在硬盤上的文件,保存在Unity項目的Assets文件夾內。好比:紋理貼圖、材質和FBX都是Assets。一些Assets以Unity原生格式保存數據,例如材質。另外一些Assets須要經過處理轉換到原生格式,例如FBX。緩存

    Object是一系列序列化數據,這些數據描述了具體的資源實例,這能夠是Unity使用的任意類型的資源,例如mesh,sprite,audio clip或animation clip。全部的Objects都是UnityEngine.Object的子類。編程語言

    大部分Object類型都是Unity內置的,但有兩個特殊類型:編輯器

    1. ScriptableObject容許開發者定義他們本身的數據類型。這些類型可以由Unity序列化和反序列化,而且在編輯器的Inspector窗口中進行操做。函數

    2. MonoBehaviour提供了連接到MonoScript的封裝。MonoScript是Unity的內部數據類型,其中保存了指向在具體的程序集和命名空間中的具體腳本類的引用。MonoScripte不包含任何實際可執行的代碼。性能

    Assets和Objects之間存在一對多的關係:也就是說,Asset文件內可以包含一個或多個Objects。spa

 

內部對象引用3d

    全部的UnityEngine.Objects均可以引用其餘的UnityEngine.Objects,被引用的Objects能夠和引用的Objects位於同一個Asset文件內,也能夠是由其餘Asset文件導入的。例如,材質對象一般有一個或多個紋理對象的引用,這些紋理對象一般都是從紋理資源文件導入的(例如PNG或JPG)。orm

    當序列化的時候,這些對象由兩部分分離的數據組成:文件的GUID和Local ID。文件的GUID標記了存儲資源的Asset文件。Local ID是局部惟一的(也就是說,在每一個Asset文件中,Local ID都是惟一的),標記了Asset文件中的每一個Object。對象

    文件的GUID存儲在.meta文件中。這些.meta文件是Unity第一次導入Assets時生成的,而且和Asset存儲在同一個目錄中。下圖展現了Diffuse材質及其.meta文件:

    .meta文件中包含了GUID:

    打開材質文件自己,能夠看到Local ID:

    若是在場景中有對象使用該材質進行渲染,那麼打開場景文件後,就會發現該材質對象由GUID以及Local ID來標記:

 

爲何使用GUID和Local ID?

    GUID的功能是提供文件路徑的抽象表示。只要使用GUID來關聯具體的文件,那麼文件在磁盤上的位置就可有可無了。所以能夠隨意移動文件而不須要更新引用該文件的Objects(由於這些Objects存儲的都是文件的GUID)。

    因爲一個Asset文件可能包含多個UnityEngine.Object資源,所以須要用Local ID來明確的標記每一個不一樣的Object。

    若是一個Asset文件關聯的GUID丟失的話,那麼全部對該Asset文件中的Objects的引用都將丟失。當.meta文件丟失時,Unity會從新生成。

    Unity維護了具體文件路徑與GUID的映射關係。當一個Asset被加載或導入時,就會新增一個映射項,該映射項將Asset的文件路徑和Asset文件的GUID鏈接在一塊兒。若是一個Asset的.meta文件丟失但其文件路徑沒有發生變化的話,Unity能確保從新生成的.meta中記錄的GUID是保持不變的。

    若是.meta文件在Unity關閉時丟失,或者Asset文件的路徑發生了變化,但.meta文件沒有跟着一塊兒移動的話,那麼全部對該Asset文件中的Objects的引用都將丟失。舉個例子,場景中的Cube使用了我建立的材質Diffuse:

Diffuse材質及其.meta文件存儲在Assets目錄下,若是如今在外部移動Diffuse材質到Assets/Temp目錄下,因爲沒有同時移動其.meta文件,所以Cube對其引用就會丟失:

 

資源及其導入

    非Unity原生資源必須導入進Unity中才能使用,這是經過asset importer完成的。這些improter在資源導入時會被自動調用,同時你也能夠用AssetImporter及其子類的API來經過代碼調整資源導入過程。

    資源導入的結果是一個或多個UnityEngine.Objects。在Unity中你能夠看到一個父對象包含多個子對象,例如sprite atlas:。這些對象都共享同一個GUID,由於他們的源數據來自於同一個Asset文件。Unity使用Local ID來區分他們:

    資源導入過程包含了十分耗時的操做,例如紋理壓縮。因此若是每次打開Unity都須要執行一遍資源導入過程的話將會十分低效,所以,Unity將資源導入的結果緩存在Library文件夾中:。具體來講,存儲在以Asset文件的GUID前兩個數字命名的文件夾中,這些文件夾位於目錄Library/metadata:

實際上即便是Unity原生資源,也會將導入結果存儲在對應文件中。可是原生資源不須要很長的轉換時間或從新序列化時間。 

   

實例ID

    儘管GUID和Local ID健壯耐用,可是GUID的比較很耗時,而在運行時咱們須要有個十分高效的系統。所以Unity在內部會維護一份緩存,這份緩存將GUID和Local ID轉換成獨一無二的整數,這些整數被稱爲Instance ID,每當有新的Objects添加到緩存中時,Instance ID以簡單的單調遞增的方式進行賦值。緩存維護了Instance ID,GUID和Local ID(這兩個定義了Object的源數據在磁盤上的位置)以及Object在內存中的實例(若是Object已經被加載到內存中的話)之間的映射關係。這樣UnityEngine.Objects就能夠維護相互之間的引用關係。經過Instance ID能夠快速找到對應的已經加載的Object,若是對應的Object尚未加載,那麼就能夠經過GUID和Local ID來找到Object的源數據,而後加載相應的Object。

    應用程序啓動時,項目內置對象(好比場景中使用的對象)的數據以及在Resources文件夾中的對象的數據將被初始化到Instance ID緩存中。當運行時有新的資源被導入(好比經過腳本建立的Texture2D對象),以及當從AssetBundle中加載對象時,就會在緩存中添加Instance ID項。Instance ID只有在被認爲已通過時的狀況下才會從緩存中刪除,這種狀況發生在一個AssetBundle被卸載時。當一個AssetBundle被卸載時,除了會致使對應的Instance ID被認爲已通過時,Instance ID和GUID以及Local ID之間的映射數據也會被從內存中刪除。若是AssetBundle被從新加載的話,那麼從該AssetBundle中加載的每個對象都會建立一個新的Instance ID。

    須要注意的是在具體平臺上的一些特定事件會致使Objects從內存中被刪除。好比當iOS上的應用程序被掛起時,圖形資源可能會從顯存中被刪除,若是這些資源是來自一個已經被卸載的AssetBundle,那麼Unity就沒法從新加載這些資源了,任何對這些資源的引用也將變得無效(例如出現不可見的模型(missing)使用粉色的材質(missing)來渲染)。

 

MonoScript

    一個MonoBehaviour包含了一個對MonoScript的引用,而MonoScript僅僅包含了用於定位到一個具體腳本類所需的信息,他們都不包含腳本類的可執行代碼。

    一個MonoScript中包含了三個字符串:一個程序集名,一個類名以及一個命名空間名。

    當Unity構建項目時,會將Assets文件夾下的全部腳本文件編譯到Mono程序集中。具體來講,Unity會爲在Assets文件夾中使用的每種不一樣的編程語言編譯一個程序集,而且會將在Assets/Plugins文件夾中的腳本單獨編譯到一個程序集中。在Assets/Plugins文件夾外的C#腳本會被編譯到Assetmbly-CSharp.dll中,在Assets/Plugins文件夾外的Java腳本會被編譯到Assembly-UnityScript.dll中,Assets/Plugins中的腳本會被編譯到Assembly-CSharp-firstpass.dll中。

    這些程序集(再加上預編譯的程序集)都會被包含在最終的應用程序中:

這些程序集就是MonoScript引用的程序集。和其餘資源不一樣,全部程序集在應用程序第一次啓動時會被所有加載進來。這種方式也是爲何一個AssetBundle(或者一個Scene、一個Prefab)中不包含掛載的MonoBehaviour組件中的可執行代碼。這種方式使得不一樣的MonoBehaviour能夠引用共同的具體類。

 

資源生命週期

    有兩種加載UnityEngine.Objects的方式:自動加載和顯示的手動加載。當一個Instance ID被解引用,其對應的Object當前沒有加載到內存中,而且Object的源數據可以被定位到時,Object會被自動加載。Objects還可以顯示的在腳本中手動加載,例如新建一個Texture2D或經過AssetBundle.LoadAsset方式加載一個Object。

    若是一個文件GUID和Local ID沒有對應的Instance ID,或者一個Instance ID對應的Object沒有被加載,而且其對應的GUID和Local ID是無效的話,那麼Object就不會被加載,可是引用關係仍舊會被保留,此時在Unity編輯器中就會出現"(Missing)"。

    Objects在下面三種具體的狀況下會被卸載: 

    1. 當清理未被引用的Asset時,未被引用的Objects會被自動卸載。當場景切換時或當調用Resources.UnloadUnusedAssets函數時會觸發清理未被引用的Asset。

    2. 來自Resources文件夾的Objects在調用Resources.UnloadAsset函數時會被銷燬。可是Instance ID會被保留,因此若是在Object被銷燬後,有任何先前對該對象的引用被解引用時,Unity會從新經過Instance ID找到GUID和Local ID,而後將該對象再次加載進來。

    3. 來自AssetBundle的Objects在調用AssetBundle.Unload(true)函數時會被當即銷燬,同時也會使得Instance ID,GUID和Local ID變得無效,任何對該對象的引用也會變成"(Missing)"。以後在C#中任何對該對象的訪問都會引起"NullReferenceException"異常。若是調用AssetBundle.Unload(false),從AssetBundle加載的Objects不會被銷燬,可是Instance ID對應的GUID和Local ID會變得無效,所以若是這些對象被從內存中釋放的話,Unity將沒法再次加載他們。

 

加載大層級對象

    當序列化Unity GameObjects(例如Prefabs)時,要記住整個層級都會被序列化。也就是說,層級中每一個GameObject及其組件在序列化數據中都會被獨立的表示。所以,加載和實例化具備大層級的GameObjects時會有性能影響。

    當實例化GameObjects時,實例化一個具備大層級的GameObject和實例化多個小層級的GameObjects而後將這些GameObjects組合在一塊兒相比,須要耗費更多的CPU時間。儘管實例化一個大層級的GameObject不須要組合GameObjects(不須要trampolining和SendTransformChanged回調)的CPU時間,但這些節約的CPU時間遠遠比不過讀取和反實例化大層級數據的時間。

    以前提到,序列化GameObjects時,整個層級中的GameObject及其組件數據都會被序列化 --- 即便這些數據是重複的。好比一個UI中有30個同樣的Button,那麼Button數據會被序列化30次。在加載時,這些數據都須要從磁盤上進行讀取,在加載大層級的GameObjects時,文件讀取時間會消耗大量CPU時間。所以,能夠把重複對象從整個層級中移出來,再單獨實例化後再組合到整個層級中。

相關文章
相關標籤/搜索