第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对象当做子资源添加到主资源中。
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