Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

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

Discussion in 'Physics' started by RakNet, Apr 6, 2019.

  1. RakNet

    RakNet

    Joined:
    Oct 9, 2013
    Posts:
    133
    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()

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. // OnTriggerExit is not called if the triggering object is destroyed, set inactive, or if the collider is disabled. This script fixes that
    7. //
    8. // Usage: Wherever you read OnTriggerEnter() and want to consistently get OnTriggerExit
    9. // In OnTriggerEnter() call ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);
    10. // In OnTriggerExit call ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);
    11. //
    12. // Algorithm: Each ReliableOnTriggerExit is associated with a collider, which is added in OnTriggerEnter via NotifyTriggerEnter
    13. // Each ReliableOnTriggerExit keeps track of OnTriggerEnter calls
    14. // If ReliableOnTriggerExit is disabled or the collider is not enabled, call all pending OnTriggerExit calls
    15. public class ReliableOnTriggerExit : MonoBehaviour
    16. {
    17.     public delegate void _OnTriggerExit(Collider c);
    18.  
    19.     Collider thisCollider;
    20.     bool ignoreNotifyTriggerExit = false;
    21.  
    22.     // Target callback
    23.     Dictionary<GameObject, _OnTriggerExit> waitingForOnTriggerExit = new Dictionary<GameObject, _OnTriggerExit>();
    24.  
    25.     public static void NotifyTriggerEnter(Collider c, GameObject caller, _OnTriggerExit onTriggerExit)
    26.     {
    27.         ReliableOnTriggerExit thisComponent = null;
    28.         ReliableOnTriggerExit[] ftncs = c.gameObject.GetComponents<ReliableOnTriggerExit>();
    29.         foreach (ReliableOnTriggerExit ftnc in ftncs)
    30.         {
    31.             if (ftnc.thisCollider == c)
    32.             {
    33.                 thisComponent = ftnc;
    34.                 break;
    35.             }
    36.         }
    37.         if (thisComponent == null)
    38.         {
    39.             thisComponent = c.gameObject.AddComponent<ReliableOnTriggerExit>();
    40.             thisComponent.thisCollider = c;
    41.         }
    42.         // 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
    43.         // In addition, force a call to NotifyTriggerExit so the number of calls to OnTriggerEnter and OnTriggerExit match up
    44.         if (thisComponent.waitingForOnTriggerExit.ContainsKey(caller) == false)
    45.         {
    46.             thisComponent.waitingForOnTriggerExit.Add(caller, onTriggerExit);
    47.             thisComponent.enabled = true;
    48.         }
    49.         else
    50.         {
    51.             thisComponent.ignoreNotifyTriggerExit = true;
    52.             thisComponent.waitingForOnTriggerExit[caller].Invoke(c);
    53.             thisComponent.ignoreNotifyTriggerExit = false;
    54.         }
    55.     }
    56.  
    57.     public static void NotifyTriggerExit(Collider c, GameObject caller)
    58.     {
    59.         if (c == null)
    60.             return;
    61.  
    62.         ReliableOnTriggerExit thisComponent = null;
    63.         ReliableOnTriggerExit[] ftncs = c.gameObject.GetComponents<ReliableOnTriggerExit>();
    64.         foreach (ReliableOnTriggerExit ftnc in ftncs)
    65.         {
    66.             if (ftnc.thisCollider == c)
    67.             {
    68.                 thisComponent = ftnc;
    69.                 break;
    70.             }
    71.         }
    72.         if (thisComponent != null && thisComponent.ignoreNotifyTriggerExit == false)
    73.         {
    74.             thisComponent.waitingForOnTriggerExit.Remove(caller);
    75.             if (thisComponent.waitingForOnTriggerExit.Count == 0)
    76.             {
    77.                 thisComponent.enabled = false;
    78.             }
    79.         }
    80.     }
    81.     private void OnDisable()
    82.     {
    83.         if (gameObject.activeInHierarchy == false)
    84.             CallCallbacks();
    85.     }
    86.     private void Update()
    87.     {
    88.         if (thisCollider == null)
    89.         {
    90.             // Will GetOnTriggerExit with null, but is better than no call at all
    91.             CallCallbacks();
    92.  
    93.             Component.Destroy(this);
    94.         }
    95.         else if (thisCollider.enabled == false)
    96.         {
    97.             CallCallbacks();
    98.         }
    99.     }
    100.     void CallCallbacks()
    101.     {
    102.         ignoreNotifyTriggerExit = true;
    103.         foreach (var v in waitingForOnTriggerExit)
    104.         {
    105.             if (v.Key == null)
    106.             {
    107.                 continue;
    108.             }
    109.  
    110.             v.Value.Invoke(thisCollider);
    111.         }
    112.         ignoreNotifyTriggerExit = false;
    113.         waitingForOnTriggerExit.Clear();
    114.         enabled = false;
    115.     }
    116. }
    117.  
    118.  
     

    Attached Files:

    • 1.jpg
      1.jpg
      File size:
      115.1 KB
      Views:
      270
    • 2.jpg
      2.jpg
      File size:
      123.2 KB
      Views:
      232
    Last edited: Apr 6, 2019
    FeastSC2 likes this.
  2. RakNet

    RakNet

    Joined:
    Oct 9, 2013
    Posts:
    133
    Here is a usage example:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class LogTriggers : MonoBehaviour
    6. {
    7.     private void OnTriggerEnter(Collider other)
    8.     {
    9.         ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);
    10.  
    11.         Debug.Log("OnTriggerEnter");
    12.     }
    13.  
    14.     private void OnTriggerExit(Collider other)
    15.     {
    16.         ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);
    17.  
    18.         Debug.Log("OnTriggerExit");
    19.     }
    20. }
    21.  
     
  3. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    601
    Really cool script man, thanks it works! :)
     
  4. Bill-Sansky

    Bill-Sansky

    Joined:
    Oct 2, 2016
    Posts:
    60
    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!
     
  5. Armegalo

    Armegalo

    Joined:
    May 14, 2018
    Posts:
    1
    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...


    Code (CSharp):
    1. MonoBehaviour[] comps = gameObject.GetComponentsInChildren<MonoBehaviour>();
    2. foreach (MonoBehaviour c in comps){
    3.    if (!(c.GetType().Name =="ReliableOnTriggerExit2D")){
    4.       c.enabled = false;
    5.    } else {
    6.       Debug.LogError("not disabling a ReliableTrigger");
    7.    }
    8. }
    As you can see I also made a 2D version by just adding 2D in the appropriate places.
     
  6. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    1,959
  7. Bill-Sansky

    Bill-Sansky

    Joined:
    Oct 2, 2016
    Posts:
    60
  8. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    1,959
    This is a function of Unity not Physx or Box2D. I implemented the function for 2D, 3D doesn't have it implemented.
     
    Bill-Sansky likes this.
  9. Bill-Sansky

    Bill-Sansky

    Joined:
    Oct 2, 2016
    Posts:
    60
    Got it. It would be super useful to also have that available for 3D :)