Question about modifying PropertyFields inside a custom inspector.

Hello! I have a Node based system that I am trying to create, to help me with creating AI easier. So I decided to also add a custom inspector for it. But I am having trouble with assigning values to PropertyFields.

  • When I first tried it, I basiclly used the following, since I saw it on some tutorials:
public class InspectorView : VisualElement
{
    public new class UxmlFactory : UxmlFactory<InspectorView, VisualElement.UxmlTraits> { }
    private Editor editor;

    public InspectorView() { }

    /// <summary>
    /// This function creates an unity editor and places it inside our InspectorView
    /// </summary>
    /// <param name="nodeView"></param>
    internal void UpdateSelection(NodeView nodeView)
    {
        // Clears any previous selection
        Clear();

        // Destroy the previous editor instance if it exists
        if (editor != null)
        {
            UnityEngine.Object.DestroyImmediate(editor);
        }

        // Create a new custom editor for the selected node
        editor = Editor.CreateEditor(nodeView.node, typeof(InspectorEditor));


        // Create an IMGUI container
        IMGUIContainer container = new IMGUIContainer(() =>
        {
            if (editor.target)
            {
                // Begin the inspector GUI
                editor.OnInspectorGUI();
            }
        });
    }

At first it looked like it was working well! (Never speak too soon). I managed to basicly control and modify fields like int, float, string etc. Just like the default inspector.

But some of my nodes of course had parts like Transform, NavMeshAgent, List<>.
And when I tried to modify those parts- it basically refused. I couldnt drag anything to those fields from hierarchy or modify them in any way.

So, back to the drawing board. I tried other stuff like DragAndDrop etc. but couldnt make it work.

So in the end I decided to use ObjectField. Because for some reason, ObjectField seem to work, I could drag items to it from the hierarchy, assign items to it etc. etc.

Now only problem was OnInspectorGUI was also creating fields for those objects, so it was getting duplicated.

So- I basically decided to create a new class that inherits from Editor and use it instead so I can override the OnInspectorGUI since those fields created inside OnInspectorGUI didnt work.

  • Following is the new InspectorEditor file I created:
using UnityEditor;

[CustomEditor(typeof(Node), true)]
public class InspectorEditor : Editor
{
    public override void OnInspectorGUI()
    {
        EditorGUI.BeginChangeCheck();
        serializedObject.UpdateIfRequiredOrScript();
        SerializedProperty iterator = serializedObject.GetIterator();
        bool enterChildren = true;
        while (iterator.NextVisible(enterChildren))
        {
            if (iterator.propertyType == SerializedPropertyType.ObjectReference) continue;

            using (new EditorGUI.DisabledScope("m_Script" == iterator.propertyPath))
            {
                EditorGUILayout.PropertyField(iterator, true);
            }

            enterChildren = false;
        }

        serializedObject.ApplyModifiedProperties();
        EditorGUI.EndChangeCheck();
    }

}

I kinda copied the DrawDefaultInspectorFunction, (which I think was called by default?) and added this part:
if (iterator.propertyType == SerializedPropertyType.ObjectReference) continue; so it bypasses any Objects or not Generic types (I think?). Which worked, it no longer created SerializedProperties for stuff like Transform, NavMeshAgent, MeshCollider etc. etc.

  • And after that, I modified my original file so they fit together:
public class InspectorView : VisualElement
{
    public new class UxmlFactory : UxmlFactory<InspectorView, VisualElement.UxmlTraits> { }
    private Editor editor;

    public InspectorView() { }

    /// <summary>
    /// This function creates an unity editor and places it inside our InspectorView
    /// </summary>
    /// <param name="nodeView"></param>
    internal void UpdateSelection(NodeView nodeView)
    {
        // Clears any previous selection
        Clear();

        // Destroy the previous editor instance if it exists
        if (editor != null)
        {
            UnityEngine.Object.DestroyImmediate(editor);
        }

        // Create a new custom editor for the selected node
        editor = Editor.CreateEditor(nodeView.node, typeof(InspectorEditor));


        // Create an IMGUI container
        IMGUIContainer container = new IMGUIContainer(() =>
        {
            if (editor.target)
            {
                // Begin the inspector GUI
                editor.OnInspectorGUI();
            }
        });


        // Add the container to the InspectorView
        Add(container);




        FieldInfo[] fields = editor.target.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.GetField);


        foreach (FieldInfo field in fields)
        {
            if (!typeof(UnityEngine.Object).IsAssignableFrom(field.FieldType)) {
                continue;
            }

            ObjectField objectField = new ObjectField(field.Name[0].ToString().ToUpper() + field.Name.Substring(1))
            {
                objectType = field.FieldType,
                value = field.GetValue(editor.target) as UnityEngine.Object
            };

            objectField.RegisterValueChangedCallback(evt =>
            {
                field.SetValue(editor.target, evt.newValue);
            });

            Add(objectField);
        }
    }
}

So in following code, I get all the FieldInfo’s from the editor.target and use that information to create ObjectFields.

And it worked (kinda). I can finally assign gameObjects properly in my node inspector. But Lists still have problems. Some of my nodes use fields like List and those parts are still handled inside the OnInspectorGUI.

I tried to use ObjectFields for it, but I guess ObjectFields dont like lists xd. Or I am doing something wrong.

Anyways! After that long explanation I have some questions.

  • First of all, I dont like the way I handle this, since its still broken and I am kinda new to Unity so, I would be grateful if there are any better ways.
  • I guess my second question is tied to my first question but still: How can I make this work for lists as well.

Have you tried just using InspectorElement instead of creating editors? Unity - Scripting API: InspectorElement

Thanks for the fast response!

I tried implementing it like the following:

 public class InspectorView : VisualElement
{
     public new class UxmlFactory : UxmlFactory<InspectorView, VisualElement.UxmlTraits> { }
     private Editor editor;
     private InspectorElement inspectorElement;

     public InspectorView() { }

     /// <summary>
     /// This function creates an unity editor and places it inside our InspectorView
     /// </summary>
     /// <param name="nodeView"></param>
     internal void UpdateSelection(NodeView nodeView)
     {
         // Clears any previous selection
         Clear();

         // Destroy the previous editor instance if it exists
         if (editor != null)
         {
             UnityEngine.Object.DestroyImmediate(editor);
         }

         // Create a new custom editor for the selected node
         editor = Editor.CreateEditor(nodeView.node);

         // Create an InspectorElement for the editor
         inspectorElement = new InspectorElement(editor);

         // Add the InspectorElement to the InspectorView
         Add(inspectorElement);
}

But this only seem to show fields like string, int, float. But not objects like Transform, or other types of GameObjects

I can show one of my nodes for referance:

public class Patrol : Leaf
{
    public List<Transform> patrolPoints;
    public Transform entity;
   
    private NavMeshAgent agent;

    public override void OnStart()
    {
        agent = entity.GetComponent<NavMeshAgent>();

        SetStrategy(new PatrolStrategy(entity, agent, patrolPoints));
    }

    public override void OnStop()
    {
        // Noop
    }

    public override void SetDescription()
    {
        this.description = "Patrols given coordinates\r\n";
    }
}

I also want to be able to modify fields like Transform and List etc.

Just pass in asset to the inspector element, not the editor.

I am little new to this, can you show an example?

Literally just:

var inspectorElement = new InspectorElement(nodeView.node);

I see, still having the same issue. I think when I used editor.target it was basically pointing to the node so not much changed.
But at least its much compact now.

Currently the code is:

public class InspectorView : VisualElement
{
    public new class UxmlFactory : UxmlFactory<InspectorView, VisualElement.UxmlTraits> { }
    private InspectorElement inspectorElement;

    public InspectorView() { }

    /// <summary>
    /// This function creates a Unity editor and places it inside our InspectorView
    /// </summary>
    /// <param name="nodeView">The NodeView to display in the inspector</param>
    internal void UpdateSelection(NodeView nodeView)
    {
        // Clear any previous selection
        Clear();

        var inspectorElement = new InspectorElement(nodeView.node);

        // Add the InspectorElement to the InspectorView
        Add(inspectorElement);

    }
}

Also, I looked at it little bit more. When I look at my nodes with the default Unity inspector, I still seem to be having the same issue. Cant drag items to i and when I manually choose an item I get “Type mismatch”.

So- there might be a problem with the serialization?

  • My basic Node setup:

1) I wrote this above as well, but for it to look nice. Will write here as well.
An example Node:

public class Patrol : Leaf
{
    public List<Transform> patrolPoints;
    public Transform entity;

    private NavMeshAgent agent;

    public override void OnStart()
    {
        agent = entity.GetComponent<NavMeshAgent>();

        SetStrategy(new PatrolStrategy(entity, agent, patrolPoints));
    }

    public override void OnStop()
    {
        // Noop
    }

    public override void SetDescription()
    {
        this.description = "Patrols given coordinates\r\n";
    }
}

2) My leaf class basically inherits from Node:
public abstract class Leaf : Node

3) And starting part of my node class is:

[System.Serializable]
public abstract class Node : ScriptableObject
{
    public enum Status { Success, Failure, Running}
    [HideInInspector] public Status status = Status.Running;
    [HideInInspector] public bool started = false;

    [TextArea] public string description;
    public readonly int priority;
    [HideInInspector] public string guid;

    public Vector2 position;

    ...
    ...

Image as an example as well:

Yes because you’re trying to reference scene objects in a scriptable object (an asset).

Assets cannot reference scene objects.

Damn… I see the problem, thanks.
Is there a solution for this?

There was a whole thread about it here where I suggested some ideas: Efficient way to reference objects in the scene

In some situations you could serialise your node graph into the scene. Plain C# objects can be used in lieu of scriptable objects, using [SerializeReference] for by-reference and non-linear serialisation. They can just be drawn via a PropertyField rather than an InspectorElement in that case, which would allow scene-objects to be referenced.

I will make sure to read that.
I will post the solution here as well if I can find figure it out.

Thanks for the help!

1 Like

Sorry for bothering again, I did look into this little more. SerializeReference might actually work the best for this as you said.
I am just not sure how exactly to implement it.

What it means is your nodes becomes serializable C# classes rather than scriptable objects (So Node will inherit from nothing). Your nodes reference other nodes via fields serialized with [SerializeReference] rather than [SerializeField] like you normally would with Unity assets, and you can draw the serialized contents of each node with a PropertyField rather than an InspectorElement (you will need to get its serialized property, of course).

All in all, a lot can probably stay the same. Just the way the nodes will be serialized changes (but the reference assigning probably doesn’t change), you draw them via a slightly different mechanism, and probably have to factory instances different to how you might with scriptable objects.

I see!

  • Should I Serialize the node from my NodeView like:
public class NodeView : UnityEditor.Experimental.GraphView.Node
{
    public Action<NodeView> OnNodeSelected;
    [SerializeReference] public Node node;
    public Port input;
    public Port output;
    ...
    ...
  • Or specificly serialize objects inside the leaves manually like:
public class Patrol : Leaf
{
    [SerializeReference] public List<Transform> patrolPoints;
    [SerializeReference] public Transform entity;

    private NavMeshAgent agent;

    public override void OnStart()
    {
        agent = entity.GetComponent<NavMeshAgent>();
        SetStrategy(new PatrolStrategy(entity, agent, patrolPoints));
    }

    public override void OnStop()
    {
        // Noop
    }

    public override void SetDescription()
    {
        this.description = "Patrols given coordinates\r\n";
    }
}

And I have been thiking on using the following code for my editor, to implement PropertyFields:

[CustomEditor(typeof(Node), true)]
public class InspectorEditor : Editor
{
    public override void OnInspectorGUI()
    {
        EditorGUI.BeginChangeCheck();
        serializedObject.UpdateIfRequiredOrScript();
        SerializedProperty iterator = serializedObject.GetIterator();
        bool enterChildren = true;
        while (iterator.NextVisible(enterChildren))
        {
            using (new EditorGUI.DisabledScope("m_Script" == iterator.propertyPath))
            {
                EditorGUILayout.PropertyField(iterator, true);
            }

            enterChildren = false;
        }

        serializedObject.ApplyModifiedProperties();
        EditorGUI.EndChangeCheck();
    }
}

[SerializeReference] is for non-Unity object references. You do not use it on Unity object types (such as Transform). Please familiarise yourself with the documentation on SerializeReference.

So when you want to serialize a reference to a node, you use [SerializeReference] on where you want save the reference. This allows you to have multiple pointers to the same pure data C# object, without it being duplicated as it would with traditional Unity serialization.

As a rough idea of what I mean:

[System.Serializable]
public abstract class Node
{
   [SerializeReference]
   private List<Node> _connectedNodes = new(); // references to other nodes
   
   [SerializeField]
   private NodeContainer _container; // assuming they are all stored in a component, we serialize a normal reference
}

public class NodeContainer : MonoBehaviour
{
   [SerializeReference]
   private List<Node> _nodes = new(); // references to all the nodes in this container
}

You don’t use Editor’s on plain C# objects. You use PropertyDrawers. Where I meant you’d use a PropertyField is in your InspectorView visual element where you were trying to render the inspector of the node scriptable objects. You’d use a PropertyField instead of an InspectorElement. Make sense?

Hm, I see! I will try this out when I have time again!

Okay, I kinda solved my problem. But for some reason couldn’t manage to do it with SerializeReference. I kinda ended up with the following code:

public class InspectorView : VisualElement
{
    /// <summary>
    /// Needed for us to be able to use this view in UI Builder.
    /// </summary>
    public new class UxmlFactory : UxmlFactory<InspectorView, VisualElement.UxmlTraits> { }

    const float MarginBetweenElements = 4f;

    public InspectorView() { }

    /// <summary>
    /// Update the node we selected in the inspector with the specified NodeView.
    /// </summary>
    /// <param name="nodeView">The NodeView to display in the inspector.</param>
    internal void UpdateSelection(NodeView nodeView)
    {
        // Clears any previous selection
        Clear();

        // Returns if no node is selected
        if (nodeView.node == null) return;

        // Serializes the node from our nodeView
        SerializedObject serializedNodeObject = new SerializedObject(nodeView.node);
        // Creates the container containing our inspector, that targets our node
        VisualElement container = CreateInspector(serializedNodeObject);

        // Add the container to the InspectorView
        Add(container);
    }

    /// <summary>
    /// Create the inspector based on the serialized object.
    /// </summary>
    /// <param name="serializedObject">The serialized object we are inspecting.</param>
    /// <returns>Visual element containing our inspector.</returns>
    private VisualElement CreateInspector(SerializedObject serializedObject)
    {
        VisualElement container = new VisualElement();

        serializedObject.UpdateIfRequiredOrScript();
        SerializedProperty iterator = serializedObject.GetIterator();
        bool enterChildren = true;

        // Iterates over all the serialized properties of our serializedObject
        while (iterator.NextVisible(enterChildren))
        {
            // Bypass the m_Script
            if (iterator.propertyPath == "m_Script") continue;

            // Create an ObjectField if property is an Object Reference
            if (iterator.propertyType == SerializedPropertyType.ObjectReference && !iterator.isArray)
            {
                ObjectField objectField = CreateObjectField(iterator, serializedObject);
                container.Add(objectField);
            }
            // Create a new reorderableList if property is an array
            else if (iterator.isArray && iterator.type != "string")
            {
                // Draw reorderable list for arrays
                var targetObject = serializedObject.targetObject;
                var targetObjectClassType = targetObject.GetType();
                var field = targetObjectClassType.GetField(iterator.propertyPath);

                // If field is null continue
                if (field == null) continue;

                // Get the actual sourceList from the field
                dynamic sourceList = field.GetValue(targetObject);

                // Ensure sourceList is not null and contains types inheriting from Object
                if (sourceList != null && typeof(UnityEngine.Object).IsAssignableFrom(sourceList.GetType().GetGenericArguments()[0]))
                {
                    ListView listView = DrawReorderableList(sourceList, label: iterator.displayName);
                    container.Add(listView);
                }
                else
                {
                    PropertyField listView = new PropertyField(iterator);
                    listView.BindProperty(iterator);
                    container.Add(listView);
                }
            }
            // For other properties, use classic PropertyField
            else
            {
                PropertyField propertyField = new PropertyField(iterator);
                propertyField.BindProperty(iterator);
                propertyField.style.marginTop = MarginBetweenElements;
                container.Add(propertyField);
            }

            enterChildren = false;
        }

        // Apply any modified properties to the serialized object
        serializedObject.ApplyModifiedProperties();

        // Returns the newly created inspector
        return container;
    }

    /// <summary>
    /// Creates a new ObjectField for a given serialized property.
    /// </summary>
    /// <param name="serializedProperty">The serialized property to create the ObjectField for.</param>
    /// <param name="serializedObject">The serialized object associated with the property.</param>
    /// <returns>Newly created ObjectField.</returns>
    private ObjectField CreateObjectField(SerializedProperty serializedProperty, SerializedObject serializedObject)
    {
        FieldInfo fieldInfo = serializedProperty.GetUnderlyingField();
        if (fieldInfo == null) return null;

        System.Type objectType = fieldInfo.FieldType;
        UnityEngine.Object value = fieldInfo.GetValue(serializedObject.targetObject) as UnityEngine.Object;

        // Create and configure the ObjectField
        ObjectField objectField = new ObjectField(serializedProperty.displayName)
        {
            objectType = objectType,
            value = value
        };

        // Register a callback for when the value of the ObjectField changes
        objectField.RegisterValueChangedCallback(evt =>
        {
            fieldInfo.SetValue(serializedObject.targetObject, evt.newValue);
        });

        objectField.style.marginTop = MarginBetweenElements;
        return objectField;
    }

    /// <summary>
    /// Draw a reorderable list for the specified source list.
    /// </summary>
    /// <typeparam name="T">Type of the list elements.</typeparam>
    /// <param name="sourceList">The source list.</param>
    /// <param name="allowSceneObjects">Whether to allow scene objects.</param>
    /// <param name="label">The label to display for the reorderable list.</param>
    /// <returns>The ListView for the reorderable list.</returns>
    public static ListView DrawReorderableList<T>(List<T> sourceList, bool allowSceneObjects = true, string label = "List") where T : UnityEngine.Object
    {
        // Handle null sourceList
        if (sourceList == null)
        {
            throw new ArgumentNullException(nameof(sourceList));
        }

        var list = new ListView(sourceList)
        {
            virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight,
            showFoldoutHeader = true,
            headerTitle = label,
            showAddRemoveFooter = true,
            reorderMode = ListViewReorderMode.Animated,
            makeItem = () => new ObjectField
            {
                objectType = typeof(T),
                allowSceneObjects = allowSceneObjects
            },
            bindItem = (element, i) =>
            {
                ((ObjectField)element).value = sourceList[i];
                ((ObjectField)element).RegisterValueChangedCallback((value) =>
                {
                    sourceList[i] = (T)value.newValue;
                });
            }
        };

        list.style.marginTop = MarginBetweenElements;
        return list;
    }
}
  • I used ObjectFields and ListViews with RegisterValueChanged callback to register new values. It seem to get the job done, even though its not the solution I have been looking for xd. But my brain kinda melted after being stuck with this damn editor tool for a week.
  • So I shall follow the rule “if it works dont touch it”. hint: it doesnt

Thanks for all the help spiney!

I just wanted to return to this requick for anyone else if they read it. Above code still doesnt save any objects you add since the code does no serialization.
It just lets you only temporarily assign scene objects to that asset. It disappears after you restart the editor.

I ended up using a monobehaviour script that I added to my gameObject, and I use that to retrieve any gameObjects from the scene.

I mean looking at your code, in CreateObjectField, you aren’t binding the object field to the serialised property, so no wonder nothing is being saved.

Honestly the whole chunk of code seems completely unnecessary when you just instance an InspectorElement and get exactly the same effect that is bound properly.

And I’ll repeat, your project assets cannot reference scene objects.