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(); } } }