It works fine
Thank @RakNet !
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 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.
Ah, bummer. There doesn’t seem to be an analogous thing in 2D.
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!
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.
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 = caller.GetComponents<ReliableOnTriggerExit>();
foreach (ReliableOnTriggerExit ftnc in ftncs)
{
if (ftnc.thisCollider == c)
{
thisComponent = ftnc;
break;
}
}
if (thisComponent == null)
{
thisComponent = caller.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 = caller.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;
}
}
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.
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
/// <summary>
/// https://discussions.unity.com/t/738523
/// 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.
/// </summary>
public class ReliableTrigger : MonoBehaviour
{
private bool IsDebugMode = true;
private const bool DoErrorCheck = true;
private int TriggerEnteredCount => EnteredTriggers.Count;
protected readonly List<Collider> EnteredTriggers = new List<Collider>();
protected Collider[] MyColliders;
protected virtual void Awake()
{
if (DoErrorCheck)
{
MyColliders = GetComponentsInChildren<Collider>();
ErrorCheck_IsReliableTriggerProperlySetup();
StartCoroutine(ErrorCheck_IsAreComponentsDisabled());
}
}
private WaitForSeconds ErrorCheckRefreshRate = new WaitForSeconds(2f);
private IEnumerator ErrorCheck_IsAreComponentsDisabled()
{
while (true)
{
if (gameObject.activeInHierarchy == false) yield return ErrorCheckRefreshRate;
if (this.enabled == false)
{
Debug.LogError($"It's now allowed to disable a {nameof(ReliableTrigger)} component on: {GetFullPathName(transform)}. You can only disable its gameObject", this);
}
foreach (var myCollider in MyColliders)
{
if (myCollider.enabled == false && myCollider.isTrigger)
{
Debug.LogError($"It's not allowed to disable a trigger on a ReliableTrigger: {GetFullPathName(myCollider.transform)}", this);
}
}
yield return ErrorCheckRefreshRate;
}
// ReSharper disable once IteratorNeverReturns
}
private string GetFullPathName(Transform t)
{
var bldr = new StringBuilder();
bldr.Append(t.name);
t = t.parent;
while (t != null)
{
bldr.Insert(0, @"\");
bldr.Insert(0, t.name);
t = t.parent;
}
return bldr.ToString();
}
private void ErrorCheck_IsReliableTriggerProperlySetup()
{
var colliders = MyColliders;
int triggerColliders = 0;
for (int i = 0; i < colliders.Length; i++)
{
if (colliders[i].isTrigger) triggerColliders++;
}
if (triggerColliders >= 2)
{
Debug.LogError($"It's not allowed to have more than 1 trigger collider on a ReliableTrigger. {GetFullPathName(transform)}", this);
}
}
protected virtual void OnTriggerEnter(Collider other)
{
ReliableOnTriggerExit.NotifyTriggerEnter(other, gameObject, OnTriggerExit);
EnteredTriggers.Add(other);
if (IsDebugMode)
Debug.Log($"Entered: {TriggerEnteredCount}. Collider: {other.name}");
}
protected virtual void OnTriggerExit(Collider other)
{
ReliableOnTriggerExit.NotifyTriggerExit(other, gameObject);
EnteredTriggers.Remove(other);
if (IsDebugMode)
Debug.Log($"Exit: {TriggerEnteredCount}. Collider: {other.name}");
}
protected virtual void OnDisable()
{
for (var index = EnteredTriggers.Count - 1; index >= 0; index--)
{
var et = EnteredTriggers[index];
OnTriggerExit(et);
}
}
}
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…
private void OnTriggerEnter(Collider detector, Collider other)
{
}
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)
@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
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?
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.
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.
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?
@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.
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:
public class OnDisablePropagatorN : MonoBehaviour
{
public event EventHandler Disabled;
private void OnDisable()
{
Disabled?.Invoke(this, EventArgs.Empty);
}
}
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.
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?
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.
How about this way?
This is really work well for me.
public GameObject grabbingObject;
public bool grabbedFlag = false;
void Update()
{
Ungrabbed();
}
void OnTriggerEnter(Collider other)
{
grabbedFlag = true;
grabbingObject = other.gameObject;
}
void OnTriggerExit(Collider other)
{
grabbedFlag = false;
}
void Ungrabbed()
{
if(!grabbingObject||!grabbingObject.activeInHierarchy)
{
grabbedFlag = false;
}
}
Not sure how useful it is in the general case, but I’m not tracking too many colliders, so I flag the collider in OnCollisionEnter & in OnCollisionStay, & check all tracked colliders for the flag in FixedUpdate: if I don’t see the flag, then the object’s exited the collider. Last thing I do is clear the flag at the end of FixedUpdate.
Edit: we now have loads of colliders, but I’m keeping the scripts disabled by default so I don’t have thousands of scripts running FixedUpdate: OnTriggerEnter (which runs even on disabled scripts) enables the script (so it can run FixedUpdate), and OnTriggerExit or my own exit detection code disables the script again. Although we have loads of trigger colliders, we only have one player object that moves around the map activating them, so this is working out ok for us so far:)
Thank you!!!