Community Made Tools

Have you made any useful utilities with Odin?

Login and submit your creations here

Nested Scriptable Object Attributes (Field and List)

Authored by Shaun
Shared 10-05-2021

Nested Scriptable Object Attributes

These custom Attributes/AttributeDrawers enable you to effortlessly create nested ScriptableObject assets in your project.

This project contains two new custom attributes: [NestedScriptableObjectField], [NestedScriptableObjectList]

These attributes should be applied to fields in a root ScriptableObject. They each provide the same functionality for their respective field types (ScriptableObject fields or Lists):

  • They provide a dropdown containing every non-abstract Type* that is or inherits from the ScriptableObject type of the field the attribute is applied to.

  • Selecting a dropdown value creates a new ScriptableObject asset as a nested subasset of the root ScriptableObject.

  • They also each provide a 'Remove' button that deletes the nested asset and clears the field/list item.

*(By default the Attribute only searches for Types within the Scripts folder, but this can be manually changed if needed by editing the GetAllScriptsOfType() method)

Demo Setup:

NestedScriptableObjectRoot.cs

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "ScriptableObjects/NestedScriptableObjectRoot")]
public class NestedScriptableObjectRoot : ScriptableObject
{
    [NestedScriptableObjectField]
    public NestedScriptableObject field;
    [NestedScriptableObjectList]
    public List<NestedScriptableObject> list = new List<NestedScriptableObject>();
}

public abstract class NestedScriptableObject : ScriptableObject {}

NestedScriptableObjectInt.cs

using Sirenix.OdinInspector;

[InlineEditor]
public class NestedScriptableObjectInt : NestedScriptableObject
{
    public int value = 0;
}

NestedScriptableObjectString.cs

using Sirenix.OdinInspector;

[InlineEditor]
public class NestedScriptableObjectString : NestedScriptableObject
{
    public string value = "test";
}

Attributes / Drawers:

NestedScriptableObjectFieldAttribute.cs

using System;

public class NestedScriptableObjectFieldAttribute : Attribute
{
    public Type Type;
}

Editor/NestedScriptableObjectFieldAttributeDrawer.cs

using UnityEngine;
using Sirenix.Utilities.Editor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using Sirenix.OdinInspector.Editor;

[DrawerPriority(DrawerPriorityLevel.SuperPriority)]
public class NestedScriptableObjectFieldAttributeDrawer<T> : OdinAttributeDrawer<NestedScriptableObjectFieldAttribute, T> where T : ScriptableObject
{
    string[] assetPaths = new string[0];
    UnityEngine.Object Parent => (UnityEngine.Object)Property.Tree.RootProperty.ValueEntry.WeakSmartValue;

    protected override void Initialize()
    {
        Attribute.Type = typeof(T);
        base.Initialize();
    }

    protected override void DrawPropertyLayout(GUIContent label)
	{
        if(assetPaths.Count() == 0)
            assetPaths = GetAllScriptsOfType();
        if (ValueEntry.SmartValue == null && !Application.isPlaying)
        {
            //Display value dropdown
            EditorGUI.BeginChangeCheck();
            Rect rect = EditorGUILayout.GetControlRect();
            rect = EditorGUI.PrefixLabel(rect, label);
            var valueIndex = SirenixEditorFields.Dropdown(rect, 0, GetDropdownList(assetPaths));
            if (EditorGUI.EndChangeCheck() && valueIndex > 0)
            {
                T newObject = (T)ScriptableObject.CreateInstance(UnityEditor.AssetDatabase.LoadAssetAtPath<MonoScript>(assetPaths[valueIndex - 1]).GetClass());
                CreateAsset(newObject);
                ValueEntry.SmartValue = newObject;
            }
        }
        else
        {
            //Display object field with a delete button
            EditorGUILayout.BeginHorizontal();
            this.CallNextDrawer(label);
            var rect = EditorGUILayout.GetControlRect(GUILayout.Width(20));
            EditorGUI.BeginChangeCheck();
            SirenixEditorGUI.IconButton(rect, EditorIcons.X);
            EditorGUILayout.EndHorizontal();
            if (EditorGUI.EndChangeCheck())
            {
                //If delete button was pressed:
                AssetDatabase.Refresh();
                GameObject.DestroyImmediate(ValueEntry.SmartValue, true);
                AssetDatabase.ForceReserializeAssets(new[] { AssetDatabase.GetAssetPath(Parent)});
                AssetDatabase.SaveAssets();
                AssetDatabase.Refresh();
            }
        }
	}

    protected virtual string[] GetAllScriptsOfType()
    {
        var items = UnityEditor.AssetDatabase.FindAssets("t:Monoscript", new[] { "Assets/Scripts" })
            .Select(x => UnityEditor.AssetDatabase.GUIDToAssetPath(x))
            .Where(x => IsCorrectType(UnityEditor.AssetDatabase.LoadAssetAtPath<MonoScript>(x)))
            .ToArray();
        return items;
    }

    protected bool IsCorrectType(MonoScript script)
    {
        if (script != null)
        {
            Type scriptType = script.GetClass();
            if (scriptType != null && (scriptType.Equals(Attribute.Type) || scriptType.IsSubclassOf(Attribute.Type)) && !scriptType.IsAbstract)
            {
                return true;
            }
        }
        return false;
    }

    protected string[] GetDropdownList(string[] paths)
    {
        List<String> names = paths.Select(s => Path.GetFileName(s)).ToList();
        names.Insert(0, "null");
        return names.ToArray();
    }

    protected void CreateAsset(T newObject)
    {
        newObject.name = "_" + newObject.GetType().Name;
        AssetDatabase.Refresh();
        AssetDatabase.AddObjectToAsset(newObject, Parent);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
    }

    protected virtual void RemoveAsset(ScriptableObject objectToRemove)
    {
        UnityEngine.Object.DestroyImmediate(objectToRemove, true);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
    }
}

NestedScriptableObjectListAttribute.cs

using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using UnityEditor;

[IncludeMyAttributes]
[ListDrawerSettings(CustomRemoveElementFunction = "@$property.GetAttribute<NestedScriptableObjectListAttribute>().RemoveObject($removeElement, $property)", Expanded = true)]
[ValueDropdown("@$property.GetAttribute<NestedScriptableObjectListAttribute>().GetAllObjectsOfType()", FlattenTreeView = true)]
[OnCollectionChanged("@$property.GetAttribute<NestedScriptableObjectListAttribute>().OnCollectionChange($info)")]
public class NestedScriptableObjectListAttribute : Attribute
{
    public List<UnityEngine.Object> objectsToRemove = new List<UnityEngine.Object>();
    public List<ScriptableObject> objectsToCreate = new List<ScriptableObject>();

    public Type Type;

    protected void RemoveObject(UnityEngine.Object objectToRemove, InspectorProperty property)
    {
        objectsToRemove.Add(objectToRemove);
    }

    protected IEnumerable GetAllObjectsOfType()
    {
        var items = UnityEditor.AssetDatabase.FindAssets("t:Monoscript", new[] { "Assets/Scripts" })
            .Select(x => UnityEditor.AssetDatabase.GUIDToAssetPath(x))
            .Where(x => IsCorrectType(UnityEditor.AssetDatabase.LoadAssetAtPath<MonoScript>(x)))
            .Select(x => new ValueDropdownItem(System.IO.Path.GetFileName(x), ScriptableObject.CreateInstance(UnityEditor.AssetDatabase.LoadAssetAtPath<MonoScript>(x).GetClass())));
        return items;
    }

    protected bool IsCorrectType(MonoScript script)
    {
        if (script != null)
        {
            Type scriptType = script.GetClass();
            if (scriptType != null && (scriptType.Equals(Type) || scriptType.IsSubclassOf(Type)) && !scriptType.IsAbstract)
            {
                return true;
            }
        }
        return false;
    }

    protected void OnCollectionChange(CollectionChangeInfo info)
    {
        if (info.ChangeType == CollectionChangeType.Add)
        {
            objectsToCreate.Add((ScriptableObject)info.Value);
        }
    }
}

Editor/NestedScriptableObjectListAttributeDrawer.cs

using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using System.Linq;

[DrawerPriority(DrawerPriorityLevel.SuperPriority)]
public class NestedScriptableObjectListAttributeDrawer<TList, T> : OdinAttributeDrawer<NestedScriptableObjectListAttribute, TList> where TList : List<T> where T : ScriptableObject
{
    UnityEngine.Object Parent => (UnityEngine.Object)Property.Parent.ValueEntry.WeakSmartValue;

    protected override void Initialize()
    {
        Attribute.Type = typeof(T);
        base.Initialize();
    }
    protected override void DrawPropertyLayout(GUIContent label)
    {
        CallNextDrawer(label);
        if(Attribute.objectsToRemove.Count > 0)
        {
            UnityEngine.Object objectToRemove = Attribute.objectsToRemove[0];
            Attribute.objectsToRemove.Remove(objectToRemove);
            if (ValueEntry.SmartValue.Contains(objectToRemove))
            {
                AssetDatabase.Refresh();
                ValueEntry.SmartValue.Remove((T)objectToRemove);
                UnityEngine.Object.DestroyImmediate(objectToRemove, true);
                if (!Application.isPlaying)
                {
                    AssetDatabase.ForceReserializeAssets(new[] {AssetDatabase.GetAssetPath(Parent)});
                    AssetDatabase.SaveAssets();
                    AssetDatabase.Refresh();
                }
            }
        }
        if(Attribute.objectsToCreate.Count > 0)
        {
            ScriptableObject objectToCreate = Attribute.objectsToCreate[0];
            Attribute.objectsToCreate.Remove(objectToCreate);
            objectToCreate.name = "_" + objectToCreate.GetType().Name;
            AssetDatabase.AddObjectToAsset(objectToCreate, Parent);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        }
    }
}