第4章 腳本化對象 scriptableObject
4.1 ScriptableObject是什么?
ScriptableObject類直接繼承自Object類,它和MonoBehaviour是並列的,都繼承自Object(但MonoBehaviour並不是直接繼承自Object)。
ScriptableObject是一個可序列化的數據容器,可以用來存儲大量的數據,一個主要用處就是通過將數據存儲在ScriptableObject對象中來減少工程以及游戲運行時因拷貝值所造成的內存占用。
ScriptableObject是Unity編輯器的基礎,在Unity編輯器中隨處可見。例如,從ScriptableObject派生類生成的諸如場景視圖和游戲視圖之類的編輯器窗口,還有從ScriptableObject派生類生成的用於在Inspector中顯示GUI的Editor對象。毫不誇張地說,Unity編輯器是使用ScriptableObject創建的。
從上圖可以看出來,編輯器窗口類是直接繼承的ScriptableObject類。
4.2 ScriptableObject與Prefab的區別
當我們有一個Prefab預制體,並添加了一些mono腳本,當我們每次實例化預制體的時候它都會將原預制體的值生成一份自己的拷貝,然后我們就可以修改場景內預制體的值而並不影響原預制體的值,這是prefab的特性。這對於我們從一個prefab模板生成屬性不同的游戲對象是很有用的,但是如果prefab里的腳本數據是不需要修改的,這樣就會造成很大的資源浪費,尤其在數據很多的時候。
為了避免這種問題,我們可以在不需要修改prefab里的腳本數據時,考慮使用ScriptableObject來存儲這些重復的數據,然后其它所有預制體都通過引用的方式來訪問這份數據。這樣不管場景中又多少預設體的實例,在內存中就只有一份數據。所以當預制體中的腳本有大量重復數據時,我們要想着將數據抽離出來,單獨保存在本地。
4.3 ScriptableObject與MonoBehaviour的區別
MonoBehaviour類
ScriptableObject類
4.4 ScriptableObject的優缺點
優點:
1、ScriptableObject的數據是存儲在asset里的,因此它不會在退出時被重置,這類似Unity里面的材質和紋理資源數據,如果我們在運行時修改它們的數值就是真的改變了;
2、這些資源在實例化的時候是被引用關系,而非直接復制一份;
3、類似其他資源,它可以被任何場景引用,即場景間共享;
4、可以在不同的項目之間實現共享;
5、沒有其他多余的東西,例如多余的Component;
缺點:
1、只有很少的回調函數 ,ScriptableObject內部實現上也繼承自MonoBehavior,當它只有Awake、OnDestroy、OnEnable、OnDisable四個消息函數;
2、真正意義上的共享,因此一旦修改數據都真的修改了;
3、只能在編輯器狀態下進行修改,一旦發布就只能讀取;
總結:在編輯器模式下,我們可以將數據保存到ScriptableObject里(當創建一個腳本化對象實例后使用AssetDatabase.CreateAsset()保存成資源),因為是作為本地資源保存的,所以在退出之后也不會丟失。
但只有在編輯器模式下才可以修改里面的數據,這是因為ScriptableObject對象雖然聲明在UnityEngine中,但是它的Scriptable是通過UnityEditor命名空間下的類(例如Editor類等)來實現的,
所以ScriptableObject生成的數據資源文件在Editor外具有只讀屬性,這是非常需要注意的一點。如果我們需要在游戲中修改數據並存儲下來,就不推薦使用ScriptableObject了。
4.5 如何使用ScriptableObject
要創建ScriptableObject,我們首先要創建一個繼承自ScriptableObject的類。類名和資源名必須相同,這與繼承自MonoBehaviour的類限制相同。
using System;
using UnityEngine; public class EditorTest : ScriptableObject { public int m_ID = 1; [SerializeField] private string m_Name = "HeiHei"; [SerializeField] internal int m_Number = 10; }
實例化 - ScriptableObject.CreateInstance
接下來生成ScriptableObject,可以使用ScriptableObject.CreateInstance來生成,但是不能使用new來實例化,因為必須通過Unity的序列化機制創建對象,這個跟繼承自MonoBehaviour的類也是相同的。
using System;
using UnityEditor; using UnityEngine; public class EditorTest : ScriptableObject { public int m_ID = 1; [SerializeField] private string m_Name = "HeiHei"; [SerializeField] internal int m_Number = 10; [MenuItem("Example/CreateEditorTestInst")] static void CreateEditorTestInstance() { var editorTest = CreateInstance<EditorTest>(); } }
生成資源文件 - AssetDatabase.CreateAsset
實例化完對象后,接下來我們就需要將實例化的對象保存成資源。我們使用AssetDatabase.CreateAsset來創建資源文件,並且確保擴展名為.asset。如果使用其他擴展名,Unity則不會將其識別為ScriptableObject的派生資源。
using UnityEditor;
using UnityEngine; public class EditorTest : ScriptableObject { [MenuItem("Example/CreateEditorTestInst")] static void CreateEditorTestInstance() { var editorTest = CreateInstance<EditorTest>(); AssetDatabase.CreateAsset(editorTest, "Assets/Test/EditorTest.asset"); AssetDatabase.Refresh(); } }
另外,使用CreateAssetMenu也可以輕松創建資源,使用CreateAssetMenu時,會在“Asets/Create”下創建一個菜單。
加載資源文件 - AssetDatabase.LoadAssetAtPath
加載方法也很簡單,只要使用AssetDatabase.LoadAssetAtPath就可以讀取。
using UnityEditor;
using UnityEngine; public class EditorTest : ScriptableObject { [MenuItem("Example/LoadExampleAsset")] static void LoadExampleAsset() { var exampleAsset = AssetDatabase.LoadAssetAtPath<EditorTest>("Assets/Test/NewEditorTest.asset"); } }
在監視器中顯示屬性
與MonoBehaviour相同,只需添加SerializeField就可以顯示該字段,PropertyDrawer也同樣適用。
using UnityEditor;
using UnityEngine; public class EditorTest : ScriptableObject { [SerializeField, Range(0, 100)] public int ID = 1; [SerializeField] private string Name = "HeiHei"; [MenuItem("Example/CreateEditorTestInst")] static void CreateExampleAssetInstance() { var exampleAsset = CreateInstance<EditorTest>(); AssetDatabase.CreateAsset(exampleAsset, "Assets/Test/NewEditorTest.asset"); AssetDatabase.Refresh(); } }
4.6 ScriptableObject的父子關系
父ScriptableObject
using UnityEngine;
public class EditorTest : ScriptableObject { [SerializeField] ChildScriptableObject child; }
子ScriptableObject
using UnityEngine;
public class ChildScriptableObject : ScriptableObject { [SerializeField, Range(0, 100)] public int ID = 1; [SerializeField] private string Name = "HeiHei"; public ChildScriptableObject() { name = "NewChildScriptableObject"; } }
然后將ParentScriptableObject保存成資源文件,同時我們還要在參數中實例化子級。
using System;
using UnityEditor; using UnityEngine; public class EditorTest : ScriptableObject { [SerializeField] ChildScriptableObject child; private const string Path = "Assets/Test/ParentScriptableObject.asset"; [MenuItem("Assets/CreateScriptableObject")] static void CreateScriptableObject() { var parent = ScriptableObject.CreateInstance<EditorTest>(); parent.child = ScriptableObject.CreateInstance<ChildScriptableObject>(); AssetDatabase.CreateAsset(parent, Path); AssetDatabase.ImportAsset(Path); } }
將ParentScriptableObject保存為資源后,這個時候如果我們查看Inspector,會發現child的屬性Type不匹配。
如果雙擊Type mismatch,那么ChildScriptableObject的信息將顯示在監視器中,看起來沒有任何問題。
但是如果我們重新啟動Unity,再次查看父對象ParentScriptableObject的監視器,會發現子對象部分變成了None。
原來為了將繼承ScriptableObject的子對象也視為序列化數據,就必須將其一並保存成本地資源。Type mismatch狀態的意思其實是指實例已經存在但並不是本地資源。所以如果實例在某些情況下被破壞(例如重新啟動Unity),則無法再訪問數據了。
將所有ScriptableObjects保存成本地資源
其實我們只要將子對象也保存到本地,並把它的引用存儲在父對象的字段中就可以避免Type Mismatch的情況。
但是,從資源管理的角度來看,一次創建具有“父”和“子”關系的獨立資源並不是一個明智的選擇, 當子級數目很多或者處理列表時,過多的子資源對於文件管理將是一種災難。
因此,我們可以想辦法把具有父子關系的資源整合成一個,這樣既解決了多個文件的管理問題又保留了父子級之間的關系。
子資源
任何繼承自UnityEngine.Object的資源都可以被當做子資源,然后將子資源信息添加到父級資源中形成嵌套結構,最典型的例子是模型資源。
模型資源一般包括網格、骨骼和動畫之類的資源。這些通常必須作為獨立資源存在,但是通過將它們當成子資源,我們就可以在主資源中訪問網格和動畫片段資源。
添加子資源 - AssetDatabase.AddObjectToAsset
把UnityEngine.Object對象當做子資源添加到主資源中。
子ScriptableObject
using UnityEditor;
using UnityEngine; public class EditorTest : ScriptableObject { [SerializeField] ChildScriptableObject child; private const string Path = "Assets/Test/ParentScriptableObject.asset"; [MenuItem("Assets/CreateScriptableObject")] static void CreateScriptableObject() { var parent = ScriptableObject.CreateInstance<EditorTest>(); parent.child = ScriptableObject.CreateInstance<ChildScriptableObject>(); AssetDatabase.AddObjectToAsset(parent.child, Path); AssetDatabase.CreateAsset(parent, Path); AssetDatabase.ImportAsset(Path); } }
隱藏子資源 - HideFlags.HideInHierarchy
有時候我們希望隱藏子資源本身,使具有嵌套結構的資源看起來只是個主資源。
using UnityEditor;
using UnityEngine; public class EditorTest : ScriptableObject { [SerializeField] ChildScriptableObject child; private const string Path = "Assets/Test/ParentScriptableObject.asset"; [MenuItem("Assets/CreateScriptableObject")] static void CreateScriptableObject() { var parent = ScriptableObject.CreateInstance<EditorTest>(); parent.child = ScriptableObject.CreateInstance<ChildScriptableObject>(); parent.child.hideFlags = HideFlags.HideInHierarchy; AssetDatabase.AddObjectToAsset(parent.child, Path); AssetDatabase.CreateAsset(parent, Path); AssetDatabase.ImportAsset(Path); } }
如下圖所示分層結構已經消失,但是我們現在也可以一起處理兩個資源。
反之,我們也可以把隱藏的子資源顯示出來,方便我們查看。
[MenuItem("Assets/SeToHideFlags.None")]
static void SetHideFlags() { var path = AssetDatabase.GetAssetPath(Selection.activeObject); foreach (var item in AssetDatabase.LoadAllAssetsAtPath(path)) { item.hideFlags = HideFlags.None; } AssetDatabase.ImportAsset(path); }
刪除子資源 - Object.DestroyImmediate
[MenuItem("Assets/RemoveChildScriptableObject")] static void Remove() { var parent = AssetDatabase.LoadAssetAtPath<EditorTest>(Path); UnityEngine.Object.DestroyImmediate(parent.child, true); parent.child = null; AssetDatabase.ImportAsset(Path); }
參考文章:Unity編輯器拓展手冊日文版 http://49.233.81.186/guicreation.html