Search code examples
c#exceptionunity-game-engineeditorcomponents

Unity3D MissingReferenceException when removing BoxCollider


I'm developing an open source editor tool for Unity3D https://github.com/JAFS6/BoxStairsTool and I'm writing a CustomEditor.

I create a main GameObject and I attach my script BoxStairs to it. This script attaches to the same GameObject a BoxCollider.

On my CustomEditor code, I have a method which is in charge of removing both two components attached before to finalize editing.

This is the code:

    private void FinalizeStairs ()
    {
        Undo.SetCurrentGroupName("Finalize stairs");
        BoxStairs script = (BoxStairs)target;
        GameObject go = script.gameObject;
        BoxCollider bc = go.GetComponent<BoxCollider>();
        
        if (bc != null)
        {
            Undo.DestroyObjectImmediate(bc);
        }
        Undo.DestroyObjectImmediate(target);
    }

This method is called on the method OnInspectorGUI after a button has been pressed

public override void OnInspectorGUI ()
{
    ...
    if (GUILayout.Button("Finalize stairs"))
    {
        FinalizeStairs();
    }
}

Both two methods are on the class

[CustomEditor(typeof(BoxStairs))]
public sealed class BoxStairsEditor : Editor

It actually removes the two components but, once the BoxCollider has been removed the following error appears:

MissingReferenceException: The object of type 'BoxCollider' has been destroyed but you are still trying to access it.

I tried to locate where the error is occurring by looking at the trace:

Your script should either check if it is null or you should not destroy the object.
UnityEditor.Editor.IsEnabled () (at C:/buildslave/unity/build/Editor/Mono/Inspector/Editor.cs:590)
UnityEditor.InspectorWindow.DrawEditor (UnityEditor.Editor editor, Int32 editorIndex, Boolean rebuildOptimizedGUIBlock, System.Boolean& showImportedObjectBarNext, UnityEngine.Rect& importedObjectBarRect) (at C:/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:1154)
UnityEditor.InspectorWindow.DrawEditors (UnityEditor.Editor[] editors) (at C:/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:1030)
UnityEditor.InspectorWindow.OnGUI () (at C:/buildslave/unity/build/Editor/Mono/Inspector/InspectorWindow.cs:352)
System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:222)

But none of my scripts appears on it.

I've been looking on the code where I'm referencing the BoxCollider and the only place is where it is created, when the stairs are created which is triggered once a change has happened on the inspector.

It is in the class:

[ExecuteInEditMode]
[SelectionBase]
public sealed class BoxStairs : MonoBehaviour

This is the code:

    /*
     * This method creates a disabled BoxCollider which marks the volume defined by
     * StairsWidth, StairsHeight, StairsDepth.
     */
    private void AddSelectionBox ()
    {
        BoxCollider VolumeBox = Root.GetComponent<BoxCollider>();

        if (VolumeBox == null)
        {
            VolumeBox = Root.AddComponent<BoxCollider>();
        }

        if (Pivot == PivotType.Downstairs)
        {
            VolumeBox.center = new Vector3(0, StairsHeight * 0.5f, StairsDepth * 0.5f);
        }
        else
        {
            VolumeBox.center = new Vector3(0, -StairsHeight * 0.5f, -StairsDepth * 0.5f);
        }

        VolumeBox.size = new Vector3(StairsWidth, StairsHeight, StairsDepth);

        VolumeBox.enabled = false;
    }

I've tried to comment this method's body to allow removing the BoxCollider without this "reference" and the error still appears, so, I guess this method is not the problem.

Also, I've removed the BoxCollider manually, without clicking the Finalize button to trigger this code, via right click on the component on the inspector "Remove Component" option and the error not appears and after that, click over finalize stairs and no problem shows up.

As @JoeBlow mentioned in the comments I've checked that the FinalizeStairs method is called just once.

Also I've checked that the process of creation with the call to AddSelectionBox method it is not happening on the moment of clicking finalize button.

So, please I need a hand on this. This is the link to the development branch https://github.com/JAFS6/BoxStairsTool/tree/feature/BoxStairsTool, here you will find that the above mentioned method FinalizeStairs has the code which removes the BoxStairs script only and on that moment it throws no errors.

Any idea or advice on this will be very helpful. Thanks in advance.

Edit: a Minimal, Complete, and Verifiable example:

Asset/BoxStairs.cs

using UnityEngine;
using System.Collections.Generic;

namespace BoxStairsTool
{
    [ExecuteInEditMode]
    [SelectionBase]
    public sealed class BoxStairs : MonoBehaviour
    {
        private GameObject Root;

        private void Start ()
        {
            Root = this.gameObject;
            this.AddSelectionBox();
        }

        private void AddSelectionBox()
        {
            BoxCollider VolumeBox = Root.GetComponent<BoxCollider>();

            if (VolumeBox == null)
            {
                VolumeBox = Root.AddComponent<BoxCollider>();
            }

            VolumeBox.size = new Vector3(20, 20, 20);

            VolumeBox.enabled = false;
        }

    }
}

Asset\Editor\BoxStairsEditor.cs

using UnityEngine;
using UnityEditor;

namespace BoxStairsTool
{
    [CustomEditor(typeof(BoxStairs))]
    public sealed class BoxStairsEditor : Editor
    {
        private const string DefaultName = "BoxStairs";

        [MenuItem("GameObject/3D Object/BoxStairs")]
        private static void CreateBoxStairsGO ()
        {
            GameObject BoxStairs = new GameObject(DefaultName);
            BoxStairs.AddComponent<BoxStairs>();

            if (Selection.transforms.Length == 1)
            {
                BoxStairs.transform.SetParent(Selection.transforms[0]);
                BoxStairs.transform.localPosition = new Vector3(0,0,0);
            }

            Selection.activeGameObject = BoxStairs;
            Undo.RegisterCreatedObjectUndo(BoxStairs, "Create BoxStairs");
        }

        public override void OnInspectorGUI ()
        {
            if (GUILayout.Button("Finalize stairs"))
            {
                FinalizeStairs();
            }
        }

        private void FinalizeStairs ()
        {
            Undo.SetCurrentGroupName("Finalize stairs");
            BoxStairs script = (BoxStairs)target;
            GameObject go = script.gameObject;
            BoxCollider bc = go.GetComponent<BoxCollider>();

            if (bc != null)
            {
                Undo.DestroyObjectImmediate(bc);
            }
            Undo.DestroyObjectImmediate(target);
        }
    }
}

Solution

  • Analysis

    I'm a programmer, so I just debug to find the problem (in my mind :D).

    MissingReferenceException: The object of type 'BoxCollider' has been destroyed but you are still trying to access it.
    Your script should either check if it is null or you should not destroy the object.
    UnityEditor.Editor.IsEnabled () (at C:/buildslave/unity/build/Editor/Mono/Inspector/Editor.cs:590)

    A MissingReferenceException occurs when the code trys to access a Unity3D.Object after it has been destroyed.

    Let's look into the decompiled code of UnityEditor.Editor.IsEnabled().

    internal virtual bool IsEnabled()
    {
        UnityEngine.Object[] targets = this.targets;
        for (int i = 0; i < targets.Length; i++)
        {
            UnityEngine.Object @object = targets[i];
            if ((@object.hideFlags & HideFlags.NotEditable) != HideFlags.None)
            {
                return false;
            }
            if (EditorUtility.IsPersistent(@object) && !AssetDatabase.IsOpenForEdit(@object))
            {
                return false;
            }
        }
        return true;
    }
    

    We won't be able to know which line is the specific line 590. But, we can tell where can a MissingReferenceException happen:

    //    ↓↓↓↓↓↓
    if ((@object.hideFlags & HideFlags.NotEditable) != HideFlags.None)
    

    @object is assigned from Editor.targets which is an array of all the object being inspected. There should be only one target object in this array in your case - the BoxCollider component.

    In conclusion, the inspector failed to access the target object (I mean targets[0]) after you calls Undo.DestroyObjectImmediate on the BoxCollider component.

    If you dig into the decompiled code of the inspector(UnityEditor.InspectorWindow), you will see that the overridden OnInspectorGUI function is called per Editor in order in UnityEditor.InspectorWindow.DrawEditors, including the internal editor of BoxCollider and your custom editor BoxStairsEditor of BoxStairs .

    Solutions

    1. Do not destory a component that is being shown by the Inspector in OnInspectorGUI.
      Maybe you can add a delegate instance to EditorApplication.update to do that instead. In this way, the deleting operation will not break the editor/inspector GUI of BoxCollider.
    2. Move the created component BoxCollider upper than your BoxStairs component before you destroy it. This may work but I'm not sure if other internal editor will access the BoxCollider or not. This solution not works when using UnityEditorInternal.ComponentUtility.MoveComponentUp. But, if the user manually move up the BoxCollider component, it works without any code changes.

    Code of solution

    After using solution 1, the NRE is gone on Unity3D 5.4 on Win10.

    using UnityEngine;
    using UnityEditor;
    
    namespace BoxStairsTool
    {
        [CustomEditor(typeof(BoxStairs))]
        public sealed class BoxStairsEditor : Editor
        {
            private const string DefaultName = "BoxStairs";
    
            [MenuItem("GameObject/3D Object/BoxStairs")]
            private static void CreateBoxStairsGO ()
            {
                GameObject BoxStairs = new GameObject(DefaultName);
                BoxStairs.AddComponent<BoxStairs>();
    
                if (Selection.transforms.Length == 1)
                {
                    BoxStairs.transform.SetParent(Selection.transforms[0]);
                    BoxStairs.transform.localPosition = new Vector3(0,0,0);
                }
    
                Selection.activeGameObject = BoxStairs;
                Undo.RegisterCreatedObjectUndo(BoxStairs, "Create BoxStairs");
            }
    
            private void OnEnable ()
            {
                EditorApplication.update -= Update;
                EditorApplication.update += Update;
            }
    
            public override void OnInspectorGUI ()
            {
                if (GUILayout.Button("Finalize stairs"))
                {
                    needFinalize = true;
                }
            }
    
            private void FinalizeStairs ()
            {
                Undo.SetCurrentGroupName("Finalize stairs");
                BoxStairs script = (BoxStairs)target;
                GameObject go = script.gameObject;
                BoxCollider bc = go.GetComponent<BoxCollider>();
    
                if (bc != null)
                {
                    Undo.DestroyObjectImmediate(bc);
                }
                Undo.DestroyObjectImmediate(target);
            }
    
            bool needFinalize;
            void Update()
            {
                if(needFinalize)
                {
                    FinalizeStairs();
                    needFinalize = false;
                    EditorApplication.update -= Update;
                }
            }
        }
    }