Unity Editor扩展:ScriptableObject
ScriptableObject简介
ScriptableObject
是一个用于生成单独Asset的结构。同时,它也能被称为是Unity中用于处理序列化的结构。
在Unity里面有单独的序列化结构,所有的Object(UnityEngine.Object
)都能够通过这个方法进行数据的序列化与反序列化。文件和Unity编辑器都能够方便的获取其中的数据。
Unity内部的Asset(Material或者AnimationClip等)都是从UnityEngine.Objec
t衍生出来的。为了制作单独的Asset,需要制作UnityEngine.Object
的子类。不过对于用户而言是不允许制作UnityEngine.Object
的子类。所以用户如果要利用Unity中的序列化结构、生成单独的Asset,就必须借助ScriptableObject
。
同时,ScriptableObject
也可用于编辑器inspector面板的编辑扩展,是脚本应用更加便捷。
只要有Unity编辑器的地方就会使用到ScriptableObject
。比如,SceneView或者是GameView之类的编辑器窗口,也是通过ScriptableObject
生成出来的。另外,Object在Inspector里面的GUI显示也要通过ScriptableObject
才行。
示例
创建ScriptableObject
创建类
首先要创建一个类,让该类继承ScriptableObject
基类。同时,ScriptableObject
的限制和MonoBehaviour
是一样的。
using UnityEngine;
public class ExampleAsset : ScriptableObject
{
}
实例化
ScriptableObject
类的对象必须由ScriptableObject.CreateInstance
方法来生成。由于Unity生成Object的时候必须经过序列化,而且如果使用new
操作符来生成对象程序就必定会出现一定的卡顿情况,所以,不能使用new
操作符来生成
using UnityEngine;
using UnityEditor;
public class ExampleAsset : ScriptableObject
{
[MenuItem("Example/Create ExampleAsset Instance")]
static void CreateExampleAssetInstance()
{
var exampleAsset = CreateInstance<ExampleAsset>();
}
}
保存为Asset
将实例化完成后的ScriptableObject
类对象作为Asset进行保存,需要通过AssetDatabase.CreateAsset
函数来生成。且Asset的后缀名必须是.asset。如果是其他后缀名的话,Unity会无法识别。
using UnityEngine;
using UnityEditor;
public class ExampleAsset : ScriptableObject
{
[MenuItem("Example/Create ExampleAsset Instance")]
static void CreateExampleAssetInstance()
{
var exampleAsset = CreateInstance<ExampleAsset>();
AssetDatabase.CreateAsset(exampleAsset, "Assets/Editor/ExampleAsset.asset");
AssetDatabase.Refresh();
}
}
代码中使用了Editor文件夹路径:“
Assets/Editor
”,故在进行下面的操作之前,需要先创建Editor文件夹。
如图操作:
便会生成如下资源:
同时,还可以使用CreateAssetMenu
属性来让使得Asset的制作更加简单。
using UnityEngine;
using UnityEditor;
[CreateAssetMenu(menuName = "Example/Create ExampleAsset Instance Two")]
public class Example_Asset : ScriptableObject
{
}
使用CreateAssetMenu
时,是在[Assets/Create]下面生成菜单来进行操作:
生成文件,并命名:
使用脚本读取ScriptableObject
使用AssetDatabase.LoadAssetAtPath
来读取新建的ExampleAsset
类的Asset资源。
[MenuItem("Example/Load ExampleAsset")]
static void LoadExampleAsset()
{
var exampleAsset = AssetDatabase.LoadAssetAtPath<ExampleAsset>("Assets/Editor/ExampleAsset.asset");
}
Inspector上的属性显示
显示一般属性
和MonoBehaviour
一样,在字段上附加SerializeField
属性就能够使字段显示出来。
using UnityEngine;
using UnityEditor;
public class ExampleAsset : ScriptableObject
{
[SerializeField]
string str;
[SerializeField, Range(0, 10)]
int number;
[MenuItem("Example/Create ExampleAsset Instance")]
static void CreateExampleAssetInstance()
{
var exampleAsset = CreateInstance<ExampleAsset>();
AssetDatabase.CreateAsset(exampleAsset, "Assets/Editor/ExampleAsset.asset");
AssetDatabase.Refresh();
}
}
如图:
ScriptableObject类作为ScriptableObject类的属性显示
下面我们对这个问题进行测试。
- 首先创建两个子父节点关系的类
父节点类:
using UnityEngine;
public class ParentScriptableObject : ScriptableObject
{
[SerializeField]
ChildScriptableObject child;
}
子节点类:
using UnityEngine;
public class ChildScriptableObject : ScriptableObject
{
public ChildScriptableObject()
{
//一开始Asset的名字
name = "New ChildScriptableObject";
}
}
- 然后将
ParentScriptableObject
作为asset被保存起来
其中含有自变量的子类也试着实例化。
using UnityEngine;
using UnityEditor;
public class ParentScriptableObject : ScriptableObject
{
[SerializeField]
ChildScriptableObject child;
private const string PATH = "Assets/Editor/New ParentScriptableObject.asset";
[MenuItem("Assets/Create ScriptableObject")]
static void CreateScriptableObject()
{
//父类实例化
var parent = CreateInstance<ParentScriptableObject>();
//子类实例化
parent.child = CreateInstance<ChildScriptableObject>();
//把父类作为asset保存起来
AssetDatabase.CreateAsset(parent, PATH);
//使用import刷新状态
AssetDatabase.ImportAsset(PATH);
}
}
结果创建Asset时报错:
意思是:“不允许从ScriptableObject构造函数(或实例字段初始化器)调用GetBool,而是在OnEnable中调用它。从ScriptableObject’ ChildScriptableObject’调用。”
原来是name
的赋值时机不对,需要在OnEnable
中进行赋值。
using UnityEngine;
public class ChildScriptableObject : ScriptableObject
{
public ChildScriptableObject()
{
}
private void OnEnable()
{
name = "New ChildScriptableObject";
}
}
再次创建Asset。
如上图,ParentScriptableObject
保存成Asset后,它的Inspector面板中,字段child变成了Type mismatch。
试着双击一下显示着Type mismatch的地方,Inspector面板便会切换为ChildScriptableObject
的信息。
然而,如果我们重启Unity的话,便会发现字段child部分显示为None(Child Scriptable Object)。
这个是因为作为ScriptableObject
父节点类的UnityEngine.Object
被作为序列化数据处理的时候,必须要以Asset的形式保存到硬盘上。Type mismatch的状态下,虽然在Inspector上看着没问题,不过代表着硬盘上并不存在对应的Asset文件。也就是说,这个实例一旦消失(例如重启Unity等情况),那么就无法方位这个数据了。
- 将
ChildScriptableObject
作为asset被保存起来
为了避免Type mismatch,只有把ScriptableObject
全部作为Asset保存下来。
Unity提供了SubAsset这样一种功能来用于整理具有节点关系的Asset。
SubAsset:
在作为MainAsset的Asset添加UnityEngine.Object就能作为SubAsset来处理。SubAsset中最容易理解的例子就是Model Asset。在Model Asset里面,会含有Mesh或者Animation之类的Asset。通常来说,这些必须要单独存在。不过对于SubAsset来说,Mesh或者AniamtionClip这些Asset的信息都会被包含在Main Asset里面。
ScriptableObject
也能够使用SubAsset的功能,因而在硬盘上不容易增添的具有节点关系的ScirptableObject
也能够构建起来。
想要在UnityEngine.Object
上追加SubAsset,只需要在想要成为Main Asset的Asset上添加Object(AssetDatabase.AddObjectToAsset
)就可以了。
[MenuItem("Assets/Create ScriptableObject")]
static void CreateScriptableObject()
{
//父类实例化
var parent = CreateInstance<ParentScriptableObject>();
//子类实例化
parent.child = CreateInstance<ChildScriptableObject>();
//在父类上添加子类
AssetDatabase.AddObjectToAsset(parent.child, PATH);
//把父类作为asset保存起来
AssetDatabase.CreateAsset(parent, PATH);
//使用import刷新状态
AssetDatabase.ImportAsset(PATH);
}
之后创建对应的Asset,就会有如下效果:
- 隐藏SubAssets
我们可以将上图中的下拉箭头隐藏,通过使用HideFlags.HideInHierarchy
来实现。
[MenuItem("Assets/Create ScriptableObject")]
static void CreateScriptableObject()
{
//父类实例化
var parent = CreateInstance<ParentScriptableObject>();
//子类实例化
parent.child = CreateInstance<ChildScriptableObject>();
//隐藏SubAsset中的子物体
parent.child.hideFlags = HideFlags.HideInHierarchy;
//在父类上添加子类
AssetDatabase.AddObjectToAsset(parent.child, PATH);
//把父类作为asset保存起来
AssetDatabase.CreateAsset(parent, PATH);
//使用import刷新状态
AssetDatabase.ImportAsset(PATH);
}
然后我们再重新创建的Asset就不再以多层的表示,而是整理到了一个Asset进行表示。
- 解除隐藏SubAssets
同样,使用HideFlags.HideInHierarchy
来解除隐藏。下面的代码,就是解除所有的HideFlags
[MenuItem("Assets/Set to HideFlags.None")]
static void SetHideFlags()
{
//选中AnimatorController的状态下弹出菜单
var path = AssetDatabase.GetAssetPath(Selection.activeObject);
//获取SubAsset里面的所有东西
foreach (var item in AssetDatabase.LoadAllAssetsAtPath(path))
{//把全部的标志位设置为None,解除隐藏状态
item.hideFlags = HideFlags.None;
}
//用Import进行刷新
AssetDatabase.ImportAsset(path);
}
解除隐藏的操作:
然后所有的SubAsset都会表示出来。
- 从MainAsset删除SubAssets
使用Object.DestoryImmediate
就能够删除SubAsset。
[MenuItem("Assets/Remove ChildScriptableObject")]
static void Remove()
{
var parent = AssetDatabase.LoadAssetAtPath<ParentScriptableObject>(PATH);
//删除子物体
Object.DestroyImmediate(parent.child, true);
//删除后会成为Missing状态,用Null取代
parent.child = null;
//用Import更新状态
AssetDatabase.ImportAsset(PATH);
}
根据上面编辑完代码后,如下图进行操作:
可以看到子节点的Asset被删除。
ScriptableObject在Inspector面板中的使用
如下图,一般情况下,我们创建的一个类,作为一个属性存在于一个继承于MonoBehaviour
类中时,在Inspector面板我们是无论怎样都无法显示该类的。
using UnityEngine;
public class ExampleInspector : MonoBehaviour
{
public ExampleObject m_EObect;
}
public class ExampleObject
{
}
但如果是继承自ScriptableObject
类的对象便可以直接显示其属性。
using UnityEngine;
public class ExampleInspector : MonoBehaviour
{
public ParentScriptableObject m_object;
}
然后,就可以通过上面我们用到的方法,创建Asset之后,拖入Inspector面板的对应属相栏中。
而且,我们可以对ScriptableObject
类使用Editor在Inspector面板的编辑器功能,来自定义Inspector面板。
在ParentScriptableObject
类中添加一个属性public int intData;
,然后通过编辑器分别自定义ExampleInspector
类和ParentScriptableObject
类。
using UnityEditor;
//指定类型
[CustomEditor(typeof(ExampleInspector))]
public class ExampleInspectorEditor : Editor
{
private ExampleInspector m_Script;
void OnEnable()
{
m_Script = (ExampleInspector)target;
}
Editor cacheEditor;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
if (null == cacheEditor)
cacheEditor = Editor.CreateEditor(m_Script.m_object);
if (null != cacheEditor)
cacheEditor.OnInspectorGUI();
//m_Script.m_object = ScriptableObject.CreateInstance<ParentScriptableObject>();
//需要在OnInspectorGUI之前修改属性,否则无法修改值
serializedObject.ApplyModifiedProperties();
}
}
上面的用到了Editor嵌套。通过Edtiro.CreateEditor
可实现Editor的嵌套。
即,想在ExampleInspector
的Inspector面板中直接看到并且可以修改ParentScriptableObject
的属性,可以重写ExampleInspector
的Editor,并在其中嵌套TestClass的Editor。
下面为ParentScriptableObject
的Editor:
using UnityEditor;
//指定类型
[CustomEditor(typeof(ParentScriptableObject))]
public class ParentScriptableObjectEditor : Editor
{
private ParentScriptableObject m_Script;
void OnEnable()
{
m_Script = (ParentScriptableObject)target;
}
public override void OnInspectorGUI()
{
//base.OnInspectorGUI();
serializedObject.Update();
m_Script.intData = EditorGUILayout.IntSlider(m_Script.intData, 0, 100);
//需要在OnInspectorGUI之前修改属性,否则无法修改值
serializedObject.ApplyModifiedProperties();
}
}
上图中ParentScriptableObject
类型的Object属性为空时,不会显示其他数据,只有其属性存在时,便会显示ParentScriptableObject
类型的intData
属性。
这里的例子比较简单,实际项目中便要根据要求,大家自己编写代码了。
参考
参考链接:
- Unity Editor官方文档链接
- https://blog.csdn.net/xdestiny110/article/details/79678922
- https://blog.csdn.net/wuwangxinan/article/details/72773297