Search Unity

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:
    107
    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:
      167
    • 2.jpg
      2.jpg
      File size:
      123.2 KB
      Views:
      118
    Last edited: Apr 6, 2019
    FeastSC2 likes this.
  2. RakNet

    RakNet

    Joined:
    Oct 9, 2013
    Posts:
    107
    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:
    596
    Really cool script man, thanks it works! :)