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.
Answer by spidermancy612 · Feb 09, 2018 at 10:16 PM
In all reality, counting based on enter and exit is the best way. Is there a reason you don't want to use them?
I totally agree! He gave a couple of reasons which I$$anonymous$$HO are not good reasons:
I don't want to keep a list or a count variable because I don't need them for anything else
It doesn't matter if you don't need them anywhere else. If you need them here it's enough to have them.
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
You don't describe the "less predictable behaviour". In any case, I think it would be best to handle the cases that cause this behaviour, i.e. "if the trigger is enabled when something is already touching it, or if an object gets destroyed while inside the trigger", rather that not use Enter and Exit.
But if you have "commited" to not using Enter and Exit, then you already have a working solution... which you already know is not the best solution ;-)
Well for example, like I wrote, OnTriggerExit isn't called when an object is destroyed when inside the trigger. When I tried using only Enter and Exit I ended up including a Stay just for this purpose, which gets called every physics frame anyway. I had in front of me three OnTrigger methods which basically handled the same thing... Also, in my project, the SafeZone component is enabled only on certain occasions (it doesn't check for collisions all the time), and gets disabled after certain events are called. From the tests I've done, if you enabled a trigger Collider when something is already within its bounds you won't get an Enter, only an Exit (when the object exits). These were the main reasons for this implementation. But like I said, it feels like reinventing the wheel :)
For one thing, I don't know the functionality of your // Send a message
and // Send a message here too.
or how you would use "a list or a count variable". It really restricts the contents of my comments, and it is also the reason why I didn't respond to your question until @spidermancy612 posted his reply (I've been following your post for hours...).
But lets assume that your functionality would be served well by keeping a list of all GameObjects that are inside the trigger, say List<GameObject> gameObjectsInsideSafeZone
.
Then what you could do is this: In the script that destroys a GameObject (which could potentially be inside the safe zone) create an event that would be triggered when the GameObject gets destroyed.
You could then subscribe to this event inside the SafeZone
script and run an event handler, whenever the event gets triggered to remove the destroyed GameObject from the List: gameObjectsInsideSafeZone.Remove(destroyedGameObject)
.
So, the gameObjectsInsideSafeZone
list would be maintained with the correct GamObjects, without needing to use Stay.
Also, in my project, the SafeZone component is enabled only on certain occasions (it doesn't check for collisions all the time)
Oh, yes it does (check for collisions all the time). OnTrigger events get called even on disabled scripts:
Per the documentation:
Trigger events will be sent to disabled $$anonymous$$onoBehaviours, to allow enabling Behaviours in response to collisions
e.g. https://docs.unity3d.com/ScriptReference/Collider2D.OnTriggerEnter2D.html
if you enabled a trigger Collider when something is already within its bounds you won't get an Enter
Yes this happens, and it's reasonable that it happens like that.
You could use Collider2D.OverlapCollider
to get the colliders of the GameObjects that are already touching the safe zone collider when it gets enabled, and add them to the gameObjectsInsideSafeZone
List . I've never needed to use this, but it seem the right method to call:
https://docs.unity3d.com/ScriptReference/Collider2D.OverlapCollider.html
Thank you for your suggestions.
This would mean having to remember to add an event to each and every entity, past or future, possibly an interface, and have the SafeZone subscribe to all of them. I find this very cumbersome and it also tightly couples the colliders and the SafeZone. It can already find out which collider is around with OnTriggerEnter2D. The fact the Exit isn't called when the object is destroyed is a design decision I don't really understand."create an event that would be triggered when the GameObject gets destroyed"
I am disabling not the script, but rather the gameObject it's attached to (and on which the trigger Collider2D sits). I'm sorry if I wasn't clear. AFAI$$anonymous$$ $$anonymous$$onoBehaviours on disabled gameobjects are not called."OnTrigger events get called even on disabled scripts"
It would be one more thing to check in an already rapidly-growing script (counts, lists, event messaging, three OnTrigger functions), but thank you for mentioning it, I didn't know it existed. I am really surprised that Unity doesn't natively encapsulate something like the above with simple OnTrigger calls, I don't know the reasons for it."Collider2D.OverlapCollider"
Answer by yummy81 · Feb 10, 2018 at 01:21 PM
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;
}
}
Your answer
Follow this Question
Related Questions
Frogger Logs OnTriggerEnter2D OnTriggerExit2D 0 Answers
Moving to Next Level on collision 1 Answer
OnTriggerExit not working as expected 5 Answers
Falling Obstacle 2 Answers
Using OnPointerClick with Buttons 1 Answer