Investigating SerializedProperty, custom controls, OnSceneGUI, Handles and multi-editing

I’ve been developing tools for the Unity editor for quite some time now, but I keep stumbling over basics. Today, I went over a few concepts and thought I’d share my results with the community. Some of this may help beginners understand different systems and for advanced users, I would be happy to ask some questions myself.

My test setup consists of a MonoBehaviour component added to a GameObject in the scene. The GameObject is then turned into a prefab and duplicated. We can now test the following cases:

  • Edit a prefab instance in the scene via the inspector
  • Edit a prefab in prefab mode
  • Multi-Edit prefab instances in the scene
  • Edit via SceneView Handles

And here is my test code:

using UnityEditor;
using UnityEngine;

public class PrefabEditTest : MonoBehaviour
{
    public Vector3 someValue;
    public bool useCustomSceneViewGUICallback = true;
}

[CustomEditor(typeof(PrefabEditTest))]
[CanEditMultipleObjects]
public class PrefabEditTestEditor : Editor
{
    public override void OnInspectorGUI()
    {
        DrawToggle();
        DrawSerializedProperty();
        DrawCustomControl();
    }

    private void DrawToggle()
    {
        // This is only here to easily switch between using Editor.SceneViewGUI and
        // a custom delegate added to SceneView.duringSceneGui.
        serializedObject.Update();
        var toggleProp = serializedObject.FindProperty("useCustomSceneViewGUICallback");
        EditorGUILayout.PropertyField(toggleProp);
        serializedObject.ApplyModifiedProperties();
    }

    private void DrawSerializedProperty()
    {
        // Regular inspector fields via SerializedProperty.
        // Everything works and is handled automatically:
        // - Undo
        // - Prefab instance overrides
        // - Editing in prefab mode
        // - Setting scene dirty and saving changes.
        // - Multi-object-editing

        // Update the object representation from serialization. This ensures that the MonoBehaviour
        // class instance of PrefabEditTest will have the most recent values that are actually stored on disk.
        serializedObject.Update();
        SerializedProperty prop = serializedObject.FindProperty("someValue");

        // This automatically records undo und handles prefab overrides (bold text and blue markings).
        // It basically does everything Unity supports in one call.
        EditorGUILayout.PropertyField(prop);

        // Make sure we write changed values to disk.
        serializedObject.ApplyModifiedProperties();
    }

    private void DrawCustomControl()
    {
        // Try to handle the same functionality with individual steps (because sometimes we need control).

        // This time we directly edit the reference to the target object (our MonoBehaviour class).
        PrefabEditTest script = (PrefabEditTest)target;

        // However, we still need the SerializedProperty to retrieve information such as prefab overrides.
        SerializedProperty prop = serializedObject.FindProperty("someValue");

        float height = EditorGUIUtility.wideMode ? EditorGUIUtility.singleLineHeight : EditorGUIUtility.singleLineHeight * 2f;
        Rect rect = EditorGUILayout.GetControlRect(true, height);

        // Handles bold label when this property is a prefab override and shows mixed values indicator when multi-editing.
        // However, small ISSUE_A: When a Vector3 component is overriden but only the x value is different,
        // it makes all three components bold, although only x should be shown as a bold override.
        var label = EditorGUI.BeginProperty(rect, new GUIContent(prop.displayName), prop);

        // Only do something if the values from our inspector control have actually changed.
        EditorGUI.BeginChangeCheck();

        // Do not write the value back to the script immediately, because we first need to record an Undo step.
        var value = EditorGUI.Vector3Field(rect, label, script.someValue);
        if (EditorGUI.EndChangeCheck())
        {
            // Handle multi-object editing. Apply changes to all instances, not only the first one.
            for (int i = 0; i < targets.Length; i++)
            {
                var target = targets[i];
                script = (PrefabEditTest)target;

                // Record undo of previous values on object (works nicely).
                Undo.RecordObject(target, "Change Value");

                // Now change the values on the MonoBehaviour class.
                script.someValue = value;

                #region ISSUE_B
                // Documentation says: "If this method is not called, changes made to the instance are lost."
                // However, in my tests, this does not change the behaviour at all, changes don't seem to be lost if I don't use it.
                // Also, should I only call this on prefab instances in the scene or can I safely call it on all GameObjects or Prefabs?
                // For Example, should I check for '!EditorUtility.IsPersistent(target)' first?
                PrefabUtility.RecordPrefabInstancePropertyModifications(target);
                #endregion
            }

            #region ISSUE_C
            // At this point, most things seem to work, except:
            // When multi-editing targets with multiple different values, their values all become the same under the hood (which is correct),
            // however, when moving the slider or changing values in the field, the inspector still shows "mixed values" until the
            // target is deselected and selected again. Basically, prop.hasMultipleDifferentValues is still true and only reset
            // once the editor is recreated. Updating or applying the serializedObject does not help.
            // To fix this, it seems, I need to assign the values back to the SerializedProperty and apply the SerializedObject.
            prop.vector3Value = script.someValue;
            serializedObject.ApplyModifiedProperties();
            #endregion
        }

        EditorGUI.EndProperty();
    }

    public void OnSceneGUI()
    {
        // Using serializedObject in this method results in an error:
        // "The serializedObject should not be used inside OnSceneGUI or OnPreviewGUI. Use the target property directly instead."
        // It seems, multi-object-editing cannot be easily implemented. At least, if we want to draw
        // a single handle control and change the values of multiple instances.
        // Instead, this method will draw multiple handles, one for each instance separately.

        PrefabEditTest script = (PrefabEditTest)target;

        // Disable this example if we are using the custom callback.
        if (script.useCustomSceneViewGUICallback)
            return;

        EditorGUI.BeginChangeCheck();
        Vector3 value = Handles.Slider(script.someValue, Vector3.right);
        if (EditorGUI.EndChangeCheck())
        {
            // Next error: when trying to use the targets array to support multi-editing:
            // "The targets array should not be used inside OnSceneGUI or OnPreviewGUI. Use the single target property instead."

            // Record undo of previous values on object (works nicely).
            Undo.RecordObject(target, "Change Value");

            // Now change the values on the MonoBehaviour class.
            script.someValue = value;

            #region ISSUE_B
            // Documentation says: "If this method is not called, changes made to the instance are lost."
            // However, in my tests, this does not change the behaviour at all, changes don't seem to be lost if I don't use it.
            // Also, should I only call this on prefab instances in the scene or can I safely call it on all GameObjects or Prefabs?
            // For Example, should I check for '!EditorUtility.IsPersistent(target)' first?
            PrefabUtility.RecordPrefabInstancePropertyModifications(target);
            #endregion
        }

        #region ISSUE_D
        // Using Handles in OnSceneGUI this way, results in the basic functionality working, but multi-object-editing is not fully supported.
        #endregion
    }

    private void OnEnable()
    {
        // Here we add a callback to the delegate, which hooks into the
        // same loop as OnSceneGUI, but allows us to use SerializedProperty.
        SceneView.duringSceneGui += MySceneGUI;
    }

    private void OnDisable()
    {
        SceneView.duringSceneGui -= MySceneGUI;
    }

    private void MySceneGUI(SceneView obj)
    {
        PrefabEditTest script = (PrefabEditTest)target;

        // Disable this example if we are not using the custom callback.
        if (!script.useCustomSceneViewGUICallback)
            return;

        // With a custom callback, we can support multi-editing simply by using serializedObject.
        serializedObject.Update();

        // This also makes the rest of the code very easy.
        SerializedProperty prop = serializedObject.FindProperty("someValue");
        prop.vector3Value = Handles.Slider(prop.vector3Value, Vector3.right);

        serializedObject.ApplyModifiedProperties();
    }
}

So here are my own questions:

ISSUE_A
5254997--525140--upload_2019-12-6_22-6-56.png
When I use a custom control to draw “Some Value” I use BeginProperty and EndProperty to make sure that the label is drawn bold and the blue line is added to indicate a prefab override. However, it makes all three vector components x, y and z bold as well, whereas the SerializedProperty implementation only highlights x. How do I implement this correctly? Or is this a bug in BeginProperty?

ISSUE_B
The documentation advises to call PrefabUtility.RecordPrefabInstancePropertyModifications(target) after changes have been made to a prefab instance. In my test, however, there was no observable difference. Values were still saved. Do I really need to call this? Are there any cases which I haven’t tested which are affected by this? And do I need to first test, if the target is actually a prefab instance, e.g. use EditorUtility.IsPersistent(target) ?

Overall I feel like there are a few things I would like to see in the documentation. For example, why can’t I use serializedObject in Editor.OnSceneGUI, but it works fine in SceneView.duringSceneGUI? And what are the recommended approaches for Handles and multi-editing?

1 Like

I am having this issue with OnSceneGUI and multi-object editing too. However, I am just trying to draw the different instances.

Wow, this is so well documented and still no reply. I'm interested in the answer too. This would be the most important information from Unity to understand generic patterns to follow when a tool is implemented for Unity.