Unity Editor扩展:ScriptableObject简记

ScriptableObject简介

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基类。同时,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属性就能够使字段显示出来。

扫描二维码关注公众号,回复: 9004438 查看本文章
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属性。
这里的例子比较简单,实际项目中便要根据要求,大家自己编写代码了。

参考

参考链接:

  1. Unity Editor官方文档链接
  2. https://blog.csdn.net/xdestiny110/article/details/79678922
  3. https://blog.csdn.net/wuwangxinan/article/details/72773297

资源例子

https://download.csdn.net/download/f_957995490/12111810

发布了20 篇原创文章 · 获赞 1 · 访问量 916

猜你喜欢

转载自blog.csdn.net/f_957995490/article/details/104030634