Home > Software engineering >  (Unity) Is there a way to get notified when referenced object instance gets deleted/missed? [With bu
(Unity) Is there a way to get notified when referenced object instance gets deleted/missed? [With bu

Time:12-18

So let's say we have MonoBehaviour class with a Transform property:

public class Class : MonoBehaviour
{
    public Transform Target; //An object is already referenced here.
}

Question is simple: is there a way to get an event or some sort of callback in case this object gets deleted (externally)? Unity will show this as "Missing":

enter image description here

Since it is just a transform, you cannot really use OnDestroy() here, and even if you could this is not a good option because of how many transform instances there are.

It should be noted that I need this to work at build, so in-editor only options are unfortunately not solving my problem.

CodePudding user response:

Here's one way to do it. I'd consider this a partial solution since there are some ways it's currently not ideal including not being able to edit prefabs while in play mode, leaving notify monobehaviours behind if AIs are destroyed, not respecting undo in the editor, and likely others. Still, this is probably worth sharing in its current state anyway.

Make a MonoBehaviour that will manage actions to call in the event the owning object is deleted. We're interested in making this work in the editor and at runtime, so I used the UnityEvent api for this.

using UnityEngine;
using UnityEditor;
using UnityEngine.Events;


[System.Serializable]
public class DestroyEvent : UnityEvent { };

[ExecuteInEditMode]
public class DestructionNotifier : MonoBehaviour
{
    public DestroyEvent OnDestroyed;

    private void Awake()
    {
        if (OnDestroyed == null)
        {
            OnDestroyed = new DestroyEvent();
        }
    }

    public void Register(UnityAction act)
    {
#if UNITY_EDITOR
        if (EditorApplication.isPlayingOrWillChangePlaymode)
        {
            // add non-persistent listener if in play mode
            OnDestroyed.AddListener(act);
        }
        else
        {
            // add persistent listener if in edit mode
            UnityEditor.Events.UnityEventTools.AddPersistentListener(
                    OnDestroyed, act);
            OnDestroyed.SetPersistentListenerState(
                    OnDestroyed.GetPersistentEventCount() - 1, 
                    UnityEventCallState.EditorAndRuntime);
        }
#else
        // add non-persistent listener
        OnDestroyed.AddListener(act);
#endif
    }

    public void Deregister(UnityAction call)
    {
        // remove or disable matching persistent actions
        for (int i = 0; i < OnDestroyed.GetPersistentEventCount(); i  )
        {
            if ((Object)call.Target == OnDestroyed.GetPersistentTarget(i))
            {
#if UNITY_EDITOR
                if (EditorApplication.isPlayingOrWillChangePlaymode)
                {
                    OnDestroyed.SetPersistentListenerState(i,
                            UnityEventCallState.Off);
                }
                else
                {
                    UnityEditor.Events.UnityEventTools
                            .RemovePersistentListener(OnDestroyed, i);
                }
#else
                OnDestroyed.SetPersistentListenerState(i, 
                        UnityEventCallState.Off);
#endif
            }
        }

        // remove matching non-persistent actions
        OnDestroyed.RemoveListener(call);

        // if in edit mode, remove self if no actions
#if UNITY_EDITOR
        RemoveEmptyEvents(); 
        if (!EditorApplication.isPlayingOrWillChangePlaymode
            && OnDestroyed.GetPersistentEventCount() == 0)
        {
            DestroyImmediate(this);
        }
#endif
}

    private void OnValidate()
    {
        RemoveEmptyEvents();
    }

    void RemoveEmptyEvents()
    {
#if UNITY_EDITOR
        for (int i = OnDestroyed.GetPersistentEventCount() - 1; i >= 0; i--)
        {
            if (OnDestroyed.GetPersistentTarget(i) == null)
            {
                UnityEditor.Events.UnityEventTools.RemovePersistentListener(
                        OnDestroyed, i);
            }
        }
#endif
    }

    void OnDestroy()
    {
        if (OnDestroyed != null)
        {
            OnDestroyed.Invoke();
        }
    }
}

Then, in your AI script, turn your transform into a DestructionNotifier property that subs/unsubs to the event as the target is changed. Use a custom inspector to allow the property to be edited in the inspector:

using UnityEngine;
using UnityEditor;

public class Class : MonoBehaviour
{
    void OnTargetDestroyed()
    {
        // Stuff to do when target is destroyed
        Debug.Log($"{gameObject.name}: target destroyed.");
    }

    [SerializeField, HideInInspector] DestructionNotifier targetNotifier;

    public Transform Target {
        get { return targetNotifier == null ? null : targetNotifier.transform; }
        set {
            if (targetNotifier != null)
            {
                targetNotifier.Deregister(OnTargetDestroyed);
            }

            if (value == null)
            {
                targetNotifier = null;
                return;
            }

            targetNotifier = value.GetComponent<DestructionNotifier>();

            if (targetNotifier == null)
            {
                targetNotifier = value.gameObject
                        .AddComponent<DestructionNotifier>();
            }
            targetNotifier.Register(OnTargetDestroyed);
        }
    }
}

#if UNITY_EDITOR
[CustomEditor(typeof(Class)), CanEditMultipleObjects]
public class AIInspector : Editor
{
    public override void OnInspectorGUI()
    {
        Class targetAI = (Class)target;
        EditorGUI.BeginChangeCheck();

        Transform newTarget = EditorGUILayout.ObjectField("Target", 
                targetAI.Target, typeof(Transform), true) as Transform;

        if (EditorGUI.EndChangeCheck())
        {
            foreach (Class curTarget in targets)
            {
                curTarget.Target = newTarget;
            }
        }
        DrawDefaultInspector();
    }
}
#endif
  • Related