? 一级特黄录像免费播放中文,久久精品久久久久久久精品
聯(lián)系我們

給我們留言

聯(lián)系我們

地址:福建省晉江市青陽街道洪山路國際工業(yè)設計園納金網(wǎng)

郵箱:info@narkii.com

電話:0595-82682267

(周一到周五, 周六周日休息)

當前位置:主頁 > 3D教程 > 3D技巧分享

玩轉(zhuǎn)Unity資源,對象和序列化

來源: 未知 | 責任編輯:CHH | 發(fā)布時間: 2017-06-22 14:20 | 瀏覽量:
本文將從Unity3d編輯器和運行時兩個角度出發(fā),主要探討以下兩方面內(nèi)容:Unity序列化系統(tǒng)內(nèi)部細節(jié)以及Unity如何維護不同對象之間的強引用。另外還會討論對象與資源的技術實現(xiàn)差別。

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

  本文內(nèi)容是理解在Unity中如何高效加載和卸載資源的基礎。正確的資源管理對縮短加載時間并減少內(nèi)存占用來說至關重要。今天先為大家分享上半部分內(nèi)容。

  1.1. 深入理解資源和對象

  在理解Unity如何確保萬無一失地管理數(shù)據(jù)之前,首先要知道Unity是如何識別并序列化數(shù)據(jù)的。首先第一點,要正確區(qū)分資源(Asset)和對象(UnityEngine.Objects)。

  Asset

  資源(Asset)是硬盤中的文件,存儲在Unity工程的Assets文件夾內(nèi)。例如,紋理(Texture),材質(zhì)(Material)和FBX文件等,它們都是資源。一些資源的數(shù)據(jù)格式是Unity原生支持的,例如材質(zhì)。有些資源則需要轉(zhuǎn)換為原生的數(shù)據(jù)格式后才能被Unity使用,例如FBX文件。

 UnityEngine.Object

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

  特殊的Object類型

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

  ScriptableObject為開發(fā)者提供了一套便捷的系統(tǒng),供開發(fā)者自定義數(shù)據(jù)類型。這些類型可以被Unity直接序列化或反序列化,并在Unity編輯器的檢視器窗口中進行操作。

  MonoBehaviour提供了鏈接MonoScript的容器。MonoScript是一種內(nèi)部數(shù)據(jù)類型,Unity用它保存對某個特定程序集和命名空間中特定腳本類的引用,MonoScript本身不包含任何實際的可執(zhí)行代碼。

  一對多關系

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

  1.2. 對象之間的引用

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

  序列化后,這些引用由兩部分數(shù)據(jù)組成:文件GUID和本地ID。文件GUID用于識別資源(Asset)文件中目標資源(Resource)的存儲位置。而本地唯一(1)的ID負責識別單個資源文件中的Object,因為一個資源文件可能會包含多個Object。

  文件GUID(.meta)

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

  上述的識別和引用系統(tǒng)可以使用文本編輯器查看:

  創(chuàng)建一個全新的Unity工程,更改編輯器設置,將Edit - Project Settings - Editor中的Version Control設為Visible Meta Files,并將Asset Serialization設為文本。

  新建材質(zhì)并向工程中導入一個紋理。將材質(zhì)賦給場景中的一個立方體,保存場景。

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

  1

  本地ID(具體文件)

  如需查看本地ID,使用文本編輯器打開材質(zhì)文件,材質(zhì)Object的定義大致如下:

  2

  在上面的例子中,前面有&符號的數(shù)字就是材質(zhì)的本地ID。如果這個材質(zhì)的Object位于一個文件GUID為“abcdefg”的資源文件中,則該材質(zhì)Object的唯一識別符就是文件GUID“abcdefg”和本地ID“2100000”的組合。

  1.3. 為什么要用文件GUID和本地ID?

  工作流程

  在Unity中,為什么要使用文件GUID和本地ID這套系統(tǒng)呢?答案是為穩(wěn)定服務,也是為了提供一套靈活的、無關具體平臺的工作流程。文件GUID提供了文件存儲位置的抽象,這樣一個文件GUID就對應一個具體的文件,這個具體的文件存儲在什么位置也就無關緊要了。因此我們才能隨意移動這個文件而不破壞所有相關Object對這個文件的引用。

  任何資源(Asset)文件中都可能含有(或通過導入產(chǎn)生)多個UnityEngine.Object資源(Resource),因此需要一個本地ID來對其中的Object做明確區(qū)分。

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

  Unity編輯器維護映射表

  Unity編輯器負責維護一張文件路徑與文件GUID之間關系的映射表。只要資源文件被讀取或?qū)?,這個映射關系就會被建立,映射會將資源的具體位置和資源的文件GUID進行關聯(lián)。Unity編輯器處于打開狀態(tài)時,假設一個文件的.meta意外丟失,并且該資源文件的路徑?jīng)]有改變,編輯器可以保證這個資源會被分配到相同的文件GUID。

  如果在Unity編輯器處于關閉狀態(tài)時丟失.meta文件,或資源文件被移動但沒有移動對應的.meta文件時,所有對資源文件中的Object的引用都會丟失。

  1.4. 復合資源和導入器

  資源導入器

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

  導入過程最終的產(chǎn)物是一系列UnityEngine.Object。在Unity編輯器中,這些對象會具體表現(xiàn)為父資源下的多個子資源,例如作為Sprite Atlas導入的紋理材質(zhì),其下屬會有多個嵌套的Sprite。每一個對象都會使用相同的文件GUID,因為它們的源數(shù)據(jù)都存儲在同一個資源文件中。它們在紋理資源中的具體區(qū)分工作則使用本地ID來完成。

  Library文件夾

  導入過程中會將源資源轉(zhuǎn)換為匹配Unity編輯器中選定的目標平臺的格式。導入過程可能會牽涉一些重量級操作,例如紋理壓縮。如果每次打開Unity編輯器時都要執(zhí)行這些操作,那效率就太低了。

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

  這對所有資源都是一樣的,不僅僅是非原生資源。只不過Unity原生支持的資源不需要對其進行轉(zhuǎn)換或序列化處理。

  上半部分的內(nèi)容主要介紹了資源(Asset)和對象(UnityEngine.Objects)的區(qū)別,以及文件GUID和本地ID二者的關聯(lián)和差異。下半部分將為大家介紹第三種ID:對象的實例ID,并看看這些ID對資源在內(nèi)存與顯存中的加載和卸載分別有著怎樣的作用。

  Footnotes(腳注)

  在文件中,本地ID是唯一的。即在一個資源文件中,里面包含的本地ID都是不重復的。

  在內(nèi)部,這種緩存被稱為PersistentManager。實際的轉(zhuǎn)換工作在在Unity的C++ Remapper類中進行,Remapper類沒有提供任何C# API調(diào)用接口。

  運行時創(chuàng)建資源的示例是在腳本中創(chuàng)建Texture2D對象:var myTexture = new Texture2D(1027, 768);

  程序運行時對象并沒有被卸載卻被從內(nèi)存中移除的情況通常會發(fā)生在Unity失去了對圖形內(nèi)容的控制的時候。例如,當手機應用被掛起并被強制在后臺運行。這種情況下,手機操作系統(tǒng)通常會將所有的圖形資源從GPU顯存中強行卸載。之后APP再回到前臺運行時,Unity不得不重新向GPU上傳需要的材質(zhì)、著色器和網(wǎng)格數(shù)據(jù),以便恢復場景的正常渲染。

  1.5. 序列化和實例

  實例ID

  盡管文件GUID和本地ID已準備妥當,它們可以強有力地維護資源之間的關系,但還有一個問題,GUID比較效率低下,我們需要為運行時準備一個效率更高的解決方案。Unity在內(nèi)部維護著一個緩存表(2),負責將文件GUID和本地ID轉(zhuǎn)換成為整數(shù)數(shù)值,這個數(shù)值在本次會話中是唯一的,稱作實例ID。實例ID會簡單地以單調(diào)遞增的方式分配給緩存中新注冊的對象。

  緩存負責維護實例ID與文件GUID和本地ID定義的對象源數(shù)據(jù)位置以及對象在內(nèi)存中的地址(如果存在)的映射。 這樣Unity就能夠強而有力地保證它們互相之間的引用關系。通過解析實例ID,我們能夠快速找到并返回ID對應的已載入實例。如果目標沒有被加載,則Unity會通過文件GUID和本地ID解析獲得對象的源數(shù)據(jù),實時載入對象。

  啟動時,實例ID緩存與所有工程內(nèi)建的對象(例如在場景中被引用),以及Resources文件夾下的所有對象,都會一起被初始化。如果在運行時(3)導入了新的資源,或從AssetBundle中載入了新的對象,緩存會被更新并為這些對象添加相應條目。實例ID僅在失效時才會被從緩存中移除,當提供了指定文件GUID和本地ID的AssetBundle被卸載時就會產(chǎn)生移除操作。

  卸載AssetBundle會使實例ID失效,實例ID與其文件GUID和本地ID之間的映射會被刪除以便節(jié)省內(nèi)存。重新載入AssetBundle后,載入的每個對象都會獲得一個新的實例ID。

  關于AssetBundle隱式卸載的更深層次討論,參考AssetBundle Usage Patterns中的管理已加載資源章節(jié)。

  注意事項(IOS上掛起狀態(tài))

  注意在某些平臺上,有一些系統(tǒng)事件會使得對象在內(nèi)存中被強行卸載。例如iOS平臺上,當一個app處于掛起狀態(tài)時,圖形資源就會從顯存中卸載。如果這些對象都來自于一個已被卸除的AssetBundle時,Unity將無法再次從源數(shù)據(jù)處加載這些對象。任何已有的對這些對象的引用都會失效。這個例子中,導致的后果就是出現(xiàn)網(wǎng)格不可見(丟失),或模型的紋理和材質(zhì)呈現(xiàn)為洋紅色(Shader丟失)。

  提示

  提示:在運行時,對上述控制流程的描述并非完全準確。文件GUID和本地ID的比較操作在載入負擔較重時的效率也會下降。構(gòu)建Unity工程時,文件GUID和本地ID,確切地說會被映射到一種更簡單的格式中。不過概念還是大致相同的,在考慮“運行時”的時候以文件GUID和本地ID的工作方式為思路還是有一些參考意義的。

  這也是在運行時資源的文件GUID無法被調(diào)取的原因。

  1.6.MonoScripts

  理解MonoBehaviour很重要的一點是知道它有一個對MonoScript的引用。MonoScript的用途非常簡單,里面包括了定位某個具體的編程類所需要的信息。這兩種對象都沒有程序類的可執(zhí)行代碼。

  一個MonoScript含有三個字符串:程序庫名稱,類名稱,命名空間。

  程序庫(dll)

  構(gòu)建工程時,Unity會收集Assets文件夾中獨立的腳本文件并將它們編譯,組成一個Mono程序庫。要特別說明的是,Unity會將Assets目錄中的語言分開編譯,Assets/Plugins目錄中的腳本同理。Plugins子目錄之外的C#腳本會放在Assembly-CSharp.dll中。而Plugins及其子目錄中的腳本則放置在Assembly-CSharp-firstpass.dll中,以此類推。

  這些程序庫(加上預編譯好的DLL程序庫)將被包含在最終構(gòu)建的Unity應用程序中。這些程序庫都會被MonoScript所引用。與其他類型的資源不同,Unity應用程序中的所有應用程序都會在程序第一次啟動時被加載。

  MonoScript就是為什么AssetBundle(或一個場景,一個Prefab)中的MonoBehaviour組件不包含有任何實際可執(zhí)行代碼的原因。這樣就可以讓不同的MonoBehaviour引用某個共享類,即便這些MonoBehaviour不在同一個AssetBundle中。

  1.7.資源(Resource)生命周期

  UnityEngine.Objects從內(nèi)存中加載或卸載的時間點是定義好的。為了縮短程序載入時間,管理應用程序的內(nèi)存足跡,理解UnityEinge.Object的資源生命周期是很重要的。

  加載UnityEngine.Object的兩種方式

  有兩種加載UnityEngine.Object的方式:自動加載或外部加載。

  當對象的實例ID與對象本身解引用,對象當前未被加載到內(nèi)存中,而且可以定位到對象的源數(shù)據(jù),此時對象會被自動加載。對象也可以外部加載,通過在腳本中創(chuàng)建對象或調(diào)用資源加載API來載入對象(例如AssetBundle.LoadAsset)。

  對象加載后,Unity會嘗試修復任何可能存在的引用關系,通過將每個引用的文件GUID和本地ID轉(zhuǎn)化成為實例ID的方式。

  一旦對象的實例ID被解引用且滿足以下兩個標準時,對象會被強制加載:

  實例ID引用了一個沒有被加載的對象。

  實例ID在緩存中存在對應的有效GUID和本地ID。

  這種情況通常會在引用被加載并解析后很短的一段時間內(nèi)發(fā)生。

  如果文件GUID和本地ID沒有實例ID,或一個已卸載對象的實例ID引用了非法的文件GUID和本地ID,則引用本身會被保留,但實際對象不會被加載。在Unity編輯器中表現(xiàn)為“(空)”引用。在運行的應用程序中,或場景視圖里,“(空)”對象通常以多種方式表示,這取決于丟失對象的類型:網(wǎng)格會變得不可見,紋理呈現(xiàn)為洋紅色等等。

  對象被卸載的三種情況

  對象被卸載有以下三種情況:

  閑置資源清理進程開始后,一些對象會被自動卸載。該過程通常會在切換場景切不保留原場景(例如調(diào)用了非疊加的場景切換API Application.LoadLevel),或者腳本中調(diào)用了Resources.UnloadUnusedAssets時自動觸發(fā)。該進程僅卸載沒有被引用的對象:對象僅在Mono變量不存在對其的引用,且不存在引用該對象的其他活動對象時被卸載。

  Resources目錄中的對象可以通過調(diào)用Resources.UnloadAsset API主動卸載。卸載后對象的實例ID會保持可用狀態(tài),對文件GUID和本地ID的條目會被保留且仍然有效。如果有Mono變量或其他有指向該對象的活動對象引用了被Resources.UnloadAsset卸載的對象,則該對象會在任意有效的引用被解引用時立刻重新加載。

  調(diào)用AssetBundle.Unload(true) API時,加載自AssetBundle的對象會被立刻自動卸載。該操作會釋放對象實例ID的文件GUID和本地ID引用,任何對卸載對象的引用都會變成“(空)”引用。C#腳本中,任何試圖訪問已卸載對象上的方法和屬性都會導致拋出空引用異常(NullReferenceException)。

  如果調(diào)用了AssetBundle.Unload(false),被卸載的AssetBundle中仍然處于激活狀態(tài)的對象不會被回收,但Unity會釋放其實例ID的文件GUID和本地GUID引用。之后假如它們被從內(nèi)存中卸載,只剩下對這些被卸載對象額引用,Unity無法再次重新加載這些對象(4)。

  1.8.載入大型結(jié)構(gòu)樹(Hierarchies)

  當序列化含有大量Unity游戲?qū)ο蟮慕Y(jié)構(gòu)樹時(例如序列化Prefab),要記住一點,即整個結(jié)構(gòu)樹都會被完全序列化。這就是說,結(jié)構(gòu)樹中的每一個游戲?qū)ο蠛徒M件都會在序列化數(shù)據(jù)中單獨表示。這會對游戲?qū)ο蠼Y(jié)構(gòu)的載入與實例化的耗時帶來有趣的影響。

  假設一個代碼塊實例化了一定數(shù)量的游戲?qū)ο?,結(jié)構(gòu)樹非常龐大的單個Prefab的實例化,會比分別實例化結(jié)構(gòu)樹的多個模塊然后運行時組合花費更多的CPU時間。

  深度分析底層數(shù)據(jù)后發(fā)現(xiàn),實例化和喚醒游戲?qū)ο笏ㄙM的CPU時間在各種情況下大致是相同的,單個獨立的Prefab的實例化和喚醒僅需非常少量的CPU時間(因為不需要tranmpolining和SendTransformChanged回調(diào))。但是,這些細小的時間節(jié)省相對于花在讀取和序列化數(shù)據(jù)上的時間是很不值的。

  正如之前提到的,序列化單獨的Prefab時,每個游戲?qū)ο蠛推浣M件都會被分開序列化——即便數(shù)據(jù)是重復的。一個有30個獨立元素的UI界面中,Unity會為這些元素序列化30次,這就會產(chǎn)生大量的序列化數(shù)據(jù)。載入時,所有30個重復元素的游戲?qū)ο蠛徒M件在被轉(zhuǎn)化為新實例對象之前都需要從硬盤中進行讀取。正是這里討論的讀取時間決定了實例化大型Prefab的性能花銷。

  Unity支持嵌套Prefab之后,對于有載入大型結(jié)構(gòu)體游戲?qū)ο笮枨蟮墓こ潭?,要想減少這類工程的載入時間,可以考慮將大型Prefab中的可重用元素分開存儲到不同的Prefab中,并在運行時對它們進行實例化,而不是完全依靠Unity的序列化和Prefab系統(tǒng)。

  Footnotes(腳注)

  在文件中,本地ID是唯一的。即在一個資源文件中,里面包含的本地ID都是不重復的。

  在內(nèi)部,這種緩存被稱為PersistentManager。實際的轉(zhuǎn)換工作在在Unity的C++ Remapper類中進行,Remapper類沒有提供任何C# API調(diào)用接口。

  運行時創(chuàng)建資源的示例是在腳本中創(chuàng)建Texture2D對象:var myTexture = new Texture2D(1027, 768);

  程序運行時對象并沒有被卸載卻被從內(nèi)存中移除的情況通常會發(fā)生在Unity失去了對圖形內(nèi)容的控制的時候。例如,當手機應用被掛起并被強制在后臺運行。這種情況下,手機操作系統(tǒng)通常會將所有的圖形資源從GPU顯存中強行卸載。之后APP再回到前臺運行時,Unity不得不重新向GPU上傳需要的材質(zhì)、著色器和網(wǎng)格數(shù)據(jù),以便恢復場景的正常渲染。

  到此整個Unity內(nèi)部資源管理與對象引用及序列化的內(nèi)容就結(jié)束了,希望看完本文的你對如何合理分配Unity項目結(jié)構(gòu)都有了比較清晰的概念。

更多精彩,盡在匯寶盆:http://trusteddivorcelawyers.com/resource

相關文章
網(wǎng)友評論

您需要登錄后才可以發(fā)帖 登錄 | 立即注冊

關閉

全部評論:0條

推薦
熱門