Using OnTriggerStay for entering, staying or exiting

I have been struggling with what was supposed to be a very simple logic problem for the last two hours. I finally managed to make it work but I wonder if there’s a more elegant solution (forgive me for the horizontal rules but for the life of me I can’t figure out how to write paragraph breaks here, double space at the end doesn’t work).


What I’m looking for is a trigger Collider2D which can report at all times if something is touching it (or inside its zone). Because any number from 0 to 10 objects might be in the zone at any given time, I can’t do it with OnTriggerEnter2D and OnTriggerExit2D, for two reasons: (1) I don’t want to keep a list or a count variable because I don’t need them for anything else (2) With Enter and Exit I get less predictable behaviour if the trigger is enabled when something is already touching it, or if an object gets destroyed while inside the trigger.


So I’m going with OnTriggerStay2D instead. But I don’t want it to continuously send a message when there’s a collider present. I just need it to report once if something is in there or not, as soon as the script is enabled and at any point after that. I also don’t need to check it every single frame (0.1 seconds is more than enough in my case). So I managed to do this with two booleans (after hours of trying to figure out the ‘if’ structure of the current and last check):


using UnityEngine;

public class SafeZone : MonoBehaviour {

	private Collider2D col;
	private bool isClearThisCheck;
	private bool isClearLastCheck;
	private float checkInterval;
	private float timeSinceLastCheck;

	void Awake()
	{
		col = GetComponent<Collider2D>();
		isClearLastCheck = true;
		checkInterval = 0.5f;
	}

	private void FixedUpdate()
	{
		if (Time.time > timeSinceLastCheck + checkInterval)
		{
			if (isClearThisCheck && !isClearLastCheck)
			{
				Debug.Log("FREE");
				// Send a message.
				isClearLastCheck = true;
			}
			isClearThisCheck = true;
			timeSinceLastCheck = Time.time;
		}
	}

	private void OnTriggerStay2D(Collider2D collision)
	{
		isClearThisCheck = false;
		if (isClearLastCheck)
		{
			Debug.Log("OCCUPIED");
			// Send a message here too.
			isClearLastCheck = false;
		}
	}
}

This works and does what it’s supposed to do. It does feel like reinventing the wheel, though, and I wonder if there’s a simpler boolean check possible (or a Unity check I’m not aware of). Also, this only works because FixedUpdate is called before the collision. If I put this in Update instead, the whole logic gets reversed and it doesn’t work. Is there any way to do this with less code?


P.S. For some reason, this code outputs free whenever I deactivate the script in the Inspector while the trigger is in the occupied state. Not a big deal, but I don’t really understand why this happens.

In all reality, counting based on enter and exit is the best way. Is there a reason you don’t want to use them?

Inspired by the idea pako suggested about the events and all this trigger stuff I wrote these two simple scripts. First, I created new empty gameobject, added to it SpriteRenderer with a white circle as a sprite and CircleCollider2D with checkbox IsTrigger set to true. Finally, I attached my Hero.cs script to it. I duplicated it a couple of times and positioned each of them randomly. And then I created new gameobject, added to it SpriteRenderer with a red circle as a sprite. Next, I added CircleCollider2D with ckeckbox IsTrigger set to true and Rigidbody2D with BodyType set to Kinematic. Then I added to it SafeZone.cs script and pressed Play. I moved all these gameobjects in the Scene View. When Hero enters the trigger area of the SafeZone then it is added to the gameObjectsInsideSafeZone list (it can be seen in the inspector since the list is public). When it exists it is removed from the list. And if you destroy Hero when it is in the trigger area of the SafeZone (by simply deleting it in the Hierarchy window) then it will also be removed. Here are two scripts. Hero.cs:

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

public class Hero : MonoBehaviour
{
	private event System.Action<Collider2D> action;
	private Collider2D col;
	
	private void Start()
	{
		action += SafeZone.Instance.Remove;
		col = GetComponent<Collider2D>();
	}
	
	private void OnDestroy()
	{
		if (action!=null)
			action(col);
		action -= SafeZone.Instance.Remove;
	}
}

SafeZone.cs:

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

public class SafeZone : MonoBehaviour
{
	public static SafeZone Instance {get;set;}
	[SerializeField]
	private List<GameObjectInsideSafeZone> gameObjectsInsideSafeZone;
	
	private void Awake()
	{
		gameObjectsInsideSafeZone = new List<GameObjectInsideSafeZone>();
		Instance = this;
	}
	
	private void OnTriggerEnter2D(Collider2D other)
	{
		gameObjectsInsideSafeZone.Add(new GameObjectInsideSafeZone(other.GetInstanceID(), other.gameObject));
	}
	
	private void OnTriggerExit2D(Collider2D other)
	{
		Remove(other);
	}
	
	public void Remove(Collider2D other)
	{
		foreach(var d in gameObjectsInsideSafeZone)
		{
			if (other.GetInstanceID() == d.id)
			{
				gameObjectsInsideSafeZone.Remove(d);
				break;
			}
		}		
	}	
}

[System.Serializable]
public struct GameObjectInsideSafeZone
{
	[SerializeField]
	public int id;
	[SerializeField]
	public GameObject go;
	public GameObjectInsideSafeZone(int id, GameObject go) : this()
	{
		this.id = id;
		this.go = go;
	}
}