OnTriggerExit/OnCollisionExit and destroyed GameObjects

I have many instances in my game where I want to do something whenever certain other objects are touching a different type of object. So I can write something like the following:

class FooBehaviour : MonoBehaviour {

    int insideCount = 0;

    public void OnTriggerEnter (Collider col) {
        if (CareAbout(col)) {
            insideCount++;
        }
    }
    public void OnTriggerExit (Collider col) {
        if (CareAbout(col)) {
            insideCount--;
        }
    }

    public void Update () {
        if (insideCount > 0) {
            DoSomething();
        }
    }
}

… which works fine. Until one of the objects that happens to be touching my FooBehaviour object is destroyed. In this case, OnTriggerExit is never called and we now have a dangling extra insideCount with no object around that will ever cause it to be decremented.

This seems like it cannot be that uncommon a case (and I’ve seen several other posts of this nature elsewhere with various hacky/fragile/unpleasant workarounds) and yet there doesn’t seem to be a good way to do this kind of thing from Unity.

So, what I’m looking for is either a) Is there some actually good way to do what I want to do here but which is robust against the objects that are intersecting me being destroyed or b) Some official response from Unity as to why OnTriggerExit is not called in this case. If it’s just a matter of not wanting to break backward compatibility in some way, can we get an OnTriggerDestroyed() call or something?

As it currently stands, I’m having to make my game code much, much less efficient and much less elegant in order to work around this unfortunate case.

3 Likes

Just to add to my post above, add “when the collider is disabled” as a case when OnTriggerExit is (annoyingly) not called.

1 Like

I guess it would make things a bit easier if OnTriggerExit() was called when a trigger disappears, but personally I wouldn’t consider it hacky or ugly to add a bit more logic that causes the destroyed object to check whether it was overlapping anything that is counting overlaps and call a function to decrement insideCount if that’s the case.

Maybe you could to create a list of all the instance ID’s of the objects overlapping it and check the length, and just use OnTriggerStay() to update the list while rejecting duplicates; I have no idea how fast that would be, though.

This makes me crazy as well. I’ve registered and unregistered events onenter/exit so that I can call “notify death event” on death. Though unity isn’t 100% accurate with Triggers in my experience and events are not free.

I’ve also heard a lot of people move the object just before they destroy it.

@hoesterey - I’ve seen that suggestion to move the object and then wait a frame before destroying it. That approach strikes me as unacceptably fragile and error-prone, not to mention unpleasantly inefficient for the physics system to deal with - Physics doesn’t tend to react well when you warp an object away like that.

3 Likes

Agreed. I find this extremely hard to deal with as well. :slight_smile:

I am still trying to find a good way to deal with this. I’ve bug reported but no response yet.
https://fogbugz.unity3d.com/default.asp?859523_fduaelfpdjp731tq

Anybody found a good workaround to this issue?

Best workaround I’ve found is to register an event OnEnter and have the object remove itself, unregister the event, and trigger the appropriate OnExit scripts when it is destroyed.

1 Like

I’m trying to do something like this. I have a ColliderWatcher script that maintains a list of current collisions. When the object is disabled/destroyed it clears the list by accessing any current collisions and removing them on the other objects in addition to self. This calls an “OnExit” method even if you destroy/disable something. Works OK except…

With a complex object hierarchy w/ rigidbody and multiple colliders the ColliderWatcher can’t maintain a proper list because EVERY trigger from the parent layer, or any child layers, all trickle down to the parent layer (because there’s a rigidbody). So I have no way of distinguishing what collider was triggered. Ugh.

Any ideas?

I’m not sure of your use case.

Psudo Code:
On each parent transform with a set of colliders add a single script that is your “Collider Watcher”

Psudo Code:
Dictionary<Collider, GameObject>
event TriggerClear;
OnEnter()
{
if(Dictionary.ContainsKey(Collider))
{
Dictionary.Add(Collider, Collider.Gameobject)
Collider.Gamobject.OnDestroyEvent+= OnDestroy;
}

}

void OnExit()
{
RemoveCollider(Collider);
}

void RemoveCollider(Collider)
{
if(Dictionary.ContainsKey(Collider))
{
Dictionary.Remove(Collider);
Collider.Gameobject.OnDestroyEvent -= OnDestroy;
}
if(Dictionary.Count == 0)
{
TriggerClear.Invoke();
}
}

void OnDestroy(Collider)
{
RemoveCollider(Collider);
}

1 Like

This strikes me as overkill. All I need is a count of objects inside a trigger, just to test if any is present. What should be achieved with just a count is now turning into a complex, large overhead job of recording every object in a trigger and testing it every frame. That’s pretty stupid and inefficient.

Surely in the case of an object being destroy, it should first notify every appropriate system that it’s gone, and then be removed. Everyone coding is expecting this behaviour and is getting confused when OnExit isn’t being called, which shows that’s the natural way to do it. :wink:

An alternative would be to perform a manual call for trigger overlaps, but there’s no means to do that outside of the most basic primitives. A manual ‘TestCollisions’ would suffice, similar to the Cast methods.

Edit: I see there is a Collder2D.Cast() method!

So could call this every update to count how many triggers are present.

I wouldn’t do physics operations on every update, you will get a nasty perf hit. Sadly right now to the best of my knowledge adding a object to a List OnEnter and removing it OnExit OR though an event OnDeath is a lot of work (and shouldn’t be) but you don’t have to test it every frame if you use events so you shouldn’t have a huge perf hit.

Sage advice but dependent entirely on the game. For what I’m doing, a test every frame is fine. Worst case I can move it to every few frames.

I’ve created a ColliderWatcher script that I attach to all colliders. It records Enter/Exit and maintains a list of current collisions. However, it also handles OnDisable/OnDestroy and calls DisableExit for each current trigger/collision so there are never “dangling” collisions where Exit is failed to be called when an object is destroyed or scene is exited.

It ended up being complicated to script (due to the way Unity’s collision engine handles complex hierarchies and everything trickles down to the rigidbody… conflating all triggers/collisions). It’s also tricky to handle how to disable colliders during a collision physics update (basically all collisions must be sorted and Enter’s called first, then Exit’s… never Exit before Enter)

You can subscribe by ColliderWatcher.AddListener(TriggerCollisionData) which contains
-Enum Trigger or Collision
-Enum Enter, Exit, or ExitDisable (Enter and Exit are like normal, but ExitDisable is new and different and called when one of the 2 contacting colliders is disabled.)
-Collision information (if it’s a collision not a trigger)
-Self Collider
-Other Collider

This solves issues #1, #3, #5 (It could also solve #4 with some sorting work)

I’m not sure whether it’s best to create further events to handle objects being destroyed or disabled, but that sounds like a relatively clean way to code it, although I think that having multiple subscribers to events can create garbage, and you then have to manage the event subscriptions to avoid memory leaks :confused: Moving objects on destroy/disable sounds really horrible, but I guess if it works for some people, then more power to them :smile:

I have a sensor class in my game that keeps a list of objects that have entered the trigger, removes them when they exit, and then has an update loop that removes the objects if their reference has become null. This only works for objects that have been destroyed. I could add a check for gameObject.activeInHierarchy to handle the disabled case, but since I’m not disabling colliders on objects that are detected by the sensor, I didn’t add that yet. I don’t like this solution very much, but it works for what I’m doing, and I actually need the list of objects anyway, since the sensor needs to know what is in range, and I also keep square distances so I can get the closest or sort the list etc.

The reason I came here today is that I just made another class that needs to keep track of colliding objects to detect if an object is on the ground (which you can also do by constantly raycasting), and encountered a bug when disabling an object, so I came looking for a clean solution. Still not sure what the best answer is, but I guess I should do some tests and see what works best.

It’s a little sad that we have to write a bunch of hacky-feeling code to deal with an issue that could probably just be cleanly handled by OnTriggerExit and OnCollisionExit. Even very simple update methods and checks are not free, and neither is having lots of things subscribing to events, and complexity always adds a much greater chance of introducing new bugs.

1 Like

why not have a Boolean flag on every object that is set if it is in a collision with the FooBehaviour, by the FooBehaviour script. This sets the flag on the collider object when it is in a collision.
The destroy command should check if the destroyed object has a flag and reduce the insidecount if the flag is set.

I hope that makes sense. to me it is clean, has very little overhead and is frame rate independent.

Unity’s physics/collision system has lots of issues
-OnCollision/TriggerExit is not called when the other object in a current collision is destroyed.
-OnCollision/TriggerExit is not called when the self object in a current collision is destroyed.
-OnCollision/TriggerExit is not called when the other object in a current collision is disabled.
-OnCollision/TriggerExit is not called when the self object in a current collision is disabled.
-OnCollision/TriggerExit is not called when the other object in a current collision’s layer is changed (If I remember right)
-OnCollision/TriggerExit is not called when the self object in a current collision layer is changed (If I remember right)
-The order of OnCollision/Trigger Enter/Exit is not consistent. If an object is changed to/from a Collider to a Trigger you can get 2 Enter’s or 2 Exit’s in a row.
-Unity’s collision system does not allow you to check a list of current collisions (and maintaining a list is unreliable due to bugs above)
-Unity’s collision system does not allow you to instantly spot check what is in a new mesh collider. For example if a dragon breathes fire you wanted to create a cone mesh and take a snapshot to see what is within the cone there is no way to do instantly do this. You must instantiate the cone, and then wait for the next fixed update.
-Unity’s collision system does not handle hierarchy well. If you have several colliders on each object it’s difficult to tell what collided/triggered with what. Colliders & Triggers also do not behave consistently in hierarchies.
-Rigidbody-interpolate affects velocity (https://fogbugz.unity3d.com/default.asp?987466_rtr9jodnm5n8afbp)

Here’s the code I used to fix. It’s really ugly. I have to add a ColliderWatcher to each collider. But it fixes most bugs above. Maybe somebody can clean it up a bit.

using UnityEngine;
using System.Collections;
using System.Collections.Generic; //allow List and Dictionary

//This script is always placed in the same level of the hierarchy as the collider
public sealed class ColliderWatcher : MonoBehaviour
{
    public Collider colliderSelf { get; private set; }
    private Rigidbody attachedRigidbody; //store this value for errorChecking at a later time (to make sure a rigidbody isn't added AFTER colliderWatcher is initialized)

    public List<ColliderWatcher> listColliderWatcherOther { get; private set; } //list of current triggers/collisions
    //public List<Collider> listColliderOther { get; private set; }

    //public delegate void DelegateCollider(Collider colliderOther, Collision collisionCBNIn); //signature of function
    //private DelegateCollider OnEnterEventSender; //called by OnCollisionEnter OR OnTriggerEnter
    //private DelegateCollider OnExitEventSender; //called by OnCollisionExit OR OnTriggerExit OR when either collider is disabled/destroyed

    public delegate void DelegateTriggerCollision(TriggerCollisionData triggerCollisionData); //signature of function
    private DelegateTriggerCollision OnTriggerCollisionEventSender; //called by OnTriggerXXX OR OnCollisionXXX OR when disabled (or when something it is currently colliding with is disabled)

    //public delegate void DelegateTriggerCollide(EnumEnterExit enumEnterExitIn, Collider colliderOther, Collision collisionCBNIn)

    //public Collider[] debugColliderArray; //serialized for debug
    //public string[] debugCurrentCollisions;

    private void Awake()
    {
        //listColliderOther = new List<Collider>(); //initialize list
        listColliderWatcherOther = new List<ColliderWatcher>(); //initialize list
        InitializeFindColliderSelf();
    }

    private void Start()
    {
        InitializeAddColliderWatcherHelper(); //is it better to do this during Start()?  That way any changing of hierarchy is complete before it adds ColliderWatcherHelper?
    }


    private void InitializeFindColliderSelf()
    {
        colliderSelf = GetComponent<Collider>(); //every ColliderWatcher must be in the same hierarchy level as it's collider (even though OnCollision/Trigger/Enter/Exit may not be called on this layer)
        if (colliderSelf == null) //ErrorCheck
        {
            BatDebug.Error(this, "768uytyrt3r467", "colliderSelf == null");
        }
    }

    private void InitializeAddColliderWatcherHelper()
    {
        if (colliderSelf != null) //ErrorCheck
        {
            attachedRigidbody = colliderSelf.attachedRigidbody; //the rigidbody MUST be attached before ColliderWatcher is initialized.  otherwise ColliderWatcherSender will be in the wrong layer of the hierarchy
            if (attachedRigidbody == null) //this collider does not have an attachedRigidbody
            {
                colliderSelf.transform.gameObject.AddComponent<ColliderWatcherHelper>(); //add the ColliderWatcherHelper directly to this layer of hierarchy
            }
            else //attachedRigidbody = true
            {
                ColliderWatcherHelper colliderWatcherHelperTemp = attachedRigidbody.gameObject.GetComponent<ColliderWatcherHelper>();
                if (colliderWatcherHelperTemp == null)
                {
                    //Debug.Logz("Adding ColliderWatcherHelper to rigidbody");
                    attachedRigidbody.gameObject.AddComponent<ColliderWatcherHelper>(); //add the ColliderWatcherHelper to the parent layer of hierarchy that contains rigidbody
                }
                //else
                //{
                //    Debug.Logz("Not ColliderWatcherHelper to rigidbody.  Already has this compoment.  Can occur if there are multiple colliders all feeding into 1 rigidbody");
                //}

            }
        }
        else
        {
            BatDebug.Error(this, "5y6uhtrgr3645y7u6jyin", "colliderSelf == null");
        }
    }

    //public void AddColliderOther(Collider colliderOtherIn)
    //{
    //    Debug.Logz("ColliderWatcher2.AddColliderOther");
    //}

    //public void RemoveColliderOther(Collider colliderOtherIn)
    //{
    //    Debug.Logz("ColliderWatcher2.RemoveColliderOther");
    //}

    /*
public void AddRemoveColliderOther(Collider colliderOtherIn, EnumEnterExit enumEnterExitIn) //called by ColliderWatcherHelper when a pair is found
{
    if(colliderOtherIn != null) //ErrorCheck
    {
        if(enumEnterExitIn == EnumEnterExit.Enter)
        {
            AddColliderToList(colliderOtherIn);
        }
        else if (enumEnterExitIn == EnumEnterExit.Exit)
        {
            RemoveColliderFromList(colliderOtherIn);
        }
        else
        {
            Debug.Logz("ERROR: Invalid enum");
        }
    }
    else
    {
        Debug.Logz("ERRORz");
    }
}
*/

    private void AddColliderWatcherToList(ColliderWatcher colliderWatcherOtherIn) //Add to list w/ ErrorCheck
    {
        if (!listColliderWatcherOther.Contains(colliderWatcherOtherIn)) //ErrorCheck, make sure list does not already contain colliderWatcherOther
        {
            listColliderWatcherOther.Add(colliderWatcherOtherIn); //add to list
        }
        else
        {
            BatDebug.Error(this, "656uyjnhgrer3243", "Already contains collider: Should never happen");
        }
    }

    private void RemoveColliderWatcherFromList(ColliderWatcher colliderWatcherOtherIn) //Remove from list w/ ErrorCheck
    {
        if (listColliderWatcherOther.Contains(colliderWatcherOtherIn)) //ErrorCheck, make sure list does not already contain colliderWatcherOther
        {
            listColliderWatcherOther.Remove(colliderWatcherOtherIn); //remove from list
        }
        else
        {
            BatDebug.Error(this, "457yu6jythrgtr345y", "List does not contain ColliderWatcher: Should never happen");
        }
    }

    /*
    private void AddColliderToList(Collider colliderOtherIn)
    {
        //Debug.Logz(gameObject.name + ".AddColliderToList " + colliderOtherIn.gameObject.name);
        if (!listColliderOther.Contains(colliderOtherIn)) //ErrorCheck, make sure list does not already contain colliderOther
        {
            listColliderOther.Add(colliderOtherIn); //add to list
            if (colliderOtherIn.GetComponent<ColliderWatcher>() == null) //ErrorCheck, make sure colliderOther has an associated ColliderWatcher
            {
                Debug.Logz("ERROR: ColliderOther does not contain ColliderWatcher: " + colliderOtherIn.gameObject.name + " " + colliderOtherIn.transform.root.gameObject.name);
            }
            if (OnEnterEventSender != null) //at least 1 listener
            {
                Debug.Logz("FIXME NULL PLACEHOLDER"); OnEnterEventSender.Invoke(colliderOtherIn, null);
            }
        }
        else
        {
            Debug.Logz("ERROR: Already contains collider: Should never happen. " + gameObject.NameHierarchy() + " attempting to add " + colliderOtherIn.gameObject.NameHierarchy());
        }
        //DebugRefreshDebugColliderArray();
    }

    private void RemoveColliderFromList(Collider colliderOtherIn)
    {
        //Debug.Logz(gameObject.name + ".RemoveColliderFromList " + colliderOtherIn.gameObject.name);
        if (colliderOtherIn != null)
        {
            if (listColliderOther.Contains(colliderOtherIn)) //ErrorCheck, make sure list contains colliderOther before it's removed
            {
                listColliderOther.Remove(colliderOtherIn); //remove from list
                if (OnExitEventSender != null) //at least 1 listener
                {
                    Debug.Logz("FIXME NULL PLACEHOLDER"); OnExitEventSender.Invoke(colliderOtherIn, null);
                }
            }
            else
            {
                Debug.Logz("ERROR: List does not contain collider.  It cannot be removed.  Should never happen." + gameObject.NameHierarchy() + "  otherGORoot=" + colliderOtherIn.gameObject.NameHierarchy());
            }
        }
        else
        {
            Debug.Logz("ERROR: colliderOtherIn == null" + gameObject.name + " " + gameObject.transform.root.gameObject.name);
        }
        //DebugRefreshDebugColliderArray();
    }
    */

    public void OnTriggerCollisionPairXXX(TriggerCollisionData triggerCollisionDataIn) //the other collision info is sent to this ColliderWatcher, and this ColliderWatcher's collision info is sent to the other ColliderWatcher
    {
        //Debug.Logz("OnTriggerCollisionPairXXX:    Self:" + gameObject.NameHierarchy() + "    Other:" + triggerCollisionDataIn.colliderWatcherOther.gameObject.NameHierarchy() + "    " + Time.time);
        if(triggerCollisionDataIn != null) //ErrorCheck
        {
            if(triggerCollisionDataIn.enumEnterExit == EnumEnterExit.Enter)
            {
                //listColliderOther.Add(triggerCollisionDataIn.coll); //add to list
                //listColliderWatcherOther.Add(triggerCollisionDataIn.colliderWatcherOther);
                AddColliderWatcherToList(triggerCollisionDataIn.colliderWatcherOther);
                //new TriggerCollisionData()
               

            }
            else if (triggerCollisionDataIn.enumEnterExit == EnumEnterExit.Exit)
            {
                //listColliderWatcherOther.Remove(triggerCollisionDataIn.colliderWatcherOther);
                RemoveColliderWatcherFromList(triggerCollisionDataIn.colliderWatcherOther);
            }
            else
            {
                BatDebug.Error(this, "5467yur4t35r42", "Unhandled enum case");
            }
            if (OnTriggerCollisionEventSender != null) //at least 1 listener
            {
                OnTriggerCollisionEventSender.Invoke(triggerCollisionDataIn);
            }
        }
        else
        {
            BatDebug.Error(this, "vf34FDSadf", "triggerCollisionDataIn == null");
        }
        //DebugRefreshCurrentCollisions();
    }



    private void OnEnable()
    {
        //Debug.Logz("OnEnable(): " + gameObject.NameHierarchy());
        if (colliderSelf != null)
        {
            colliderSelf.enabled = true;
        }
        else
        {
            BatDebug.Error(this, "456tjhgrer243r54t", "colliderSelf == null");
        }
    }

    private void OnDestroy() //If something is Destroy(gameObject) during OnCollision in the Physics loop it will be disabled immediately, but disabled at the END of all triggers/collisions.  Attempting to execute stack during OnDestroy is a good spot.
    {
        //Debug.Logz(this.GetType().Name + ".OnDestroy(): " + gameObject.NameHierarchy() + " " + Time.time + " currentCollisions=" + listColliderWatcherOther.Count);
        ColliderWatcherManager.ColliderWatcherDisabled.ExecuteStack("OnDestroy");
    }

    private void OnDisable() //this is called before something is destroyed.  it should also be called if a dead monster is made hollow (the collider.enabled should NOT be set directly)  this is used because other scripts will still exist (they won't be null'd yet)
    {
        //Debug.Logz(this.GetType().Name + ".OnDisable(): " + gameObject.NameHierarchy() + " " + Time.time + " currentCollisions=" + listColliderWatcherOther.Count + " " + ScriptExecutionOrder.enumLoop);

        if (colliderSelf != null)
        {
            colliderSelf.enabled = false;
        }
        else
        {
            BatDebug.Error(this, "3454thtbgrr233rt", "colliderSelf == null");
        }


        new ColliderWatcherManager.ColliderWatcherDisabled(this); //adds to stack & executes stack. Even if current collisions = 0 it still adds to stack because it's possible that some collisions may still occur during this loop and add to list
        //ColliderWatcherManager

        //Debug.Logz("OnDisable2: " + gameObject.NameHierarchy() + " " + Time.time + " currentCollisions=" + listColliderWatcherOther.Count);

        //for (int i = 0; i <= 10000; i++) //~while loop, prevent infinite looping
        //{
        //    if (i == 10000)
        //    {
        //        Debug.Logz("ERROR: Infinite Loop");
        //    }

        //    if (listColliderWatcherOther.Count > 0)
        //    {
        //        //listColliderWatcherOther[0].ExecuteAndRemoveFromStack();
        //    }
        //    else //has cleared all collisions
        //    {
        //        break;
        //    }
        //}

        /*
        for (int i = listColliderOther.Count - 1; i >= 0; i--) //reverse iterate because collection will be modified
        {
            if (listColliderOther[i] != null)
            {
                ColliderWatcher colliderWatcherOther = listColliderOther[i].GetComponent<ColliderWatcher>();
                if (colliderWatcherOther != null)
                {
                    colliderWatcherOther.RemoveColliderFromList(colliderSelf); //remove this collliderSelf from the OTHER watcher
                    RemoveColliderFromList(listColliderOther[i]); //remove colliderOther from this' list
                }
                else
                {
                    Debug.Logz("ERROR: ColliderOther does not contain ColliderWatcher: " + listColliderOther[i].gameObject.name + " " + listColliderOther[i].transform.root.gameObject.name);
                }
            }
            else
            {
                Debug.Logz("ERROR: listColliderOther[" + i + "] == null.   This name = " + gameObject.transform.root.gameObject.name);
            }


        }
        */
    }

    public void OnDisabledFromStack() //called from ColliderWatcherManager when this is removed from stack (occurs after OnDisable)
    {
        //Debug.Logz("OnDisabledFromStack(): " + gameObject.NameHierarchy() + " " + Time.time + " currentCollisions=" + listColliderWatcherOther.Count);
        TriggerCollisionData triggerCollisionDataA = new TriggerCollisionData(EnumTriggerCollision.Disable, EnumEnterExit.Exit, this, null);
        for (int i = listColliderWatcherOther.Count - 1; i >= 0; i--) //reverse iterate because collection will be modified
        {
            if (listColliderWatcherOther[i] != null)
            {
               
                TriggerCollisionData triggerCollisionDataB = new TriggerCollisionData(EnumTriggerCollision.Disable, EnumEnterExit.Exit, listColliderWatcherOther[i], null);
                TriggerCollisionData.ApplyPair(triggerCollisionDataA, triggerCollisionDataB);

                //ColliderWatcher colliderWatcherOther = listColliderOther[i].GetComponent<ColliderWatcher>();
                //if (colliderWatcherOther != null)
                //{

                //colliderWatcherOther.RemoveColliderFromList(colliderSelf); //remove this collliderSelf from the OTHER watcher
                //RemoveColliderFromList(listColliderOther[i]); //remove colliderOther from this' list
                //}
                //else
                //{
                //    Debug.Logz("ERROR: ColliderOther does not contain ColliderWatcher: " + listColliderOther[i].gameObject.name + " " + listColliderOther[i].transform.root.gameObject.name);
                //}
            }
            else
            {
                BatDebug.Error(this, "5u6trhyt4r35", "listColliderOther[" + i + "] == null");
            }


        }
    }

    /*
    public void AddListenerEnter(DelegateCollider delegateColliderIn) //subscribe to eventSender
    {
        if (delegateColliderIn != null)
        {
            OnEnterEventSender += delegateColliderIn;
        }
        else
        {
            Debug.Logz("ERROR: Null");
        }
    }

    public void AddListenerExit(DelegateCollider delegateColliderIn) //subscribe to eventSender
    {
        if (delegateColliderIn != null)
        {
            OnExitEventSender += delegateColliderIn;
        }
        else
        {
            Debug.Logz("ERROR: Null");
        }
    }
    */

    public void AddListener(DelegateTriggerCollision delegateTriggerCollisionIn)
    {
        if (delegateTriggerCollisionIn != null)
        {
            OnTriggerCollisionEventSender += delegateTriggerCollisionIn;
        }
        else
        {
            BatDebug.Error(this, "54675yutjhrtr423", "delegateTriggerCollisionIn == null");
        }
    }

    public void RemoveListener(DelegateTriggerCollision delegateTriggerCollisionIn)
    {
        if (delegateTriggerCollisionIn != null)
        {
            OnTriggerCollisionEventSender -= delegateTriggerCollisionIn;
        }
        else
        {
            BatDebug.Error(this, "545ythgbfrew2rt45y", "delegateTriggerCollisionIn == null");
        }
    }

    //private void DebugRefreshDebugColliderArray()
    //{
    //    debugColliderArray = listColliderOther.ToArray();
    //}

        /*
    private void DebugRefreshCurrentCollisions()
    {
        List<string> listStringTemp = new List<string>();
        for(int i = 0; i < listColliderWatcherOther.Count; i++)
        {
            string otherNameTemp = "null";
            if(listColliderWatcherOther[i] != null)
            {
                otherNameTemp = listColliderWatcherOther[i].gameObject.NameHierarchy();
            }
            listStringTemp.Add("[" + i + "]" + otherNameTemp);
        }
        debugCurrentCollisions = listStringTemp.ToArray();
    }
    */

    public bool isTrigger
    {
        get
        {
            return colliderSelf.isTrigger;
        }
        set
        {
            if(isTrigger != value) //value has change
            {
                //bool isEnabledPre = enabled;
                if(listColliderWatcherOther.Count > 0) //currently enabled
                {
                    BatDebug.Error(this, "465yhgrfe23rt4y", "annot change isTrigger while currently colliding??  Since Trigger always occurs before Collided it will either be added twice in a row or removed twice in a row causing errors");
                }
            }
            else
            {
                BatDebug.Error(this, "45ythgrr3454y5", "Set to same value of " + value);
            }
        }

    }
}


using UnityEngine;
using System.Collections;
using System.Collections.Generic; //allow List

public class ColliderWatcherHelper : MonoBehaviour
{
    //private static EnumEnterExit enumEnterExit = EnumEnterExit.Enter;
    //private static Collider colliderA = null; //for OnTriggerXXX
    //private static Collider colliderB = null; //for OnTriggerXXX
    //private static Collision collisionA = null; //for OnCollisionXXX (will be null for trigger)
    //private static Collision collisionB = null; //for OnCollisionXXX (will be null for trigger)

    //private static float timeFixedTimeA = 1.111f;
    //private static float timeFixedTimeB = 2.222f;

    //private static List<ColliderPair> stackColliderPair = new List<ColliderPair>();
    //private static bool currentlyExecutingStack = false;

    private void Awake()
    {
        ColliderWatcher[] colliderWatcherArray = GetComponents<ColliderWatcher>();
        if(colliderWatcherArray.Length >= 2) //There can only be 1 collisionHelper
        {
            BatDebug.Error(this, "54657u6yihjrge", "Already contains ColliderWatcherHelper.  There should only be 1");
        }
    }

   

    public void OnTriggerEnter(Collider colliderOtherIn)
    {
        //Debug.Logz("OnTriggerEnter: " + gameObject.NameHierarchy() + "    other: " + colliderOtherIn.gameObject.NameHierarchy());
        //Debug.Logz(gameObject.name + ".OnTriggerEnter");
        //OnEnter(colliderOtherIn);

        //if(ErrorCheckAndFindColliderWatcher(colliderOtherIn))
        //{
        //    ColliderWatcherManager.OnTriggerXXX(EnumEnterExit.Enter, colliderOtherIn);
        //}

        //OnTriggerXXX(EnumEnterExit.Exit, colliderOtherIn);
        OnTriggerCollisionXXX(EnumTriggerCollision.Trigger, EnumEnterExit.Enter, colliderOtherIn, null);
    }

    public void OnTriggerExit(Collider colliderOtherIn)
    {
        //Debug.Logz("OnTriggerExit: " + gameObject.NameHierarchy() + "    other: " + colliderOtherIn.gameObject.NameHierarchy());
        //Debug.Logz(gameObject.name + ".OnTriggerExit");
        //OnExit(colliderIn);

        //OnTriggerXXX(EnumEnterExit.Exit, colliderOtherIn);
        OnTriggerCollisionXXX(EnumTriggerCollision.Trigger, EnumEnterExit.Exit, colliderOtherIn, null);
    }

    //private void OnTriggerXXX(EnumEnterExit enumEnterExitIn, Collider colliderOtherIn)
    //{
    //    ColliderWatcher colliderWatcherOtherTemp = ErrorCheckAndFindColliderWatcher(colliderOtherIn); //if this fails it spits out errors
    //    if (colliderWatcherOtherTemp != null) //ErrorCheck
    //    {
    //        ColliderWatcherManager.OnTriggerXXX(EnumEnterExit.Exit, colliderWatcherOtherTemp, colliderOtherIn);
    //    }
    //}

    public void OnCollisionEnter(Collision collisionOtherIn)
    {
        //Debug.Logz("OnCollisionEnter: " + gameObject.NameHierarchy() + "    other: " + collisionOtherIn.collider.gameObject.NameHierarchy());
        //Debug.Logz(gameObject.name + ".OnCollisionEnter");
        //OnEnter(collisionOtherIn.collider);
        //ContactPoint[] contactPointArray = collisionOtherIn.contacts;
        //Debug.Logz(gameObject.NameHierarchy() + ".OnCollisionEnter: Contact Points = " + contactPointArray.Length);
        //for(int i = 0; i < contactPointArray.Length; i++)
        //{
        //    Debug.Logz("[" + i + "]   Self: " + contactPointArray[i].thisCollider.gameObject.NameHierarchy() + "     Other: " + contactPointArray[i].otherCollider.gameObject.NameHierarchy());
        //}

        //if (!ErrorCheckColliderHasColliderWatcher(collisionOtherIn.collider))
        //{
        //    ColliderWatcherManager.OnCollisionXXX(EnumEnterExit.Enter, colliderIn);
        //}
        OnTriggerCollisionXXX(EnumTriggerCollision.Collision, EnumEnterExit.Enter, collisionOtherIn.collider, collisionOtherIn);
    }

    public void OnCollisionExit(Collision collisionOtherIn)
    {
        //Debug.Logz("OnCollisionExit: " + gameObject.NameHierarchy() + "    other: " + collisionOtherIn.collider.gameObject.NameHierarchy());
        //Debug.Logz(gameObject.name + ".OnCollisionExit");
        //OnExit(collisionOtherIn.collider);
        OnTriggerCollisionXXX(EnumTriggerCollision.Collision, EnumEnterExit.Exit, collisionOtherIn.collider, collisionOtherIn);
    }

    //private void OnCollisionXXX(EnumEnterExit enumEnterExitIn, Collision collisionOtherIn)
    //{
    //    ColliderWatcher colliderWatcherOtherTemp = ErrorCheckAndFindColliderWatcher(collisionOtherIn.collider); //if this fails it spits out errors
    //    if (colliderWatcherOtherTemp != null) //ErrorCheck
    //    {
    //        ColliderWatcherManager.OnCollisionXXX(EnumEnterExit.Exit, colliderWatcherOtherTemp, collisionOtherIn);
    //    }
    //}

    //private void OnEnter(Collider colliderOtherIn) //collision AND trigger
    //{
    //    //Debug.Logz("OnEnter: " + gameObject.NameHierarchy() + "    other: " + colliderOtherIn.gameObject.NameHierarchy());
    //    enumEnterExit = EnumEnterExit.Enter;
    //    OnEnterExit(colliderOtherIn);

       
    //}

    //private void OnExit(Collider colliderOtherIn) //collision AND trigger
    //{
    //    //Debug.Logz("OnEnter: " + gameObject.NameHierarchy() + "    other: " + colliderOtherIn.gameObject.NameHierarchy());
    //    enumEnterExit = EnumEnterExit.Exit;
    //    OnEnterExit(colliderOtherIn);
    //}

    private void OnTriggerCollisionXXX(EnumTriggerCollision enumTriggerCollisionIn, EnumEnterExit enumEnterExitIn, Collider colliderOtherIn, Collision collisionCBNIn) //Collider is used for Trigger, and Collision is used for Collision
    {
        //Debug.Logz("ColliderWatcherHelper.OnTriggerCollisionXXX    Self: " + gameObject.NameHierarchy() + "    Other: " + colliderOtherIn.gameObject.NameHierarchy() + " " + enumTriggerCollisionIn + " " + enumEnterExit);
        //ScriptExecutionOrder.DebugDisplay();
        ColliderWatcher colliderWatcherOtherTemp = ErrorCheckAndFindColliderWatcher(colliderOtherIn); //if this fails it spits out errors
        if (colliderWatcherOtherTemp != null) //ErrorCheck
        {
            //ColliderWatcherManager.OnTriggerXXX(EnumEnterExit.Exit, colliderWatcherOtherTemp, colliderOtherIn);
            //ColliderWatcherManager.OnTriggerCollisionXXX(enumTriggerCollisionIn, enumEnterExitIn, colliderWatcherOtherTemp, collisionCBNIn);

            TriggerCollisionData triggerCollisionData = new TriggerCollisionData(enumTriggerCollisionIn, enumEnterExitIn, colliderWatcherOtherTemp, collisionCBNIn); //one half of the collision (takes 2 to make a pair)
            ColliderWatcherManager.OnTriggerCollisionXXX(triggerCollisionData); //send one half of the pair (may be the 1st or 2nd half)
        }
    }

    /*
    private void OnEnterExit(Collider colliderOtherIn) //used for both enter/exit for both collision/trigger
    {
        if (colliderOtherIn != null) //ErrorCheck
        {
            if (colliderOtherIn.GetComponent<ColliderWatcher>()) //ErrorCheck just to make sure the other collider contains ColliderWatcher (common error if you forget to add ColliderWatcher to every collider in the game)
            {
                if (colliderA == null)
                {
                    colliderA = colliderOtherIn;
                    //timeFixedTimeA = Time.fixedTime;
                }
                else
                {
                    colliderB = colliderOtherIn;
                    //timeFixedTimeB = Time.fixedTime;
                    EnterExitPair();
                }
            }
            else
            {
                Debug.Logz("ERROR: " + colliderOtherIn.gameObject.NameHierarchy() + " does not contain ColliderWatcher");
            }
        }
        else
        {
            Debug.Logz("ERROR: colliderOtherIn == null");
        }
    }
    */

    /*
    private static void EnterExitPair() //called OnTriggerEnter/OnCollisionEnter
    {
        //Debug.Logz("Pair " + enumEnterExit + "  A:" + colliderA.gameObject.NameHierarchy() + "   B:" + colliderB.gameObject.NameHierarchy());

        if (colliderA != null && colliderB != null) //ErrorCheck
        {
            ColliderWatcher colliderWatcherA = colliderA.GetComponent<ColliderWatcher>();
            ColliderWatcher colliderWatcherB = colliderB.GetComponent<ColliderWatcher>();
            if (colliderWatcherA != null && colliderWatcherB != null) //ErrorCheck
            {
                bool enabledBeforeA = colliderWatcherA.enabled;
                bool enabledBeforeB = colliderWatcherB.enabled;
                colliderWatcherA.AddRemoveColliderOther(colliderB, enumEnterExit);
                colliderWatcherB.AddRemoveColliderOther(colliderA, enumEnterExit);
                if(colliderWatcherA.enabled != enabledBeforeA) //ErrorCheck, cannot change collider.enabled status during OnCollisionEnter/Exit
                {
                    Debug.Logz("ERROR: During OnCollisionEnter/Exit the ColliderWatcher must not be disabled/eanbled during the execution of the pair");
                }
                if (colliderWatcherB.enabled != enabledBeforeB) //ErrorCheck, cannot change collider.enabled status during OnCollisionEnter/Exit
                {
                    Debug.Logz("ERROR: During OnCollisionEnter/Exit the ColliderWatcher must not be disabled/eanbled during the execution of the pair");
                }

                //stackColliderPair.Add(new ColliderPair(colliderA, colliderWatcherA, colliderB, colliderWatcherB, enumEnterExit));
                //if (currentlyExecutingStack == false)
                //{
                //    currentlyExecutingStack = true;
                //    while(stackColliderPair.Count > 0)
                //    {
                //        stackColliderPair[0].colliderWatcherA.AddRemoveColliderOther(stackColliderPair[0].colliderB, stackColliderPair[0].enumEnterExit);
                //        stackColliderPair[0].colliderWatcherB.AddRemoveColliderOther(stackColliderPair[0].colliderA, stackColliderPair[0].enumEnterExit);
                //        stackColliderPair.RemoveAt(0);
                //    }
                //    currentlyExecutingStack = false;
                //}
            }
            else
            {
                Debug.Logz("ERROR: colliderWatcherA A or B is null");
            }
        }
        else
        {
            Debug.Logz("ERROR: collider A or B is null");
        }

        //regardless if there are errors or not, clear the static collider pair
        colliderA = null;
        colliderB = null;
    }
    */

    //public enum EnumEnterExit { Enter, Exit}

    //public static void ErrorCheckUpdate() //called by GameController
    //{
    //    if(colliderA != null || colliderB != null)
    //    {
    //        colliderA = null;
    //        colliderB = null;
    //        Debug.Logz("ERROR: collisions should always occur in pairs.  during update there should never be a colliderA or B that is not null");
    //    }
    //}

    /*
    private struct ColliderPair
    {
        public Collider colliderA { get; private set; }
        public ColliderWatcher colliderWatcherA { get; private set; }
        public Collider colliderB { get; private set; }
        public ColliderWatcher colliderWatcherB { get; private set; }
        public EnumEnterExit enumEnterExit { get; private set; }

        public ColliderPair(Collider colliderAIn, ColliderWatcher colliderWatcherAIn, Collider colliderBIn, ColliderWatcher colliderWatcherBIn, EnumEnterExit enumEnterExitIn) //constructor
        {
            colliderA = colliderAIn;
            colliderWatcherA = colliderWatcherAIn;
            colliderB = colliderBIn;
            colliderWatcherB = colliderWatcherBIn;
            if(colliderA == null || colliderWatcherA == null || colliderB == null || colliderWatcherB == null) //ErrorCheck
            {
                Debug.Logz("ERROR: Something null");
            }
            enumEnterExit = enumEnterExitIn;
        }
    }
    */

    private ColliderWatcher ErrorCheckAndFindColliderWatcher(Collider colliderIn) //return true if error, false if no error
    {
        if(colliderIn != null)
        {
            ColliderWatcher returnMe = colliderIn.GetComponent<ColliderWatcher>(); //attempt to get
            if (returnMe != null) //contains a colliderWatcher
            {
                return returnMe;
            }
            else
            {
                BatDebug.Error(this, "565uyjhtrgtr3", "returnMe == null: no ColliderWatcher");
                return null;
            }
        }
        else
        {
            BatDebug.Error(this, "35zcxv353543", "colliderIn == null");
            return null;
        }
    }

   
}



using UnityEngine;
using System.Collections;
using System;
using System.Collections.Generic; //allow List

public class ColliderWatcherManager : MonoBehaviour
{
    private static ColliderWatcherManager singleton;

    private static TriggerCollisionData triggerCollisionDataA = null;
    private static TriggerCollisionData triggerCollisionDataB = null;

    private void Awake()
    {
        InitializeSingleton();
    }

    private void InitializeSingleton()
    {
        if(singleton != null)
        {
            BatDebug.Error(this, "756avs34aweasf", "singleton set twice");
        }
        singleton = this;
    }

    //private void Update() //due to scriptExecutionOrder this will be the FIRST thing that will be run.  Essentially the 1st thing ran after Physics performs a stack of collisions.
    //{
    //    ColliderWatcherDisabled.ExecuteStack("ColliderWatcherManager.Update");
    //}

    public static void OnTriggerCollisionXXX(TriggerCollisionData triggerCollisionDataIn) //for trigger AND collision
    {
        //Debug.Logz("OnTriggerCollisionXXX: " + triggerCollisionDataIn.enumTriggerCollision + " " + triggerCollisionDataIn.enumEnterExit + " " + triggerCollisionDataIn.colliderWatcherOther.gameObject.NameHierarchy() + " " + triggerCollisionDataIn.collisionCBN);
        if (triggerCollisionDataA == null)
        {
            triggerCollisionDataA = triggerCollisionDataIn;
        }
        else //A is already assign, so assign B
        {
            triggerCollisionDataB = triggerCollisionDataIn;
            //PLACEHOLDER, errorChecks

            TriggerCollisionData.ApplyPair(triggerCollisionDataA, triggerCollisionDataB);

            triggerCollisionDataA = null; //clear
            triggerCollisionDataB = null; //clear
        }
    }

    /*
    public abstract class Stackable// : IComparable<Stackable>
    {
        private static List<Stackable> listStack = new List<Stackable>();
        private static bool isExecutingStack = false;

        public Stackable() //constructor
        {
            listStack.Add(this);
            //ExecuteStack();
            Debug.Logz("StackCount: Add = " + listStack.Count);
        }

        private void ExecuteAndRemoveFromStack()
        {
            Debug.Logz("ExecuteAndRemoveFromStack: " + this.GetType().Name);
            listStack.Remove(this);
            Debug.Logz("StackCount: Remove = " + listStack.Count);
            ExecuteAndRemoveFromStack1();

        }

        protected abstract void ExecuteAndRemoveFromStack1();

        public static void ExecuteStack()
        {
            if (isExecutingStack == false && listStack.Count > 0)
            {
                Debug.Logz("ExecuteStack: count = " + listStack.Count);
                listStack.Sort();
                isExecutingStack = true;
                for (int i = 0; i <= 10000; i++) //~while loop, prevent infinite looping
                {
                    if (i == 10000)
                    {
                        isExecutingStack = false;
                        Debug.Logz("ERROR: Infinite Loop");
                    }

                    if (listStack.Count > 0)
                    {
                        listStack[0].ExecuteAndRemoveFromStack();
                    }
                    else
                    {
                        isExecutingStack = false;
                        break;
                    }
                }
            }
            //else
            //{
            //    Debug.Logz("Currently executing stack, not executing again");
            //}
           
        }

        //public int CompareTo(Stackable other)
        //{
        //    return other.sortValue - sortValue;
        //}

        //protected abstract int sortValue
        //{
        //    get;
        //}
    }
    */

    /*
    public sealed class TriggerCollisionPair : Stackable
    {
        private TriggerCollisionData triggerCollisionDataA;
        private TriggerCollisionData triggerCollisionDataB;

        public TriggerCollisionPair(TriggerCollisionData triggerCollisionDataAIn, TriggerCollisionData triggerCollisionDataBIn) //constructor
        {
            if(triggerCollisionDataAIn == null || triggerCollisionDataBIn == null)
            {
                Debug.Logz("ERRORz");
            }
            triggerCollisionDataA = triggerCollisionDataAIn;
            triggerCollisionDataB = triggerCollisionDataBIn;
            //ExecuteStack(); //execute AFTER everything is assigned (which is why ExecuteStack cannot be part of parent constructor)
        }

       

        protected sealed override void ExecuteAndRemoveFromStack1()
        {
            if(triggerCollisionDataA == null)
            {
                Debug.Logz("ERRORz");
            }
            if(triggerCollisionDataA.colliderWatcher == null)
            {
                Debug.Logz("ERRORz");
            }
            triggerCollisionDataA.colliderWatcher.OnTriggerCollisionPairXXX(triggerCollisionDataB);
            triggerCollisionDataB.colliderWatcher.OnTriggerCollisionPairXXX(triggerCollisionDataA);
            //triggerCollisionDataA.colliderWatcher.AddRemoveColliderOther()
        }

        protected sealed override int sortValue //trigger/collisions happen first
        {
            get
            {
                return 1;
            }
        }
    }
    */

    public sealed class ColliderWatcherDisabled// : Stackable
    {
        private static List<ColliderWatcherDisabled> listStack = new List<ColliderWatcherDisabled>(); //list of all disabled colliders (they are added to stack of they occur during the physics loop, and are execute immediately if they occur at a different time)

        private ColliderWatcher colliderWatcher;
        private static bool isExecutingStack = false;

        public ColliderWatcherDisabled(ColliderWatcher colliderWatcherIn) //constructor
        {
            //Debug.Logz("New ColliderWatcherDisabled: Loop = " + ScriptExecutionOrder.enumLoop);
            colliderWatcher = colliderWatcherIn;
            listStack.Add(this);

            if (ScriptExecutionOrder.enumLoop != ScriptExecutionOrder.EnumLoop.Physics) //anything other than the physics trigger/collision loop
            {
                //Debug.Logz("Execute Stack Immediate");
                ExecuteStack("ColliderWatcherDisabled Immediate"); //execute stack. if other colliderWatchers are disabled while executing stack then they will also be added to the stack
            }
            else //was disabled during the trigger/collision loop
            {
                //Debug.Logz("Execute Stack Delayed");
                //Debug.Logz("Postponing execution of disable stack because it occured within the Physics loop");
            }
        }

        private void ExecuteAndRemoveFromStack()
        {
            listStack.Remove(this);
            colliderWatcher.OnDisabledFromStack();
        }

        public static void ExecuteStack(string callerNameIn)
        {
            //Debug.Logz("ExecuteStack: currentlyExecuting = " + isExecutingStack + "  stackCount=" + listStack.Count);
            if (isExecutingStack == false && listStack.Count > 0)
            {
                //Debug.Logz("ExecuteStack: count = " + listStack.Count + " " + Time.time + "  callerNameIn=" + callerNameIn);
                //listStack.Sort();
                isExecutingStack = true;
                for (int i = 0; i <= 10000; i++) //~while loop, prevent infinite looping
                {
                    if (i == 10000)
                    {
                        isExecutingStack = false;
                        BatDebug.Error(typeof(ColliderWatcherManager), "455yjunghr32r4t5h", "Infinite Loop");
                    }

                    if (listStack.Count > 0)
                    {
                        //Debug.Logz("ExecuteAndRemoveFromStack: " + listStack[0].colliderWatcher.gameObject.NameHierarchy());
                        listStack[0].ExecuteAndRemoveFromStack();
                    }
                    else
                    {
                        isExecutingStack = false;
                        break;
                    }
                }
            }
        }
    }

   

}
4 Likes

Does anyone know if Unreal has these problems? It’s becoming daily more apparent to me that Unity is very poorly structured and lacks very basic things that would prevent performance hits. This is really a no brainer - a destroyed object should trigger all exits as it has exited. Instead we are expected to write code to maintain lists and arrays, perform checks at least every few frames etc for something that should already be in place. Another stupid niggle to add to the list right next to rigidbody movement juddering when moving along side navmesh agents. These problems have been in place for years.

5 Likes

Also not being able to spotcheck an attack is a big problem for me.

Like if I have a dragons breath an I want to do an instant snapshot to see what a cone hits… is there a way to do this? Like SphereCast or RayCast but with a custom shaped collider like a cone or arc.

Just leave a variable true when it enters the collider, and false when it leaves the collider …

When you destroy the object, just check the state of the variable in the OnDestroy void.