Unity對象和序列化原理介紹

Unity使用了很多年了,在這裏我敢說很多人對Unity對象和序列化原理並不是很清楚,比如Asset與Object之間有啥區別?如何管理Asset?Unity的序列化是咋回事等等,還有在項目中遇到資源丟失,它爲什麼丟失了等等,本篇就給讀者介紹Unity對象的內部運行原理,下面就從Asset與Object對象區別說起。

Asset與對象

先介紹Asset,它是存儲在Unity項目中的文件夾中,比如紋理,3D模型,音頻文件等等。Unity對象也就是我們說的Object,它是一組序列化的數據,比如Mesh,Sprite,AudioClip或者AnimationClip,所有對象都是Object基類的子類。
Asset與對象之間是一種一對多的關係,換句話說,任何給定的Asset文件都包含一個或多個Objects。
繼續介紹,我們知道對象之間可以互相引用,引用的對象可以保存在同一個Asset文件中,也可以從其他Asset文件導入,比如:材質 Object 通常具有一個或多個對紋理對象的引用,這些紋理對象通常從一個或多個紋理資源文件導入。
序列化時,這些引用由兩個單獨的數據組成:文件GUID 和 本地ID,文件GUID 標識存儲目標資源的資源文件。本地惟一的(引用)ID標識資源文件中的每個對象,因爲一個資源文件可能包含多個對象。文件GUID 存儲在 .meta 文件中。這些 .meta 文件是在 Unity 首次導入資源時生成的,並存儲在與資源相同的目錄中。如下圖所示:
在這裏插入圖片描述
再看一下對應的GUID標識,如下圖所示:
在這裏插入圖片描述
圖中guid即是GUID的標識,如果想看一下ID的標識,我們打開一個mat文件,如下圖所示:
在這裏插入圖片描述
在上面的示例中,以&符號開頭的數字是對象的本地ID,如果 Object 位於由文件GUID“f0d883fa…”標識的Asset內,則可以將 Object 唯一標識爲文件GUID“f0d883fa…” 和本地ID“2100000” 的組合。
爲什麼 Unity 的文件GUID 和本地ID 是必需的?答案是可以讓資源和對象獨立於平臺。
GUID 標識提供了特定位置的抽象,只要文件標識GUID 可以與文件相關聯,該文件在磁盤上的位置就變得無關緊要了,該文件可以自由移動,而無需更新引用該文件的所有對象。這個就好比,我們Unity工程中的文件可以隨意移動位置都沒有關係。
由於Assets中包含多個Object,我們需要ID對她們加以區分,如果與Asset文件關聯的文件GUID 丟失,則對該Asset文件中所有對象的引用也將丟失,這就是爲什麼重要的是 .meta 文件必須保持與相關文件名相同的文件名字,並與相關的Asset文件保存在同一文件夾中,請注意,如果Asset放錯位置,Unity會重新生成新的 .meta 文件。

Unity Editor 具有指向已知文件GUID 的文件路徑映射,只要加載或導入Asset,就會記錄該映射,映射將Asset的路徑鏈接到 GUID。如果在 .meta 文件丟失且Asset路徑未更改時, Unity 編輯器處於打開狀態,則編輯器可以確保Asset保留相同的文件GUID。
如果在關閉 Unity 編輯器時丟失 .meta 文件,或者Asset的路徑發生更改而 .meta 文件並沒有隨Asset一起移動時,那麼對該資產中對象的所有引用都將被破壞。我們在上傳資源到服務器時,經常遇到引用丟失就是因爲meta更改了或者丟失了。

資源導入

導入過程的結果是一個或多個 UnityEngine.Objects,例如嵌套在Asset下的多個Sprite已導入爲Sprite圖集。這些對象中的每一個都將共享文件GUID,因爲它們的源數據存儲在同一Asset文件中。它們將通過本地ID在導入的紋理 Asset 中區分。

導入過程將源資源轉換爲適合 Unity Editor 中選擇的目標平臺(如 windows)的格式。導入過程可以包括許多其他特殊操作,例如紋理壓縮,由於這通常是一個耗時的過程,因此導入的Asset會緩存在 Library 文件夾中,從而無需在下次編輯器啓動時再次重新導入 Assets。

具體來說,導入過程的結果存儲在以 Asset 的文件GUID 的前兩位數命名的文件夾,該文件夾存儲在 Library / metadata / 文件夾中,Asset中的各個對象被序列化爲單個二進制文件,其名稱與Asset的文件GUID 相同。如下圖所示:
在這裏插入圖片描述

序列化和實例

Unity 內部維護着一個緩存,在向緩存註冊新對象時以遞增的順序分配實例ID,緩存維護實例ID、文件GUID 和定義對象之間的映射,以及對象在內存中的實例。這允許Objects 可以維護對彼此的引用,在解析實例ID引用時,可以快速返回由實例ID 表示的已加載對象,如果沒有加載目標對象,則可以將文件GUID 和本地ID 解析爲對象的源數據,從而允許 Unity 即時加載對象。
程序在啓動時,實例ID 緩存初始化項目所需要的所有對象的數據,包括Resources中和導入的Asset資源。僅當卸載對文對象時,纔會從緩存中刪除實例ID。發生這種情況時,將刪除實例ID和對應的文件GUID 和本地ID 之間的映射以節省內存。如果重新加載 AssetBundle,將爲從重新加載的 AssetBundle 加載的每個 Object 創建一個新的實例ID。資源文件GUID 不能在運行時查詢。

MonoScripts

MonoScript 包含三個字符串:程序集名稱,類名稱和命名空間,構建項目時,Unity 會將 Assets 文件夾中的所有鬆散腳本文件編譯爲 Mono 程序集,插件子文件夾之外的 C#腳本放在 Assembly-CSharp.dll 中,Plugins 子文件夾中的腳本放在 Assembly-CSharp-firstpass.dll 中,依此類推,此外,Unity 2017.3 後的版本還引入了定義自定義託管程序集的功能。

這些程序集以及預構建的程序集DLL 文件都包含在 Unity 應用程序中,它們也是 MonoScript 引用的程序集,與其他資源不同,Unity 應用程序中包含的所有程序集都在應用程序啓動時加載。

資源生命週期

要減少加載時間並管理應用程序的內存佔用,理解 UnityEngine.Objects 的資源生命週期非常重要。加載對象時,Unity 會嘗試通過將每個引用的文件GUID 和本地ID 轉換爲實例ID ,加載對象接口比如:AssetBundle.LoadAsset。加載的對象分兩種:
一是,實例ID 引用當前未加載的 Object。
二是,實例ID 有在緩存中註冊的有效文件GUID 和本地ID。
如果文件GUID 和本地ID 沒有實例ID,或者要卸載的對象的實例ID 引用無效文件GUID 和本地ID,則保留引用但不會加載實際的對象,比如我們在開發中運行應用程序時,或在“場景視圖(Scene View)” 中,“(Missing)” 對象將以不同的方式顯示,具體取決於其類型。例如,網格看起來是不可見的,而紋理可能看起來是洋紅色,丟失了,就是這個原因造成的。

下面給讀者介紹關於卸載對象的原理:
一:對象在發生未使用的資源清理時自動卸載,當場景被破壞或者切換時(即 SceneManager.LoadScene 時),或者當腳本調用Resources.UnloadUnusedAssets API 時,此過程僅卸載未引用的對象; 只有當沒有 Mono 變量持有對對象的引用,並且沒有其他對象持有對對象的引用時,纔會卸載對象。此外,請注意,不會卸載任何標有HideFlags.DontUnloadUnusedAsset和HideFlags.HideAndDontSave的對象。
二:可以通過調用Resources.UnloadAsset API 卸載在Resources文件夾的對象,這些對象的實例ID 仍然有效,並且仍將包含有效的文件GUID 和本地ID,如果任何 Mono 變量或其他 Object 包含對使用Resources.UnloadAsset卸載的 Object 的引用,則只要取消引用,就會重新加載該 Object。
三:在調用AssetBundle.Unload(true)API 時,會立即自動卸載源 AssetBundles 的對象,這使得對象實例ID 的文件GUID 和本地ID 無效,並且對已卸載對象的任何實時引用將變爲“(Missing)”引用。從C#腳本嘗試訪問卸載對象上的方法或屬性時將產生NullReferenceException。這個在編程過程中也會遇到。
四:如果調用 AssetBundle.Unload(false),使得卸載的 AssetBundle 對象將不會被銷燬,但 Unity 將使實例ID的文件GUID 和本地ID 引用無效,如果從內存中卸載這些對象並且仍然存在對已卸載對象的實時引用,則 Unity 將無法重新加載這些對象。

遊戲對象層次結構

序列化 Unity 遊戲對象的層次結構時,比如做預製體,層次結構中的每個 GameObject 和 Component 將在序列化數據中單獨表示,這對加載和實例化 GameObjects 層次結構所需的時間產生了一定的影響。
在創建任何 GameObject 層次結構時,CPU 時間以幾種不同的方式使用:
一:讀取源數據(來自存儲,來自 AssetBundle,或來自另一個 GameObject 等)
二:實例化新的 GameObjects 和組件
三:在主線程上喚醒新的 GameObjects 和 Components
讀取源數據的時間隨序列化到層次結構中的組件和遊戲對象的數量線性增加,並且還要乘以數據源的速度。我們加載資源時,從內存中的其他位置讀取數據要比從存儲設備加載數據快得多,因此,當在存儲速度較慢的平臺上加載預製體時,從存儲中讀取預製體的序列化數據所花費的時間可能很快超過實例化預製體所花費的時間。也就是說,加載操作的成本與存儲I / O 時間有關。

在序列化單個預製體時,每個 GameObject 和組件的數據都是單獨序列化的,這可能會重複複製數據,例如,具有 30 個相同組件的 UI 將具有序列化30次的相同對象,從而產生大量二進制數據。在加載時,必須從磁盤讀取這30個重複元素的每一個遊戲對象(gameobject) 和組件(compontent) 的數據,然後再傳輸到新實例化的對象。此文件讀取時間是實例化大型預製體的總體成本的重要因素,比較大的結構應該在模塊化塊中實例化,然後在運行時拼接在一起,這是一種理想情況。

總結

當實例化新的 GameObject(遊戲對象) 時,使用 GameObject.Instantiate 接受父參數的重載變量,使用此重載可避免爲新 GameObject 分配根變換層次結構,效率提升很多。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章