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:
    315
    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:
      1,497
    • 2.jpg
      2.jpg
      File size:
      123.2 KB
      Views:
      1,395
    Last edited: Apr 6, 2019
  2. RakNet

    RakNet

    Joined:
    Oct 9, 2013
    Posts:
    315
    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:
    978
    Really cool script man, thanks it works! :)
     
  4. Shrubokrant

    Shrubokrant

    Joined:
    Oct 2, 2016
    Posts:
    80
    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!
     
    futurlab_xbox, M_R_M and EZaca like this.
  5. Armegalo

    Armegalo

    Joined:
    May 14, 2018
    Posts:
    30
    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:
    11,492
  7. Shrubokrant

    Shrubokrant

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

    MelvMay

    Unity Technologies

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

    Shrubokrant

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

    ScallyGames

    Joined:
    Sep 10, 2012
    Posts:
    47
    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.
     
    Last edited: Oct 18, 2020
    EZaca likes this.
  11. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    It would be awesome to have this in 3D aswell indeed. Can @MelvMay contact the 3D team to implement this too?
     
  12. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,492
    @yant is the person to speak to.
     
    FeastSC2 likes this.
  13. yant

    yant

    Unity Technologies

    Joined:
    Jul 24, 2013
    Posts:
    596
    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.
     
  14. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,492
    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 :)
     
  15. UDN_5c806b49-d8a0-4f67-a296-c12c91aa7396

    UDN_5c806b49-d8a0-4f67-a296-c12c91aa7396

    Joined:
    Jan 9, 2017
    Posts:
    152
    @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?
     
  16. Antony-Blackett

    Antony-Blackett

    Joined:
    Feb 15, 2011
    Posts:
    1,778
    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?
     
  17. RakNet

    RakNet

    Joined:
    Oct 9, 2013
    Posts:
    315
    It only runs in the OnTriggerEnter and OnTriggerExit so should not significantly reduce performance.
     
  18. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    @yant any news regarding this feature being implemented for 3D?
     
  19. EZaca

    EZaca

    Joined:
    Dec 9, 2017
    Posts:
    32
    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.
     
  20. UDN_5c806b49-d8a0-4f67-a296-c12c91aa7396

    UDN_5c806b49-d8a0-4f67-a296-c12c91aa7396

    Joined:
    Jan 9, 2017
    Posts:
    152
    @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!
     
  21. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    It works fine
     
  22. UDN_5c806b49-d8a0-4f67-a296-c12c91aa7396

    UDN_5c806b49-d8a0-4f67-a296-c12c91aa7396

    Joined:
    Jan 9, 2017
    Posts:
    152
    Thank @RakNet !
     
  23. Qriva

    Qriva

    Joined:
    Jun 30, 2019
    Posts:
    1,314
    There is one additional workaround for OnTriggerExit in very specific case:
    If there is rigidbody component on the object where you want to disable collision, instead of disabling the Collider, you can do
    Code (CSharp):
    1. Rigidbody.detectCollisions = false
    . In such a case OnTriggerExit event will be called correctly.

    The downside is that rigidbody will continue simulation, but this can be fixed by setting kinematic to true / enabling constraints or you can just disable or destroy it after next FixedUpdate.
     
  24. mlittmancs

    mlittmancs

    Joined:
    May 30, 2021
    Posts:
    1
    Ah, bummer. There doesn't seem to be an analogous thing in 2D.
     
  25. ss0cratess

    ss0cratess

    Joined:
    Oct 23, 2018
    Posts:
    4
    Hello, this is indeed a solid script, only issue I'm facing is that I'm using it in multiple colliders throughout my game's map and it seems that they deactivate when the player exits them (when that's not what I need it to do).
    I'm using it for a 3d project on Unity 2020.3.25f1. Thank you!
     
  26. homer_3

    homer_3

    Joined:
    Jun 19, 2011
    Posts:
    111
    I'm a bit confused on how this is working for people since OP's code seems to have a bug. In order for me to get the OnTriggerExit notifications, I had to make a small modification. The caller is who should own the Reilable script. I fixed it here. But thanks for the script OP, it's very useful.

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

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    I've been looking into this some more because now I need it for my project.

    If you put it on the caller, you won't receive a notification when the target is being disabled.
    But maybe you're only disabling the caller so this currently works for you, however it's not fool proof.

    That being said you're right that if the caller is disabled there currently is no notification for that in what the OP's script has provided.

    I suggest you use the OP's script and if you want to get a callback when the caller is disabled (which makes a lot of sense), you should use the idea in the script below.
    The idea is to call OnTriggerExit on all the triggers you entered in the OnDisable() method.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using System.Text;
    4. using UnityEngine;
    5.  
    6. /// <summary>
    7. /// https://forum.unity.com/threads/fix-ontriggerexit-will-now-be-called-for-disabled-gameobjects-colliders.657205/
    8. /// You will still get the remaining issues:
    9. ///
    10. ///On the caller:
    11. ///- With 1 trigger collider: If you disable the trigger collider instead of the gameObject, you won't receive the expected notifications.
    12. ///- With multiple trigger colliders: You won't receive the expected notifications.
    13. ///
    14. ///On the target:
    15. ///- No issues. (You can have multiple colliders and you can disable individual colliders).
    16. ///
    17. ///Conclusions:
    18. ///You should only have 1 collider on the caller. You cannot disable the collider on the collider but you can disable its gameObject.
    19. /// </summary>
    20. public class ReliableTrigger : MonoBehaviour
    21. {
    22.     private bool IsDebugMode = true;
    23.     private const bool DoErrorCheck = true;
    24.     private int TriggerEnteredCount => EnteredTriggers.Count;
    25.     protected readonly List<Collider> EnteredTriggers = new List<Collider>();
    26.     protected Collider[] MyColliders;
    27.  
    28.     protected virtual void Awake()
    29.     {
    30.         if (DoErrorCheck)
    31.         {
    32.             MyColliders = GetComponentsInChildren<Collider>();
    33.             ErrorCheck_IsReliableTriggerProperlySetup();
    34.             StartCoroutine(ErrorCheck_IsAreComponentsDisabled());
    35.         }
    36.     }
    37.  
    38.     private WaitForSeconds ErrorCheckRefreshRate = new WaitForSeconds(2f);
    39.     private IEnumerator ErrorCheck_IsAreComponentsDisabled()
    40.     {
    41.         while (true)
    42.         {
    43.             if (gameObject.activeInHierarchy == false) yield return ErrorCheckRefreshRate;
    44.  
    45.             if (this.enabled == false)
    46.             {
    47.                 Debug.LogError($"It's now allowed to disable a {nameof(ReliableTrigger)} component on: {GetFullPathName(transform)}. You can only disable its gameObject", this);
    48.             }
    49.          
    50.             foreach (var myCollider in MyColliders)
    51.             {
    52.                 if (myCollider.enabled == false && myCollider.isTrigger)
    53.                 {
    54.                     Debug.LogError($"It's not allowed to disable a trigger on a ReliableTrigger: {GetFullPathName(myCollider.transform)}", this);
    55.                 }
    56.             }
    57.  
    58.             yield return ErrorCheckRefreshRate;
    59.         }
    60.         // ReSharper disable once IteratorNeverReturns
    61.     }
    62.  
    63.     private string GetFullPathName(Transform t)
    64.     {
    65.         var bldr = new StringBuilder();
    66.         bldr.Append(t.name);
    67.         t = t.parent;
    68.         while (t != null)
    69.         {
    70.             bldr.Insert(0, @"\");
    71.             bldr.Insert(0, t.name);
    72.             t = t.parent;
    73.         }
    74.         return bldr.ToString();
    75.     }
    76.  
    77.     private void ErrorCheck_IsReliableTriggerProperlySetup()
    78.     {
    79.         var colliders = MyColliders;
    80.  
    81.         int triggerColliders = 0;
    82.         for (int i = 0; i < colliders.Length; i++)
    83.         {
    84.             if (colliders[i].isTrigger) triggerColliders++;
    85.         }
    86.  
    87.         if (triggerColliders >= 2)
    88.         {
    89.             Debug.LogError($"It's not allowed to have more than 1 trigger collider on a ReliableTrigger. {GetFullPathName(transform)}", this);
    90.         }
    91.     }
    92.  
    93.     protected virtual void OnTriggerEnter(Collider other)
    94.     {
    95.         ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);
    96.      
    97.         EnteredTriggers.Add(other);
    98.      
    99.         if (IsDebugMode)
    100.             Debug.Log($"Entered: {TriggerEnteredCount}. Collider: {other.name}");
    101.     }
    102.  
    103.     protected virtual void OnTriggerExit(Collider other)
    104.     {
    105.         ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);
    106.      
    107.         EnteredTriggers.Remove(other);
    108.      
    109.         if (IsDebugMode)
    110.             Debug.Log($"Exit: {TriggerEnteredCount}. Collider: {other.name}");
    111.     }
    112.  
    113.     protected virtual void OnDisable()
    114.     {
    115.         for (var index = EnteredTriggers.Count - 1; index >= 0; index--)
    116.         {
    117.             var et = EnteredTriggers[index];
    118.             OnTriggerExit(et);
    119.         }
    120.     }
    121. }
     
    Last edited: Apr 14, 2022
  28. FeastSC2

    FeastSC2

    Joined:
    Sep 30, 2016
    Posts:
    978
    By using the script I provided in combination with the OP's script.
    You will still get the remaining issues:

    On the caller:
    - With 1 trigger collider: If you disable the trigger collider instead of the gameObject, you won't receive the expected notifications.
    - With multiple trigger colliders: You won't receive the expected notifications.

    On the target:
    - No issues. (You can have multiple colliders and you can disable individual colliders).

    Conclusions:
    You should only have 1 collider on the caller. You cannot disable the collider on the collider but you can disable its gameObject.


    These issues could be fixed if during OnTriggerEnter we also received the collider that triggered the detection. Not sure how to access that however...

    Code (CSharp):
    1. private void OnTriggerEnter(Collider detector, Collider other)
    2. {
    3. }
     
    Last edited: Apr 14, 2022
  29. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    542
    No official "fix" from Unity yet?
    Kinda annoying that disable / destory of objects always requires extra workarounds because Unity methods are not called as expected, there are similar cases in other Unity code (for example PointerExit in the UI system)
     
    dende2019 and Vincent454 like this.
  30. PandaArcade

    PandaArcade

    Joined:
    Jan 2, 2017
    Posts:
    130
    @yant @MelvMay This is a problem we come up against in every project :( Whatever hacks you would have to do to fix this in the backend are likely far nicer than whatever workarounds we're coming up with. Please fix :D

    While I'm here, why doesn't the Collision object passed into OnCollisionExit contain a reference to this Collider? It only has reference to the other collider. How am I supposed to know which of this RigidBodies colliders stopped colliding?
     
    Last edited: Jun 2, 2022
  31. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,492
    I don't need to "fix" 2D physics, this already works there as I mentioned above in 2019. I don't work on 3D physics though.

    Again, I can only reply regarding 2D. Collision2D does have a property for both collider/otherCollider/body/otherBody.
     
  32. AmazingRuss

    AmazingRuss

    Joined:
    May 25, 2008
    Posts:
    933
    The really FUN part is that it will fire... sometimes....

    Please fix this Unity! if it's not enabled or doesn't exist, Elvis has left the building.
     
    CYCLE-6 and Guitoon like this.
  33. Tehenauin

    Tehenauin

    Joined:
    Oct 18, 2018
    Posts:
    43
    I tried the script from @RakNet but for some reason, it doesnt work as expected. Is it supposed to also call OnTriggerExit on the object that gets disabled or only on the other?
     
  34. theCodeHermit

    theCodeHermit

    Joined:
    Sep 20, 2018
    Posts:
    39
    @RakNet Thank you ! It worked perfectly !
    I spend 2 days working out some hack solutions so I consistently get the trigger exit. Now I can delete all that spaghetti code.
     
  35. OAlexM

    OAlexM

    Joined:
    Feb 26, 2016
    Posts:
    6
    Hello! The problem from an architectural pov is that the physics exit methods are not being called if the objects getting disabled inside the collider.
    This is a general problem as many others already mentioned above. There is a nice method to detect such situations where objects getting disabled while being cached somewhere. Every class caching objects which need to recognise a disable or destroy situation to remove the object from their caches should put on the fly a OnDisablePropagatorN or OnDestroyPropagatorN component on it and subscribe for the regarded
    case. (generally use Disable for pooled objects and Destroy for instantiated and destroyed objects). In case an object gets destroyed the caching class gets informed and can remove the object from its caches.
    A OnDisablePropagatorN could look like this:
    Code (CSharp):
    1.     public class OnDisablePropagatorN : MonoBehaviour
    2.     {
    3.         public event EventHandler Disabled;
    4.  
    5.         private void OnDisable()
    6.         {
    7.             Disabled?.Invoke(this, EventArgs.Empty);
    8.         }
    9.     }
    10.  
    After putting this on an objects subscribe to the public Disabled event handler.
    In case the object gets removed from the cache don't forget to unsubscribe from the event.
     
  36. Vorrin

    Vorrin

    Joined:
    Jan 14, 2013
    Posts:
    10
    I am facing an issue with this, using Physics2D, with Callbacks On Disable on.

    I have a monobehaviour on a parent object, detecting OnTriggerEnter/Exit2D, and a trigger collider on a child object. When I turn said child ( on and off, whilst colliding with another non trigger collider) it only fires OnTriggerEnter2D and OnTriggerExit2D once, and never again. It works as expected if I turn the parent object on and off (meaning enter and exit get called repeatedly).

    This very much doesn't feel like expected behavior to me, is it supposed to be?
    And if so, would somebody have a succinct workaround for this specific case?
     
    Last edited: Jun 12, 2023
  37. antsy1992

    antsy1992

    Joined:
    Jan 8, 2021
    Posts:
    8
    I have similar issue but for situation where I do not disabling an object, but on its collider I change sharedMesh. I generate mesh pretty frequently and apparently if you caused OnTriggerEnter(), then switched collider with "myCollider.sharedMesh = myNewGeneratedMesh" Unity looses that previous shared mesh and does not call OnTriggerExit() when objects stop colliding ("triggering"?) each other. Cant really wrap my head around how to apply OP's solution to my situation yet.
     
    Last edited: Feb 3, 2024