Unity: Don’t Cache in OnTrigger

In a nutshell: steer clear of OnTrigger callbacks when it comes to managing caches.

 

By caches, I’m referring to any collection that monitors the objects within the collider. These callbacks prove unreliable, and Unity currently has no plans to address this behaviour in the immediate future.

 

There’s no assurance that you’ll consistently receive an OnTriggerExit call for every OnTriggerEnter. Moreover, objects may trigger multiple OnTriggerEnters before initiating an OnTriggerExit. The peculiar behaviour often arises when objects are disabled or enabled within the collider. Disabling an object won’t trigger an OnTriggerExit, but enabling it will result in an additional OnTriggerEnter. Additionally, fast-moving objects sometimes fail to trigger correctly.

 

If you still need to track objects, you can use the following script. While it may not solve all problems, it does provide a solution for handling objects that are enabled or disabled within the collider.

 

	using System.Collections.Generic;
	using UnityEngine;
	using UnityEngine.Pool;

	/// <summary>
	/// This component extends Unity's default trigger callbacks by also calling
	/// TriggerExit for game objects and colliders that were disabled while
	/// inside the trigger. Additionally, it calls TriggerExit for all contained
	/// colliders when the component itself is disabled.
	/// </summary>
	public class ReliableTrigger : MonoBehaviour
	{
		public event System.Action<Collider> TriggerEnter;
		public event System.Action<Collider> TriggerExit;

		private readonly HashSet<Collider> enteredColliders = new HashSet<Collider>();

		private void OnTriggerEnter(Collider other)
		{
			enteredColliders.Add(other);
			TriggerEnter?.Invoke(other);
		}

		private void OnTriggerExit(Collider other)
		{
			enteredColliders.Remove(other);
			TriggerExit?.Invoke(other);
		}

		private void Update()
		{
			// Note: Not sure if it's necessary to strip null objects from the
			// hash set to prevent it from growing. Objects are not destroyed often.
			List<Collider> collidersToExit = ListPool<Collider>.Get();
			foreach (var collider in enteredColliders)
			{
				if (collider == null)
					continue;

				if (collider.enabled == false || collider.gameObject.activeInHierarchy == false)
					collidersToExit.Add(collider);
			}

			foreach (var collider in collidersToExit)
				OnTriggerExit(collider);

			ListPool<Collider>.Release(collidersToExit);
		}

		private void OnDisable()
		{
			List<Collider> collidersToExit = ListPool<Collider>.Get();
			collidersToExit.AddRange(enteredColliders);
			enteredColliders.Clear();

			foreach (var collider in collidersToExit)
			{
				if (collider != null)
					OnTriggerExit(collider);
			}
			ListPool<Collider>.Release(collidersToExit);
		}
	}