Skip to content

Instantly share code, notes, and snippets.

@tomkail
Last active March 18, 2024 17:46
Show Gist options
  • Save tomkail/ba4136e6aa990f4dc94e0d39ec6a058c to your computer and use it in GitHub Desktop.
Save tomkail/ba4136e6aa990f4dc94e0d39ec6a058c to your computer and use it in GitHub Desktop.
Displays the fields of a ScriptableObject in the inspector
// Developed by Tom Kail at Inkle
// Released under the MIT Licence as held at https://opensource.org/licenses/MIT
// Must be placed within a folder named "Editor"
using System;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Object = UnityEngine.Object;
/// <summary>
/// Extends how ScriptableObject object references are displayed in the inspector
/// Shows you all values under the object reference
/// Also provides a button to create a new ScriptableObject if property is null.
/// </summary>
[CustomPropertyDrawer(typeof(ScriptableObject), true)]
public class ExtendedScriptableObjectDrawer : PropertyDrawer {
public override float GetPropertyHeight (SerializedProperty property, GUIContent label) {
float totalHeight = EditorGUIUtility.singleLineHeight;
if(property.objectReferenceValue == null || !AreAnySubPropertiesVisible(property)){
return totalHeight;
}
if(property.isExpanded) {
var data = property.objectReferenceValue as ScriptableObject;
if( data == null ) return EditorGUIUtility.singleLineHeight;
SerializedObject serializedObject = new SerializedObject(data);
SerializedProperty prop = serializedObject.GetIterator();
if (prop.NextVisible(true)) {
do {
if(prop.name == "m_Script") continue;
var subProp = serializedObject.FindProperty(prop.name);
float height = EditorGUI.GetPropertyHeight(subProp, null, true) + EditorGUIUtility.standardVerticalSpacing;
totalHeight += height;
}
while (prop.NextVisible(false));
}
// Add a tiny bit of height if open for the background
totalHeight += EditorGUIUtility.standardVerticalSpacing;
serializedObject.Dispose();
}
return totalHeight;
}
const int buttonWidth = 66;
static readonly List<string> ignoreClassFullNames = new List<string>{ "TMPro.TMP_FontAsset" };
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
EditorGUI.BeginProperty (position, label, property);
var type = GetFieldType();
if(type == null || ignoreClassFullNames.Contains(type.FullName)) {
EditorGUI.PropertyField(position, property, label);
EditorGUI.EndProperty ();
return;
}
ScriptableObject propertySO = null;
if(!property.hasMultipleDifferentValues && property.serializedObject.targetObject != null && property.serializedObject.targetObject is ScriptableObject) {
propertySO = (ScriptableObject)property.serializedObject.targetObject;
}
var propertyRect = Rect.zero;
var guiContent = new GUIContent(property.displayName);
var foldoutRect = new Rect(position.x, position.y, EditorGUIUtility.labelWidth, EditorGUIUtility.singleLineHeight);
if(property.objectReferenceValue != null && AreAnySubPropertiesVisible(property)) {
property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, guiContent, true);
} else {
// So yeah having a foldout look like a label is a weird hack
// but both code paths seem to need to be a foldout or
// the object field control goes weird when the codepath changes.
// I guess because foldout is an interactable control of its own and throws off the controlID?
foldoutRect.x += 12;
EditorGUI.Foldout(foldoutRect, property.isExpanded, guiContent, true, EditorStyles.label);
}
var indentedPosition = EditorGUI.IndentedRect(position);
var indentOffset = indentedPosition.x - position.x;
propertyRect = new Rect(position.x + (EditorGUIUtility.labelWidth - indentOffset), position.y, position.width - (EditorGUIUtility.labelWidth - indentOffset), EditorGUIUtility.singleLineHeight);
if(propertySO != null || property.objectReferenceValue == null) {
propertyRect.width -= buttonWidth;
}
EditorGUI.ObjectField(propertyRect, property, type, GUIContent.none);
if (GUI.changed) property.serializedObject.ApplyModifiedProperties();
var buttonRect = new Rect(position.x + position.width - buttonWidth, position.y, buttonWidth, EditorGUIUtility.singleLineHeight);
if(property.propertyType == SerializedPropertyType.ObjectReference && property.objectReferenceValue != null) {
var data = (ScriptableObject)property.objectReferenceValue;
if(property.isExpanded) {
// Draw a background that shows us clearly which fields are part of the ScriptableObject
GUI.Box(new Rect(0, position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing - 1, Screen.width, position.height - EditorGUIUtility.singleLineHeight - EditorGUIUtility.standardVerticalSpacing), "");
EditorGUI.indentLevel++;
SerializedObject serializedObject = new SerializedObject(data);
// Iterate over all the values and draw them
SerializedProperty prop = serializedObject.GetIterator();
float y = position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
if (prop.NextVisible(true)) {
do {
// Don't bother drawing the class file
if(prop.name == "m_Script") continue;
float height = EditorGUI.GetPropertyHeight(prop, new GUIContent(prop.displayName), true);
EditorGUI.PropertyField(new Rect(position.x, y, position.width-buttonWidth, height), prop, true);
y += height + EditorGUIUtility.standardVerticalSpacing;
}
while (prop.NextVisible(false));
}
if (GUI.changed)
serializedObject.ApplyModifiedProperties();
serializedObject.Dispose();
EditorGUI.indentLevel--;
}
} else {
if(GUI.Button(buttonRect, "Create")) {
string selectedAssetPath = "Assets";
if(property.serializedObject.targetObject is MonoBehaviour) {
MonoScript ms = MonoScript.FromMonoBehaviour((MonoBehaviour)property.serializedObject.targetObject);
selectedAssetPath = System.IO.Path.GetDirectoryName(AssetDatabase.GetAssetPath( ms ));
}
property.objectReferenceValue = CreateAssetWithSavePrompt(type, selectedAssetPath);
}
}
property.serializedObject.ApplyModifiedProperties();
EditorGUI.EndProperty ();
}
public static T _GUILayout<T> (string label, T objectReferenceValue, ref bool isExpanded) where T : ScriptableObject {
return _GUILayout<T>(new GUIContent(label), objectReferenceValue, ref isExpanded);
}
public static T _GUILayout<T> (GUIContent label, T objectReferenceValue, ref bool isExpanded) where T : ScriptableObject {
Rect position = EditorGUILayout.BeginVertical();
var propertyRect = Rect.zero;
var guiContent = label;
var foldoutRect = new Rect(position.x, position.y, EditorGUIUtility.labelWidth, EditorGUIUtility.singleLineHeight);
if(objectReferenceValue != null) {
isExpanded = EditorGUI.Foldout(foldoutRect, isExpanded, guiContent, true);
var indentedPosition = EditorGUI.IndentedRect(position);
var indentOffset = indentedPosition.x - position.x;
propertyRect = new Rect(position.x + EditorGUIUtility.labelWidth - indentOffset, position.y, position.width - EditorGUIUtility.labelWidth - indentOffset, EditorGUIUtility.singleLineHeight);
} else {
// So yeah having a foldout look like a label is a weird hack
// but both code paths seem to need to be a foldout or
// the object field control goes weird when the codepath changes.
// I guess because foldout is an interactable control of its own and throws off the controlID?
foldoutRect.x += 12;
EditorGUI.Foldout(foldoutRect, isExpanded, guiContent, true, EditorStyles.label);
var indentedPosition = EditorGUI.IndentedRect(position);
var indentOffset = indentedPosition.x - position.x;
propertyRect = new Rect(position.x + EditorGUIUtility.labelWidth - indentOffset, position.y, position.width - EditorGUIUtility.labelWidth - indentOffset-60, EditorGUIUtility.singleLineHeight);
}
EditorGUILayout.BeginHorizontal();
objectReferenceValue = EditorGUILayout.ObjectField(new GUIContent(" "), objectReferenceValue, typeof(T), false) as T;
if(objectReferenceValue != null) {
EditorGUILayout.EndHorizontal();
if(isExpanded) {
DrawScriptableObjectChildFields(objectReferenceValue);
}
} else {
if(GUILayout.Button("Create", GUILayout.Width(buttonWidth))) {
string selectedAssetPath = "Assets";
var newAsset = CreateAssetWithSavePrompt(typeof(T), selectedAssetPath);
if(newAsset != null) {
objectReferenceValue = (T)newAsset;
}
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
return objectReferenceValue;
}
static void DrawScriptableObjectChildFields<T> (T objectReferenceValue) where T : ScriptableObject {
// Draw a background that shows us clearly which fields are part of the ScriptableObject
EditorGUI.indentLevel++;
EditorGUILayout.BeginVertical(GUI.skin.box);
var serializedObject = new SerializedObject(objectReferenceValue);
// Iterate over all the values and draw them
SerializedProperty prop = serializedObject.GetIterator();
if (prop.NextVisible(true)) {
do {
// Don't bother drawing the class file
if(prop.name == "m_Script") continue;
EditorGUILayout.PropertyField(prop, true);
}
while (prop.NextVisible(false));
}
if (GUI.changed)
serializedObject.ApplyModifiedProperties();
serializedObject.Dispose();
EditorGUILayout.EndVertical();
EditorGUI.indentLevel--;
}
public static T DrawScriptableObjectField<T> (GUIContent label, T objectReferenceValue, ref bool isExpanded) where T : ScriptableObject {
Rect position = EditorGUILayout.BeginVertical();
var propertyRect = Rect.zero;
var guiContent = label;
var foldoutRect = new Rect(position.x, position.y, EditorGUIUtility.labelWidth, EditorGUIUtility.singleLineHeight);
if(objectReferenceValue != null) {
isExpanded = EditorGUI.Foldout(foldoutRect, isExpanded, guiContent, true);
var indentedPosition = EditorGUI.IndentedRect(position);
var indentOffset = indentedPosition.x - position.x;
propertyRect = new Rect(position.x + EditorGUIUtility.labelWidth - indentOffset, position.y, position.width - EditorGUIUtility.labelWidth - indentOffset, EditorGUIUtility.singleLineHeight);
} else {
// So yeah having a foldout look like a label is a weird hack
// but both code paths seem to need to be a foldout or
// the object field control goes weird when the codepath changes.
// I guess because foldout is an interactable control of its own and throws off the controlID?
foldoutRect.x += 12;
EditorGUI.Foldout(foldoutRect, isExpanded, guiContent, true, EditorStyles.label);
var indentedPosition = EditorGUI.IndentedRect(position);
var indentOffset = indentedPosition.x - position.x;
propertyRect = new Rect(position.x + EditorGUIUtility.labelWidth - indentOffset, position.y, position.width - EditorGUIUtility.labelWidth - indentOffset-60, EditorGUIUtility.singleLineHeight);
}
EditorGUILayout.BeginHorizontal();
objectReferenceValue = EditorGUILayout.ObjectField(new GUIContent(" "), objectReferenceValue, typeof(T), false) as T;
if(objectReferenceValue != null) {
EditorGUILayout.EndHorizontal();
if(isExpanded) {
}
} else {
if(GUILayout.Button("Create", GUILayout.Width(buttonWidth))) {
string selectedAssetPath = "Assets";
var newAsset = CreateAssetWithSavePrompt(typeof(T), selectedAssetPath);
if(newAsset != null) {
objectReferenceValue = (T)newAsset;
}
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
return objectReferenceValue;
}
// Creates a new ScriptableObject via the default Save File panel
static ScriptableObject CreateAssetWithSavePrompt (Type type, string path) {
path = EditorUtility.SaveFilePanelInProject("Save ScriptableObject", type.Name+".asset", "asset", "Enter a file name for the ScriptableObject.", path);
if (path == "") return null;
ScriptableObject asset = ScriptableObject.CreateInstance(type);
AssetDatabase.CreateAsset (asset, path);
AssetDatabase.SaveAssets ();
AssetDatabase.Refresh();
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
EditorGUIUtility.PingObject(asset);
return asset;
}
Type GetFieldType () {
Type type = fieldInfo.FieldType;
if(type.IsArray) type = type.GetElementType();
else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) type = type.GetGenericArguments()[0];
return type;
}
static bool AreAnySubPropertiesVisible(SerializedProperty property) {
var data = (ScriptableObject)property.objectReferenceValue;
SerializedObject serializedObject = new SerializedObject(data);
SerializedProperty prop = serializedObject.GetIterator();
while (prop.NextVisible(true)) {
if (prop.name == "m_Script") continue;
return true; //if theres any visible property other than m_script
}
serializedObject.Dispose();
return false;
}
}
@MostHated
Copy link

MostHated commented Jun 16, 2021

Not 100% sure what is going on here, but it sure isn't happy. I can easily open and close the first two levels of foldouts, but after that, it seems to be a "no-go". I can click in fields and adjust sliders, but can't open or close inner nested foldouts. Also, the items below the top one keep randomly disappearing.

Not even sure if much can be done about all this, I might just need a full-on custom editor specifically for my use case.

whattheheck

@tomkail
Copy link
Author

tomkail commented Jun 22, 2021

Not 100% sure what is going on here, but it sure isn't happy. I can easily open and close the first two levels of foldouts, but after that, it seems to be a "no-go". I can click in fields and adjust sliders, but can't open or close inner nested foldouts. Also, the items below the top one keep randomly disappearing.

Not even sure if much can be done about all this, I might just need a full-on custom editor specifically for my use case.

whattheheck

Sorry yeah, your editor setup looks quite complex. I'd guess Unity culls anything it believes to be off-screen, and it's failing to do that properly - or something. Let us know if you do ever work it out!

@Wheelinardo
Copy link

Hi there. I have an ability system where I will often nest SOs inside of the same SO like so:

image

My issue is that your script seems to perform actions on the scriptable objects by name so that if I try to expand one of my lists it will do it on both scriptable objects. Is there a workaround for this?

@tomkail
Copy link
Author

tomkail commented Oct 20, 2021 via email

@MostHated
Copy link

Yeah, that happens for me as well. I just kind of chalked it up to "Unity being Unity".

@forestrf
Copy link

For the situations when the scriptableobject is abstract, show a dropdown with types that inherit from it to choose from when creating the asset (otherwise it will try to create a null SO):

string GetSelectedAssetPath() {
	string selectedAssetPath = "Assets";
	if (property.serializedObject.targetObject is MonoBehaviour) {
		MonoScript ms = MonoScript.FromMonoBehaviour((MonoBehaviour) property.serializedObject.targetObject);
		selectedAssetPath = System.IO.Path.GetDirectoryName(AssetDatabase.GetAssetPath(ms));
	}
	return selectedAssetPath;
}

if (GUI.Button(buttonRect, "Create")) {
	if (type.IsAbstract) {
		GenericMenu typeChooser = new GenericMenu();
		foreach (var elem in type.Assembly.GetTypes().Where(t => type.IsAssignableFrom(t))) {
			if (elem.IsAbstract) continue;
			typeChooser.AddItem(new GUIContent(elem.Name), false, (elem) => {
				property.objectReferenceValue = CreateAssetWithSavePrompt(elem as Type, GetSelectedAssetPath());
				property.serializedObject.ApplyModifiedProperties();
			}, elem);
		}
		typeChooser.ShowAsContext();
	}
	else {
		property.objectReferenceValue = CreateAssetWithSavePrompt(type, GetSelectedAssetPath());
	}
}

@SimOgaard
Copy link

Fantastic script, im just wondering, has anyone managed to get List and/or Arrays of ScriptableObjects working? Without having to fall back to [NonReorderable] as @Steffenvy stated. In my case, they render ontop of eachothers, as if GetPropertyHeight returns the wrong value.

I'm on Unity 2021.3.4f1

@tomkail
Copy link
Author

tomkail commented Jul 12, 2022

This is a particularly shitty Unity bug - you can track it here.
Would recommend upgrading to the latest LTS, which fixes it.

@tomkail
Copy link
Author

tomkail commented Jul 12, 2022

And - for new readers; this gist is updated when fixes/upgrades are suggested in this thread, so you can use the gist rather than trawling comments for updates :)

@j-schoch
Copy link

j-schoch commented Jul 28, 2022

Love this script, super useful!

I would appreciate if you would consider including some logic that could prevent the "Create" button from appearing when the property type is abstract.

Or perhaps add a subclass selector? (see this code for example of that)

Edit: Just noticed the above post requesting the same thing.https://gist.github.com/tomkail/ba4136e6aa990f4dc94e0d39ec6a058c?permalink_comment_id=3960949#gistcomment-3960949

@tomkail
Copy link
Author

tomkail commented Jul 29, 2022

Np! That could be useful but I don't have time to build it - if anyone fancies writing that in I'd happily update this gist :)

@SanielX
Copy link

SanielX commented Aug 20, 2022

Not 100% sure what is going on here, but it sure isn't happy. I can easily open and close the first two levels of foldouts, but after that, it seems to be a "no-go". I can click in fields and adjust sliders, but can't open or close inner nested foldouts. Also, the items below the top one keep randomly disappearing.

If anyone still happens to have this problem. It seems that the reason this happens is because when you use new SerializedObject you do not dispose it after. I'm assuming GC doesn't get a chance to collect them and they get broken or something. I'm talking about lines 102 and 29. You need to dispose those after usage and it should work

@tomkail
Copy link
Author

tomkail commented Aug 22, 2022

Not 100% sure what is going on here, but it sure isn't happy. I can easily open and close the first two levels of foldouts, but after that, it seems to be a "no-go". I can click in fields and adjust sliders, but can't open or close inner nested foldouts. Also, the items below the top one keep randomly disappearing.

If anyone still happens to have this problem. It seems that the reason this happens is because when you use new SerializedObject you do not dispose it after. I'm assuming GC doesn't get a chance to collect them and they get broken or something. I'm talking about lines 102 and 29. You need to dispose those after usage and it should work

Fantastic find, thank you! I've updated the gist. It compiles but I've not tested to see if it resolves the issue.

@rugelly
Copy link

rugelly commented Sep 28, 2022

I have a possibly unique problem? values, no matter how nested, update fine in the hierarchy window. My problem is in the inspector: nested values not updating during runtime except for a single time when moused over the inspection window.

Here I have a scriptableobject that contains a float, it updates during runtime in the inspector window so no problems here.
first

Now this one is a scriptableobject that references the previous one. The problem is that it only updates when I mouse over the inspector.
second

Is there a way to update the value without wildly swinging my mouse around?

@SanielX
Copy link

SanielX commented Sep 29, 2022

Is there a way to update the value without wildly swinging my mouse around?

Well first of all the idea of storing runtime values of a gameobject in asset is nonsensical
The only way you can make editor update values continuously is to create CustomEditor that has RequiresConstantRepaint method overriden which returns true. You can also switch inspector to debug mode but that will obviously disable inline editing attribute

@rugelly
Copy link

rugelly commented Sep 30, 2022

Is there a way to update the value without wildly swinging my mouse around?

Well first of all the idea of storing runtime values of a gameobject in asset is nonsensical The only way you can make editor update values continuously is to create CustomEditor that has RequiresConstantRepaint method overriden which returns true. You can also switch inspector to debug mode but that will obviously disable inline editing attribute

custom editor works like a charm thanks!
And thats not what im doing with the values so dont panic, thanks for the concern lol

@perrauo
Copy link

perrauo commented Dec 10, 2022

With Unity 2022.2 I am having a weird offset for ExtendObject properties. Any ideas?
image

@perrauo
Copy link

perrauo commented Dec 10, 2022

Same problem for Serializable classes used as members of the extended object. I think foldout in general is the issue.
image

@Manamongods
Copy link

That sounds to me on first impression to be caused by Unity's switch to UI toolkit default editors. See if the problem is gone when changing the setting "Project Settings/Editor/Use IMGUI Default Inspector" to true. I believe UI Toolkit might not properly set EditorGUIUtility.hierarchyMode to true in its PropertyField code.

@robrab2000
Copy link

Is there a way I can exclude a specific type of ScriptableObject from being drawn with this?

@tomkail
Copy link
Author

tomkail commented Feb 1, 2023

@robrab2000 Yup, see ignoreClassFullNames

@robrab2000
Copy link

Thanks! It doesn't seem to be fixing the issue I'm having however. I'm using OdinInspector to create window which has buttons and does stuff. I want to be able to assign a custom scriptable object in the window but it seems to be struggling to find the type. When it renders the editor window I get loads of errors: NullReferenceException: Object reference not set to an instance of an object ExtendedScriptableObjectDrawer.GetFieldType () (at Assets/Scripts/SO-Scripts/Editor/ExtendedScriptableObjectDrawer.cs:271)

Line 271 is:
Type GetFieldType () { Type type = fieldInfo.FieldType;

The GetFieldType() function necessarily gets called before checking against the ignoreClassFullNames list. Perhaps the issue is with Odin though...

@tomkail
Copy link
Author

tomkail commented Feb 1, 2023 via email

@robrab2000
Copy link

I'm drawing a custom scriptable object within an OdinEditorWindow

Screenshot 2023-02-01 at 14 56 04

Screenshot 2023-02-01 at 14 56 25

Here is the window shown with the ExtendedScriptableObjectDrawer disabled and then enabled
Screenshot 2023-02-01 at 16 37 43
Screenshot 2023-02-01 at 13 05 51

@larryPlayabl
Copy link

This is such a great tool. Thank you!

I've been getting Unity editor crashes in this case:

  • My ScriptableObject type (call it MySOType) contains a List
  • I create a cyclical reference using this list: A -> B -> A

I've done some digging through the code to try to prevent it by statically tracking some state but I haven't been able to prevent the crash. Any thoughts?

@Manamongods
Copy link

Manamongods commented Mar 1, 2023

This is such a great tool. Thank you!

I've been getting Unity editor crashes in this case:

  • My ScriptableObject type (call it MySOType) contains a List
  • I create a cyclical reference using this list: A -> B -> A

I've done some digging through the code to try to prevent it by statically tracking some state but I haven't been able to prevent the crash. Any thoughts?

I believe this can be fixed by adding lines similar to these, taken from my own modified script so some changes are probably necessary:

public static uint maxDepth = 5;

public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
    depth++;
    try
    {
        float totalHeight = EditorGUIUtility.singleLineHeight;
        var so = property.objectReferenceValue as ScriptableObject;
        if (depth > maxDepth || so == null)
            return totalHeight;

        //...
    }
    finally
    {
        depth--;
    }
}

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    depth++;
    try
    {
        label = EditorGUI.BeginProperty(position, label, property);

        var type = GetFieldType();


        // Ignoring
        if (depth > maxDepth || type == null
            || ignoreClassFullNames.Contains(type.FullName)
            || Attribute.IsDefined(fieldInfo, typeof(NonExtendedScriptableObjectAttribute)))
        {
            property.isExpanded = false; // Just in case
            DoObjectField(position, property, label, type);
            //EditorGUI.PropertyField(position, property, label);
            EditorGUI.EndProperty();
            return;
        }

        //...
    }
    finally
    {
        depth--;
    }
}

@tomkail
Copy link
Author

tomkail commented Mar 3, 2023

This is such a great tool. Thank you!
I've been getting Unity editor crashes in this case:

  • My ScriptableObject type (call it MySOType) contains a List
  • I create a cyclical reference using this list: A -> B -> A

I've done some digging through the code to try to prevent it by statically tracking some state but I haven't been able to prevent the crash. Any thoughts?

I believe this can be fixed by adding lines similar to these, taken from my own modified script so some changes are probably necessary:

public static uint maxDepth = 5;

public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
    depth++;
    try
    {
        float totalHeight = EditorGUIUtility.singleLineHeight;
        var so = property.objectReferenceValue as ScriptableObject;
        if (depth > maxDepth || so == null)
            return totalHeight;

        //...
    }
    finally
    {
        depth--;
    }
}

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    depth++;
    try
    {
        label = EditorGUI.BeginProperty(position, label, property);

        var type = GetFieldType();


        // Ignoring
        if (depth > maxDepth || type == null
            || ignoreClassFullNames.Contains(type.FullName)
            || Attribute.IsDefined(fieldInfo, typeof(NonExtendedScriptableObjectAttribute)))
        {
            property.isExpanded = false; // Just in case
            DoObjectField(position, property, label, type);
            //EditorGUI.PropertyField(position, property, label);
            EditorGUI.EndProperty();
            return;
        }

        //...
    }
    finally
    {
        depth--;
    }
}

Super helpful! Thanks! If anyone can confirm this works we can merge it in; I've not got the time to do it just now. Ta!

@J3ster1337
Copy link

Thank you for saving my day! (probably more than a day actually :D)

@restush
Copy link

restush commented Jun 12, 2023

I'm not sure about the codes, but I have move to UIToolkit. It's less codes to write editor and easy to implement.

@EliCDavis
Copy link

Added a small code change so it won't trype to create ScriptableObjects of abstract class

                if (!type.IsAbstract && GUI.Button(buttonRect, "Create"))

@EliCDavis
Copy link

Actually took it a step further,

Made a Property Attribute

    public class ExtendableScriptableObject : PropertyAttribute
    {
        public Type createType;


        public ExtendableScriptableObject()
        {
        }

        public ExtendableScriptableObject(Type createType)
        {
            this.createType = createType;
        }
    }

And then edited the create function

else if (!type.IsAbstract || propertyAttribute.createType != null)
           {
               if (GUI.Button(buttonRect, "Create"))
               {
                   string selectedAssetPath = "Assets";
                   if (property.serializedObject.targetObject is MonoBehaviour)
                   {
                       MonoScript ms =
                           MonoScript.FromMonoBehaviour((MonoBehaviour)property.serializedObject.targetObject);
                       selectedAssetPath = System.IO.Path.GetDirectoryName(AssetDatabase.GetAssetPath(ms));
                   }

                   var typeToCreate = type;
                   if (propertyAttribute.createType != null)
                   {
                       typeToCreate = propertyAttribute.createType;
                   }

                   property.objectReferenceValue = CreateAssetWithSavePrompt(typeToCreate, selectedAssetPath);
               }
           }

And now I can use it like this: (RecoludeAPIRecordingResolver extends RecordingResolverConfig)

        [ExtendableScriptableObject(typeof(RecoludeAPIRecordingResolver))] [SerializeField]
        private RecordingResolverConfig resolverConfig;

@WatchaGames
Copy link

WatchaGames commented Nov 13, 2023

Hi, love this. Not a specialist of prop drawer in unity.
Updating to latest Odin in my main project started to have errors resulting in wrangled layout in inspector of a gameobject with Scriptables.

After a bit of digging (and again I don't know what Im doing in Prop Drawers ;-)
I had to add a test in this function so that it does not break/stops when encountering refs to scriptables objects that are not referencing any (basically null pointers to SO in the MonoBehaviour)

Type GetFieldType () {
		// FIX NICO ? IF NULL then dont try to get type
		if (fieldInfo == null)
		{
			return null;
		}
		Type type = fieldInfo.FieldType;
		if(type.IsArray) type = type.GetElementType();
		else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) type = type.GetGenericArguments()[0];
		return type;
	}


Does it make sense ?
Thanks a lot for this.
Nick

@sandsalamand
Copy link

If you end up with a Type Mismatch in your inspector, this script will spam invalid cast warnings at this line in AreAnySubPropertiesVisible:
var data = (ScriptableObject)property.objectReferenceValue;

Does anyone know of a simple way to guard against this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment