深入Unity資源(Asset)

Unity資源統稱爲Asset。本節討論四個話題:Asset 和 Unity.Object
的概念和內涵;File Guid, local ID 和 Instance ID的由來;MonoBehavior, ScriptableObject 和普通 c# 類的區別和應用場景;Unity.Object的資源生命週期(主要是加載和卸載)。

(一)Asset 和 Unity.Object

官方詳解

Asset

  • 泛指Unity可識別的資源文件,據可依具體分成 Native Asset 和 Non-Native Asset

    Native:
    擁有可以被 Unity 直接識別的格式,例如MAT文件
    Non-Native:
    外部資源,需要導入(import)才能被Unity識別,例如FBX文件,PNG文件等等

  • 每個Asset文件具有全局唯一的File Guid,存放在相應的**“.meta”文件**中

    這裏有一個搜索某個Asset被哪裏引用的小技巧:根據guid,用 notepad++ 對Asset文件夾做全文搜索即可(Ctrl+SHift+F

  • None-Native資源的meta文件中記載了其使用了哪種 Importer

    例如,PNG用的是TextureImporter,FBX用的是ModelImporter,cs代碼用的是MonoImporter,shader文件用的是ShaderImporter,等等。很多默認用的是DefaultImporter。也可以自定義一些Importer,繼承AssetImporter。

  • 所有資源Import後暫存在Library/metadata文件夾中,這樣就不用每次打開編輯器都進行耗時的導入操作(例如紋理壓縮)

Object

  • 一個或多個Object共同描述了序列化了的資源的實例。也就是說,Object是序列化了的,需要經過實例化,纔會加載到內存中。

  • 一個Asset裏面有多個Object,內部以localID(或叫FileID)區分

    常見 Object如:Sprite, Texture, AudioClip, Material, Motion, GameObject, Component等等。特殊的兩個:MonoBehaviorScriptableObject

  • 只有 Unity.Object 及其子類,纔可以在 Unity 編輯器中拖放。

  • 只有 Component 及其子類,纔可以附着到gameObject上。代碼中訪問的Component對象不可單獨存在,必須要附着在某個gameObject上。

爲什麼訪問`tranform`的字段那麼耗時?

首先,this.transform 等同於 GetComponent<Tranform>(),遍歷多個組件找到第一個Tranform本身就不快。

例如 transform.position,在訪問到transform後,需要遍歷該對象在Hierachy中的所有父節點,計算出全局座標。若要改變這個字段,也會遍歷Hierachy,因爲保存的是相對父節點的局部座標。然後會向所有Componenets發送消息。

(二)File Guid, local ID 和 Instance ID

首先,爲了能用文本編輯器查看資源內部結構,需進行如下操作:

Edit->Project Settings->Editor ->Assets Serialization,選擇 force text

 

我們已經知道,每個文件有全局唯一的File Guid。一個文件中有多個Object,每個Object有文件中唯一的local Id。所以,每個Object的全局唯一標識爲File Guid + local Id。Unity編輯器會自動將文件路徑和 FileGuid的映射關係保存在一張表裏。

一個典型的Object(Monobehavior)的描述如下:

--- !u!114 &114105719140368892
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1612640623411334}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 1980459831, guid: f5f67c52d1564df4a8936ccd202a3bd8, type: 3}
  m_Name: 
  m_EditorClassIdentifier: 
  m_UiScaleMode: 1
  m_ReferencePixelsPerUnit: 100
  m_ScaleFactor: 1
  m_ReferenceResolution: {x: 800, y: 600}
  m_ScreenMatchMode: 0
  m_MatchWidthOrHeight: 0
  m_PhysicalUnit: 3
  m_FallbackScreenDPI: 96
  m_DefaultSpriteDPI: 96
  m_DynamicPixelsPerUnit: 1

第一行,& 符號後面的數字就是這個Object的local id。

那爲什麼還需要什麼Instance ID呢?

官方給出的解答是:訪問起來更快。

While File GUIDs and Local IDs are robust, GUID comparisons are slow.
Unity internally maintains a cache that translates File GUIDs and Local IDs into simple integers that are unique only during a single session

當一個新的Object被註冊到cache中,其自動獲得一個Instance ID。具體來說,啓動時,Unity會載入場景和所有Resource目錄下的Objects,爲其分配InstanceID。如果動態加載或卸載AssetBundle,Unity會生成或刪除爲其分配的InstanceID。所以,重新載入同一個AB包,獲得的InstanceID不可保證相同。

(三)MonoBehavior, ScriptableObject 和普通 c# 類

從上一節對Monobehavior類型的Obejct的描述可以看到,其有一個對MonoScript的引用。Monoscript包含的信息很簡單:

  • assembly name
  • class name
  • namespace.

構建工程時,Unity會將Asset文件夾裏面所有的腳本打包進程序集裏。與其他資源不同,程序集在應用啓動時就全部載入了

每種語言有不同的程序集。例如Assembly-CSharp.dll;Plugin裏面也會單獨打進一個程序集Assembly-CSharp-firstpass.dll

由此可以想到,對於AB包來說,裏面其實不含任何的c#代碼資源,只有指向這些預先生成的dll文件的引用

c#代碼可以分爲三種,繼承Monobehavior的,繼承ScriptableObject的,以及c#原生的。其應用場景和區別如下:

  • 只有Monobehavior繼承了component類,纔可以往gameObject上掛。

  • 不需要掛在gameObject上,比如只爲了存數據,可以用ScriptableObject或c#原生類。

  • 接上一條,若要求可序列化,則只有用ScriptableObject。

  • ScriptableObject與c#原生類的區別還在於:前者是一種Unity的資源,需要通過DestroyResources.UnloadUnusedAssets()來釋放,其不歸c#的GC管理。(相關討論

(四)Unity.Object的資源生命週期

加載時機

  • 自動加載。 當其Instance ID被“解引用(dereference)”時,若Object還沒被加載到內存,則Unity會自動機加載。前文已經說過,與其他資源不同,程序集不是Unity.Obejct,其在應用啓動時就全部載入了。
  • 手動加載。 通過腳本創建,或者調用資源加載API的時候。

解引用的意思是訪問引用的對象,不是將引用置爲null來解除。

加載時,會建立起Instance ID到FileGuid和localID的映射。如果這個映射失效了,Unity編輯器裏就會顯示(Missing),如果丟失的是紋理,則會顯示粉紅色。

加載的耗時分佈

  • Time to read the source data

    (from storage, from another GameObject, etc.)

  • Time to set up the parent-child relationships between the new Transforms
  • Time to instantiate the new GameObjects and Components
  • Time to awaken the new GameObjects and Components

    對於具有複雜層級的gameObject,後三項的耗時甚至可以忽略。這是因爲在序列化gameObject時,即使對於重複的元素,unity也會重複地將其序列化。所以在反序列化時,有一個優化就是將重複的元素另保存成一個prefab,然後動態實例化,拼接成一個大的gameObject。

卸載時機

  • 切換場景;調用Resources.UnloadUnusedAssets之類的API。這種會將沒有用到的對象卸載(注意:沒有用到是指c#代碼沒有其引用且unity裏沒有其他Object引用到它。例如下面不會卸載掉MyPrefab資源,要obj=null後纔行)。

    Object obj = Resources.Load("MyPrefab");
    GameObject instance = Instantiate(obj) as GameObject;
    Destroy(instance);
    Resources.UnloadUnusedAssets();
  • Resources文件夾裏面的資源在Resources.UnloadAsset後卸載,但保留InstanceID到FileGuid和localID的映射,當mono或別的object持有該對象的引用並解引用時,該對象會自動重新加載。

  • AssetBundle裏面的資源在AssetBundle.Unload(true)後立即卸載,並且所有訪問其的引用會拋出異常!

  • AssetBundle.Unload(false), compressed file data for assets inside the bundle will be unloaded, but any actual objects already loaded from this bundle will be kept intact. Of course you won't be able to load any more objects from this bundle.
  • AssetBundle.Unload(true), all objects that were loaded from this bundle will be destroyed as well. If there are game objects in your scene referencing those assets, the references to them will become missing.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章