How to allow only one object in a trigger at a time?
I have the following scenario (Diagram 1); the grey circles are AI agents and the green squares are trigger boxes. Diagram 2 is a zoomed in version of Diagram 1. Each trigger has the same tag, so an agent will look for the closest trigger to them and move towards it. When an agent's transform reaches the center of the trigger they start to earn points, and after a random amount of time they move on.
When an agent has started to earn points I'd like to change the trigger's tag to "occupied" so that other agents don't try to move to this trigger, and instead look for the closest "vacant" one. The problem is the timing. I cannot change the tag OnTriggerEnter because the agent inside is still moving towards the center of the trigger, and therefore changing the tag would make it think the trigger is occupied and it would start looking for another trigger. But at the same time, not changing it allows other agents to continue to enter the trigger.
I have tried to change the tag during OnTriggerStay, as soon as the agent starts earning points. And when the agent leaves (OnTriggerExit), the tag is set back to vacant so that other agents can use it. The trouble here is that if there are multiple agents entering the trigger at around the same time, the trigger has no way of knowing the difference between them. For example, Agent 1 enters the trigger, but just before they reach the trigger's center, Agent 2 enters the trigger. At this point there's two agents in the trigger, but the trigger doesn't know that. So Agent 1 could trigger OnTriggerEnter, while Agent 2 can then trigger OnTriggerStay, skipping OnTriggerEnter.
And that is the basis of the issue. I would like to be able to have OnTriggerEnter, Stay and Exit triggered by only one agent at a time. How could I do this?
Here is my script:
using UnityEngine;
using System.Collections;
public class AICover : MonoBehaviour {
public string enemyTag;
public string coverFreeTag;
public string coverNotFreeTag;
public string uniqueID;
private EnemyUniqueID enemyUniqueID;
public bool almostNotFree = false;
public Transform coverTransform;
private NavMeshAgent enemyNavMeshAgentScript;
private float enemyNavMeshAgentOrigRadius;
// Use this for initialization
void Start () {
coverTransform = transform;
coverFreeTag = coverTransform.gameObject.tag;
}
// Update is called once per frame
void Update () {
}
void OnTriggerEnter (Collider other) {
if (other.CompareTag (enemyTag)) {
enemyNavMeshAgentScript = other.GetComponent<NavMeshAgent> ();
enemyNavMeshAgentOrigRadius = enemyNavMeshAgentScript.radius;
enemyNavMeshAgentScript.radius = 0.0001f;
if (coverTransform.gameObject.tag == coverFreeTag && !almostNotFree) {
uniqueID = System.DateTime.Now.ToString ("ddMMyyyyHHmmss") + Random.Range (1, 1000).ToString ();
enemyUniqueID = other.GetComponent<EnemyUniqueID> ();
enemyUniqueID.enemyID = uniqueID;
almostNotFree = true;
}
}
}
void OnTriggerStay (Collider other) {
if (other.CompareTag (enemyTag) && coverTransform.gameObject.tag == coverFreeTag && almostNotFree && uniqueID == enemyUniqueID.enemyID) {
coverTransform.gameObject.tag = coverNotFreeTag;
}
}
void OnTriggerExit (Collider other) {
if (other.CompareTag(enemyTag)) {
enemyUniqueID = other.GetComponent<EnemyUniqueID>();
enemyNavMeshAgentScript.radius = enemyNavMeshAgentOrigRadius;
if (coverTransform.gameObject.tag == coverNotFreeTag && almostNotFree && uniqueID == enemyUniqueID.enemyID) {
coverTransform.gameObject.tag = coverFreeTag;
almostNotFree = false;
}
}
}
}
Answer by DiegoSLTS · Jun 20, 2016 at 01:18 PM
I'd do something like this:
An agent looks for the closest non-ocuppied trigger
The agent starts moving towards it
On each update it checks if the trigger has become occupied by other. If it has, goes back to step 1
If it enters a trigger, store a reference in that object to the agent that entered, this would be the "occupied" flag.
When it finished earning points, clear the object reference so other AI agents can move there even if the current one hasn't leave the trigger
So, when an agent checks if a square is occupied, it actually check if the reference is not null AND if it's no a reference to itself. In the case where two agents enter the same square, only one will make it as the reference, so the other one will move away form it.
Yes I think what I need to do is only go through TriggerStay if a reference has already been stored. I've tried to do this by cross checking the unique id saved both in the trigger and the agent that has entered the trigger. The trouble is that somehow after the first agent enters and is referenced, and a bool is set to stop checking for agents, other agents still manage to get through and are referenced, overwriting the first one. And then the first agent doesn't get the rest of the trigger script working on it, since it is working on the other. So there must be a "hole" somewhere in the conditionals through which the second, third etc agents are getting through.
I think you're overcomplicating this. All those tags and 2 uniqueIDs (of different types) seems like too much.
Also, I'm not sure, but it looks like you're trying to solve this only with a script on the triggers. I'd suggest you do something on the enemies, so they check if a trigger is ocuppied, not the trigger itself when things enter and leave.
$$anonymous$$aybe your game is more complex but from what I understand something simple as this should work:
On the trigger (AICover, right?):
Just use one GameObject variable to store the enemy that will use it.
Update that variable in OnTriggerEnter only if it's null and only if the target for that enemy is this trigger.
Set it to null again when the enemy moves away from it. Add a method that will be called by an enemy when it decided to leave, so it can be called independant of it being entering, inside, or leaving the trigger.
On the agent (I guess it also has some script attached, otherwise add one):
Pick an empty AICover target
On every update check if the target is still empty. The check would look something like:
if (target.Enemy != null && target.Enemy != gameObject)
If it's not empty, pick another empty target, otherwise move towards it.
Once the enemy enters the target it'll start being the "enemy" reference on that AICover and any other agent moving towards it will pick a different one. This way the enemy can still move to the center. Any enemy that pass through a trigger that's not it's target will move as if it isn't there.
When the AI decides to move from the cover, call the method to free the cover, setting the "enemy" reference to null again so others can pick it as their target.
I assume you already have some code to pick an AICover and some code that decides when to leave a cover position.
Yes you're right I do have conditionals which are probably redundant, the trouble is that removing some of them prevents more than one agent entering the trigger. I have added an additional check to the enemy AI for moving away from occupied triggers. The trouble is that when you have multiple agents, even just two, moving at about the same speed, when they enter the trigger their agent radius prevents either of them from reaching the triggers center. They both push against each other just in front of the center but neither can actually reach it. I have tried to solve this by reducing the navmesh agent radius when inside the trigger, and resetting it when outside the trigger. But this of course allows agents to go straight through each other. Which is another problem.
I think that for all intents and purposes your answer is correct, I just cannot get the trigger and agent ai to work together, so probably even more debugging is needed.
Answer by glitchers · Jun 20, 2016 at 01:13 PM
Instead of changing tags on your cover objects use variables in your cover class. You can create a method to determine if an agent can move towards that as their target by returning if it is occupied.
Additionally for your uniqueID you can use System.Guid.NewGuid().ToString()
(read more here)
Yes definitely, the agent does have a state for finding a vacant trigger and setting the target as appropriate. The trouble here is that the trigger itself has a difficult time keeping track of multiple agents that are inside it. As per the script above, the trigger goes through a number of checks to try to isolate each enemy that enters it i.e. first tag, then its seek target (not included in the code above for clarity), and then its unique id. In order for the logic to work correctly, the trigger needs to apply the script to the agent in the correct order i.e (1) TriggerEnter, (2) TriggerStay, (3) TriggerExit. If any of these are skipped the code will not work correctly. But what can happen is that the first agent can trigger the OnTriggerEnter, and a second agent can trigger OnTriggerStay since to the trigger the first and second agent might as well be the same. It has no way of differentiating them at this point. I think I may need to change the layer of the trigger in TriggerEnter to avoid the second, third etc agent triggering TriggerStay, TriggerExit etc. Thank you for the NewGuid suggestion, I did not know about this.