Have you made any useful utilities with Odin?
Login and submit your creations hereThese 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)
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";
}
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();
}
}
}