Fix: OnTriggerExit will now be called for disabled GameObjects / Colliders

By design, OnTriggerExit isn’t called if the object that was responsible for OnTriggerEnter is disabled or destroyed. The result is you can get OnTriggerEnter without ever getting OnTriggerExit, and can get OnTriggerEnter multiple times for the same object, for example if the object that caused the trigger was deactivated and then activated again

This is bad design and breaks some optimization techniques such as adding objects in a zone to a HashSet when they enter the zone and removing them when they leave.

Others have suggested hacks, such as moving the object and waiting one frame before destroying it.

Here is a fix that is robust, doesn’t require hacks, and was designed to be fast and easy to use.

It requires you to call
ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);
and
ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);
every time you use OnTriggerEnter() and want to be sure you’ll get OnTriggerExit()

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// OnTriggerExit is not called if the triggering object is destroyed, set inactive, or if the collider is disabled. This script fixes that
//
// Usage: Wherever you read OnTriggerEnter() and want to consistently get OnTriggerExit
// In OnTriggerEnter() call ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);
// In OnTriggerExit call ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);
//
// Algorithm: Each ReliableOnTriggerExit is associated with a collider, which is added in OnTriggerEnter via NotifyTriggerEnter
// Each ReliableOnTriggerExit keeps track of OnTriggerEnter calls
// If ReliableOnTriggerExit is disabled or the collider is not enabled, call all pending OnTriggerExit calls
public class ReliableOnTriggerExit : MonoBehaviour
{
    public delegate void _OnTriggerExit(Collider c);

    Collider thisCollider;
    bool ignoreNotifyTriggerExit = false;

    // Target callback
    Dictionary<GameObject, _OnTriggerExit> waitingForOnTriggerExit = new Dictionary<GameObject, _OnTriggerExit>();

    public static void NotifyTriggerEnter(Collider c, GameObject caller, _OnTriggerExit onTriggerExit)
    {
        ReliableOnTriggerExit thisComponent = null;
        ReliableOnTriggerExit[] ftncs = c.gameObject.GetComponents<ReliableOnTriggerExit>();
        foreach (ReliableOnTriggerExit ftnc in ftncs)
        {
            if (ftnc.thisCollider == c)
            {
                thisComponent = ftnc;
                break;
            }
        }
        if (thisComponent == null)
        {
            thisComponent = c.gameObject.AddComponent<ReliableOnTriggerExit>();
            thisComponent.thisCollider = c;
        }
        // Unity bug? (!!!!): Removing a Rigidbody while the collider is in contact will call OnTriggerEnter twice, so I need to check to make sure it isn't in the list twice
        // In addition, force a call to NotifyTriggerExit so the number of calls to OnTriggerEnter and OnTriggerExit match up
        if (thisComponent.waitingForOnTriggerExit.ContainsKey(caller) == false)
        {
            thisComponent.waitingForOnTriggerExit.Add(caller, onTriggerExit);
            thisComponent.enabled = true;
        }
        else
        {
            thisComponent.ignoreNotifyTriggerExit = true;
            thisComponent.waitingForOnTriggerExit[caller].Invoke(c);
            thisComponent.ignoreNotifyTriggerExit = false;
        }
    }

    public static void NotifyTriggerExit(Collider c, GameObject caller)
    {
        if (c == null)
            return;

        ReliableOnTriggerExit thisComponent = null;
        ReliableOnTriggerExit[] ftncs = c.gameObject.GetComponents<ReliableOnTriggerExit>();
        foreach (ReliableOnTriggerExit ftnc in ftncs)
        {
            if (ftnc.thisCollider == c)
            {
                thisComponent = ftnc;
                break;
            }
        }
        if (thisComponent != null && thisComponent.ignoreNotifyTriggerExit == false)
        {
            thisComponent.waitingForOnTriggerExit.Remove(caller);
            if (thisComponent.waitingForOnTriggerExit.Count == 0)
            {
                thisComponent.enabled = false;
            }
        }
    }
    private void OnDisable()
    {
        if (gameObject.activeInHierarchy == false)
            CallCallbacks();
    }
    private void Update()
    {
        if (thisCollider == null)
        {
            // Will GetOnTriggerExit with null, but is better than no call at all
            CallCallbacks();

            Component.Destroy(this);
        }
        else if (thisCollider.enabled == false)
        {
            CallCallbacks();
        }
    }
    void CallCallbacks()
    {
        ignoreNotifyTriggerExit = true;
        foreach (var v in waitingForOnTriggerExit)
        {
            if (v.Key == null)
            {
                continue;
            }

            v.Value.Invoke(thisCollider);
        }
        ignoreNotifyTriggerExit = false;
        waitingForOnTriggerExit.Clear();
        enabled = false;
    }
}


23 Likes

Here is a usage example:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LogTriggers : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);

        Debug.Log("OnTriggerEnter");
    }

    private void OnTriggerExit(Collider other)
    {
        ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);

        Debug.Log("OnTriggerExit");
    }
}
6 Likes

Really cool script man, thanks it works! :slight_smile:

thanks for the script!
You say that “by design” it does not trigger when objects get disabled, do you also know what’s the reasoning for that from Unity? it sounds really inconsistent!

3 Likes

Works great as long as your not disabling ALL scripts on the game object as well as its colliders.
I made a quick fix tho…

MonoBehaviour[] comps = gameObject.GetComponentsInChildren<MonoBehaviour>();
foreach (MonoBehaviour c in comps){
   if (!(c.GetType().Name =="ReliableOnTriggerExit2D")){
      c.enabled = false;
   } else {
      Debug.LogError("not disabling a ReliableTrigger");
   }
}

As you can see I also made a 2D version by just adding 2D in the appropriate places.

Are you therefore saying that the following property which enables that doesn’t work in 2D?

https://docs.unity3d.com/ScriptReference/Physics2D-callbacksOnDisable.html

Does that mean that the absence of such an option in 3D is linked to Physix?

This is a function of Unity not Physx or Box2D. I implemented the function for 2D, 3D doesn’t have it implemented.

1 Like

Got it. It would be super useful to also have that available for 3D :slight_smile:

5 Likes

Is there a proper feature request for this behavior in 3D?
I don’t think the comment by @Bill-Sansky will necessarily be found to get this feature properly implemented in Unity 3D.

1 Like

It would be awesome to have this in 3D aswell indeed. Can @MelvMay contact the 3D team to implement this too?

@yant is the person to speak to.

1 Like

Yes, thanks. It used to be a direct consequence of how PhysX 2.8 worked, and I was intending to solve that in Unity 5.0 – and had a version shipped with it, however it turned out to be a can of worms unfortunately (I didn’t implement it the way Melvyn did, with a flag, – so many games became broken; also it turned out there was no way of differentiating a Collider destruction due to scene unload from a Collider destruction due to normal destruction - so we would start sending OnTriggerExit when switching scenes, and also right before exiting the app which was quite unexpected). That said, we should totally review this once the load allows us to.

9 Likes

Absolutely agree. Due to how Unity works internally, objects can be destroyed for various reasons and in many cases, at a point where you can’t send a message (perform a callback) and doing so has terrible consequences. Unfortunately from the physics POV, it just sees the physics component being destroyed and cannot differentiate this. It’s why I opted for only doing the Exit-On-Disable because this is always at a “safe” point but it’s still less than ideal. I’d like to see Exit for any reason but it’s just not possible given the internal architecture for components, scene handling etc.

Unfortunately right now, it’s like a PC; if you hit shutdown (disable) then you get “exit” but if you pull out the plug (unload the scene) you won’t. Okay, perhaps not the best analogy :slight_smile:

4 Likes

@RakNet thanks for this script! It works beautifully. Can I check if it is constantly doing the checks, will the performance be reduced with many elements?

Kind of off topic but also a little related.

Swapping rigidbody from kinematic to non kinematic will trigger an OnTriggerExit and OnTriggerEnter. This was the case in Unity 3.x, then in Unity 4 and 5 it changed to no longer trigger when toggling is kinematic. Then in 2017 or maybe 2018 it came back.
It’s quite annoying upgrading a project when something like this changes, can you make this behaviour a flag too so that future pain can be mitigated if it switches again?

5 Likes

It only runs in the OnTriggerEnter and OnTriggerExit so should not significantly reduce performance.

1 Like

@yant any news regarding this feature being implemented for 3D?

Same issue here, but I simplified the first solution to just use a HashSet and remember who started the collision, checking if the object is deactivated before the OnCollisionExit gets called. I do add the object to the list in the OnCollisionEnter, remove in the OnCollisionExit and test the list in the Update method. Surely that’s not a generic solution that works in every case, and may have many drawbacks, but it worked for now.

@FeastSC2 why won’t it work with 3D? I’m using it with 3D game objects using Photon Pun and it works fine. @RakNet really made a solid script!