Search code examples
unity-game-engineunity-editorunity3d-editor

Unity3D editor: Rendering nested elements in sidebar


In Unity, one of my MonoBehaviours has a field pointing to another object (a ScriptableObject). If I double-click that field, I can see the fields of that object. How do I render those fields into the top-level MonoBehaviour's property drawer?

In picture form

What I have

enter image description here

(double-click the element)

enter image description here

What I want

enter image description here

I have my own [CustomEditor] component, but I can't quite get it to work right; stuff like this:

SerializedProperty activityStack = serializedObject.FindProperty("activityStack");
EditorGUILayout.PropertyField(activityStack.GetArrayElementAtIndex(0));

just renders the "Element 0 (Idle Activity)" bit and not the actual contents of the reference.


Solution

  • Because the default PropertyField for a ScriptableObject is just the one you get: A UnityEngine.Object reference field like for GameObject and Components and other assets ;)


    Of course you can implement what you want to achieve but that's a bit more complex and not really good for maintenance and I would not recommend it.


    I don't know your ScriptableObject so here an example

    public class ExampleSO : ScriptableObject
    {
        public int SomeInt;
        [SerializeField] private string _someString;
    }
    

    and your MonoBehaviour e.g.

    public class Example : MonoBehaviour
    {
        public List<ExampleSO> _SOList;
    }
    

    Then the editor could look like e.g.

    using UnityEditor;
    using UnityEngine;
    
    // This is the namespace for the ReorderableList
    using UnityEditorInternal;
    
    [CustomEditor(typeof(Example))]
    public class ExampleEditor : Editor
    {
        SerializedProperty _SOList;
    
        Example _example;
        MonoScript _script;
    
        ReorderableList _list;
    
        private void OnEnable()
        {
            // Link up the serializedProperty
            _SOList = serializedObject.FindProperty("_SOList");
    
            // get the casted target instance (only needed for drawing the script field)
            _example = (Example) target;
    
            // get the according script instance (only needed for drawing the script field)
            _script = MonoScript.FromMonoBehaviour(_example);
    
            // Set up the ReorderableList
            _list = new ReorderableList(serializedObject, _SOList, true, true, true, true)
            {
                // What shall be displayed as header for the list?
                drawHeaderCallback = (Rect rect) => EditorGUI.LabelField(rect, _SOList.displayName),
    
                // How is each element displayed?
                drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
                {
                    // Get the element in the list (SerializedProperty)
                    var element = _SOList.GetArrayElementAtIndex(index);
    
                    // and draw the default object reference field
                    EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), element, new GUIContent("Reference"));
    
                    // Check if an asset is referenced - if not we are done here
                    if (!element.objectReferenceValue) return;
    
                    // Otherwise get the SerializedObject for this asset
                    var elementSerializedObject = new SerializedObject(element.objectReferenceValue);
    
                    // and all the properties (SerializedProperty) of it you want to display
                    var someInt = elementSerializedObject.FindProperty("SomeInt");
                    var someString = elementSerializedObject.FindProperty("_someString");
    
                    // Similar to the OnInspectorGUI first load the current values into this serializedobject
                    elementSerializedObject.Update();
                    {
                        // Adding some indentation just to show that the following fields are actually belonging to the referenced asset
                        EditorGUI.indentLevel++;
                        {
                            rect = EditorGUI.IndentedRect(rect);
    
                            // shift down the rect by one line
                            rect.y += EditorGUIUtility.singleLineHeight;
    
                            // Draw the field for the Int 
                            EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), someInt);
    
                            // Shift down the rect another line
                            rect.y += EditorGUIUtility.singleLineHeight;
      
                            // Draw the string field
                            EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), someString);
                        }
                        EditorGUI.indentLevel--;
                    }
                    // Write back the changed values and trigger the checks for logging dirty states and Undo/Redo
                    elementSerializedObject.ApplyModifiedProperties();
                },
    
                // How much vertical space should be reserved for each element?
                elementHeightCallback = (int index) =>
                {
                    // Get the elements serialized property
                    var element = _SOList.GetArrayElementAtIndex(index);
    
                    // by default we have only the asset reference -> single line
                    var lines = 1;
    
                    // if the asset is referenced adds space for the additional fields
                    if (element.objectReferenceValue) lines += 2; // or how many lines you'll need
    
                    return lines * EditorGUIUtility.singleLineHeight;
                }
            };
        }
    
        public override void OnInspectorGUI()
        {
            // draw th script field
            DrawScriptField();
    
            // Load the current values into the serializedObject
            serializedObject.Update();
            {
                // let the ReorderableList do its magic
                _list.DoLayoutList();
            }
            // Write back the changed values into the actual instance
            serializedObject.ApplyModifiedProperties();
        }
    
        // Just draws the usual script field at the top of the Inspector
        private void DrawScriptField()
        {
            EditorGUI.BeginDisabledGroup(true);
            {
                EditorGUILayout.ObjectField("Script", _script, typeof(Example), false);
            }
            EditorGUI.EndDisabledGroup();
    
            EditorGUILayout.Space();
        }
    }
    

    Which results in the following Inspector. As you can see I opened the Isnpectors of the MonoBehaviour and two instances of the ExampleSO to show how the values are taken over to the actual instances

    enter image description here