赞
踩
ScriptableObject
是一个用于生成单独Asset的结构。同时,它也能被称为是Unity中用于处理序列化的结构。
在Unity里面有单独的序列化结构,所有的Object(UnityEngine.Object
)都能够通过这个方法进行数据的序列化与反序列化。文件和Unity编辑器都能够方便的获取其中的数据。
Unity内部的Asset(Material或者AnimationClip等)都是从UnityEngine.Object
衍生出来的。为了制作单独的Asset,需要制作UnityEngine.Object
的子类。不过对于用户而言是不允许制作UnityEngine.Object
的子类。所以用户如果要利用Unity中的序列化结构、生成单独的Asset,就必须借助ScriptableObject
。
同时,ScriptableObject
也可用于编辑器inspector面板的编辑扩展,是脚本应用更加便捷。
只要有Unity编辑器的地方就会使用到ScriptableObject
。比如,SceneView或者是GameView之类的编辑器窗口,也是通过ScriptableObject
生成出来的。另外,Object在Inspector里面的GUI显示也要通过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>();
}
}
将实例化完成后的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]下面生成菜单来进行操作:
生成文件,并命名:
使用AssetDatabase.LoadAssetAtPath
来读取新建的ExampleAsset
类的Asset资源。
[MenuItem("Example/Load ExampleAsset")]
static void LoadExampleAsset()
{
var exampleAsset = AssetDatabase.LoadAssetAtPath<ExampleAsset>("Assets/Editor/ExampleAsset.asset");
}
和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(); } }
如图:
下面我们对这个问题进行测试。
父节点类:
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,就会有如下效果:
我们可以将上图中的下拉箭头隐藏,通过使用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进行表示。
同样,使用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都会表示出来。
使用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被删除。
如下图,一般情况下,我们创建的一个类,作为一个属性存在于一个继承于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
属性。
这里的例子比较简单,实际项目中便要根据要求,大家自己编写代码了。
参考链接:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。