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);
}
}
}
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
.
OnInspectorGUI
.BoxCollider
.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.UnityEditorInternal.ComponentUtility.MoveComponentUp
. But, if the user manually move up the BoxCollider
component, it works without any code changes.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;
}
}
}
}