Search Unity

Efficient enemy detection with triggers

Discussion in 'Scripting' started by Tetsubo, Aug 17, 2017.

  1. Tetsubo

    Tetsubo

    Joined:
    Jun 26, 2014
    Posts:
    40
    Hey there!

    currently I am trying to find out the optimal solution for the following problem:

    in a 2d tower defense game, when enemies come near a tower (or even moving tank) the tower/slowly moving tank take a defensive stance. when there are no enemies near - tower/tank go back to normal stance.

    My initial solution was with an attached trigger to the tower/tank, which execute a code in the OnTriggerEnter function. The problem was, that when the enemies are destroyed in the trigger zone, and because OnTriggerExit was not triggered, the tower/tank still "think" there are still enemies near and remains in the defensive stance. Also there can be a lot (~20+) enemies in the trigger zone, that come in and out from the zone.

    What will be the performance impact if I attach a stance script on the enemies and not on the tower/tank? So that when the enemies detect a tower near, an OnTriggerEnter in the enemy scripts is triggered, and they call a function on the tower (which they have a reference to in advance) to inform it that "they are near". That will solve the described problem above, because in the OnDestroy() function the enemies will check if they are in the tower zone and inform the tower that "they die and go out of the zone".. The problem with performance would be, that all the enemies will call the same function (or same two functions) of the tower script: enemyComingIn() and enemyComingOut() all the time. This concurrent calls might slow the game.. And besides, these two functions will call an Animator variable of the tower to be updated.

    Is there any better solution? Any opinions and help are very welcome!
     
  2. ADNCG

    ADNCG

    Joined:
    Jun 9, 2014
    Posts:
    994
    How about let the towers and enemies be responsible for animating themselves?

    Have a class that holds a reference to all the enemies positions. Have the towers request that list every frames, iterate through the positions, calculating their distances from the tower. Break out of the loop as soon as one of them is in range, otherwise it means the tower is clear to go back to normal stance.

    Same thing, but the other way around for the enemies.

    This should be fairly quick to setup, and I'd just try it and see if it works, before worrying about performance hits.

    If anything, you could have half the towers request the list for a given frame, then the other half the next frame, and alternate. They'd call it every 2 frames and it would reduce the calculations to half.

    Edit : Alternatively, if all towers/enemies have colliders, you could use this for your detection. Same deal, towers and enemies are responsible for handling themselves. https://docs.unity3d.com/ScriptReference/Physics.OverlapSphere.html
     
    Last edited: Aug 17, 2017
  3. Tetsubo

    Tetsubo

    Joined:
    Jun 26, 2014
    Posts:
    40
    Hi, Antoine and thank you for the fast response!

    Referencing all the enemies and calculating for every enemy the distance on every frame (or actually fixedUpdate) with Vector2.Distance() would be.. the same as a trigger does, more or less, isn't that right?
     
  4. ADNCG

    ADNCG

    Joined:
    Jun 9, 2014
    Posts:
    994
    In update, not fixed update, it's not a built-in physics call.

    The same, relative to what? Performance? Detection? For performance, you'll have to try it out. Perhaps someone more knowledgeable will step in. For detection, personally I think it grants better control. You could even use the same system to figure out which target should be prioritized. Plus, like I said, you can manage the number of calls per frame.
     
  5. Tetsubo

    Tetsubo

    Joined:
    Jun 26, 2014
    Posts:
    40
    @AntoineDesbiens I mean relative to both: performance and detection.

    Also the detection could be done with raycasting to every enemy instead Distance or magnitude, as well as Physics2d.CircleCast or Physics2d.OverlapCircle.

    It suffices to know if there is a single enemy near the tower/moving tank in a given distance or none at all. Ignore targeting and reference to the enemy, I don't need that info - it is just about going to defensive or normal stance of the tower/tank.

    So, given these conditions I wonder which approach is the best one:
    - check with .Distance (tower, enemy) for every enemy if there is one near the tower and change stance if appropriately.
    - check not with Distance but with a Raycast for given max radius to every enemy, if the ray hits one
    - check with physics2d.OverlapCircle
    - check with physics2d.CircleCast. Is it more performant than OverlapCircle?
    - use the "classic" Trigger and OnTriggerEnter + OnTriggerExit

    What do you think @AntoineDesbiens ? Can't estimate which is most propriate/fastest approach. Unity profiling is something I don't know how to work with .. yet. So if there is someone that has experience with that "enemy detection efficiency problem", I would love to read his opinion!
     
  6. Tetsubo

    Tetsubo

    Joined:
    Jun 26, 2014
    Posts:
    40
    Maybe @MelvMay as Physics guru might give his opinion here also? :)
     
  7. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,455
    The best approach is the one that gives you what you need and provides the best performance. I'd suggest learning to use the profiler as nobody can really give you a hard answers to your game without profiling however, to answer what I can: Overlap queries are faster than shape cast queries like CircleCast. The exception can be line/ray cast which are pretty efficient.

    The performance of calculating distance yourself is something you'd have to determine but it sounds like it'd scale as the number of enemies/towers scales up which isn't a good thing but it depends on the numbers and the target device(s) so again, need to profile.

    An alternate method is to use the OverlapCollider which gives you a list of colliders overlapping a collider or all colliders on a Rigidbody2D. This just checks existing contacts and gives you the list of associated colliders; it doesn't perform any geometric queries so is pretty fast. Note that all this does is use GetContacts for you.
     
  8. Tetsubo

    Tetsubo

    Joined:
    Jun 26, 2014
    Posts:
    40
    Thanks for the answer @MelvMay !

    Still have some questions, which you might answer straight away, before I start to learn about the Unity profiler:
    • What about OnTriggerEnter2D / OnTriggerExit2D? Is OverlapCollider faster than that? How are actually OnTriggerEnter/Exit implemented? In case of Circle collider ~> CircleCast?
    • OverlapCollider will give me a List of overlapped colliders.. that means, if I make the detection every frame in a Coroutine, that will internally call a "new List<Collider2D>()", right? Which might be memory inefficient, right?
    • Will OverlapCollider consider the Collision Matrix from Physics Settings? So I can separate the layer needed just for this kind of collision?
    • Rigidbody2D.OverlapCollider vs Collider2D.OverlapCollider? Does it matter?
    • What I am really looking for is a function that gives me a boolean - yes/no, there are (not) any colliders overlapping. Don't even need a list. As efficient as possible. Unity doesn't provide such function yet, right?
    For info: there will be a single moving tank/tower (the player) and up to 20 simultaneously active enemies in the scene. And it's going to be a mobile game.

    Can you help with some answers to the above questions, @MelvMay ? I just don't have the internal knowledge how all the Collider detection is implemented in Unity to pick the right choice, I couldn't sleep last night thinking about that stuff..
     
    Last edited: Aug 20, 2017
  9. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,455
    I cannot give you deep detail on how they are implemented, it's simply that when a contact between two colliders starts, we perform the Enter callback and the same when the final contact is destroyed we perform the Exit callback. In theory Enter/Exit callbacks can be faster because they are events whereas performing an explicit query is polling and involves performing querying the physics database again and again but that isn't always true because the system is still checking the triggers per update.

    For starters, don't perform physics queries each frame because the results don't change until the simulation has run; unless of course you're manually updating the simulation yourself with the new calls that allow that. Beyond that, the API that allow you to pass in an array/list to be populated assume you're going to reuse the same array/list; don't throw it away to the GC after each use.

    Why not use OverlapCollider? It doesn't perform any queries, only checks existing calculated contacts and gives you the associated Colliders as I've already said.

    Alternately you can use IsTouching to give you a true/false. You can even use the ContactFilter2D to filter. If all your enemies are on a specific layer then you can use IsTouchingLayers. Obviously a true/false doesn't tell you which enemies so not sure why that would be useful but IsTouching can be used to check an enemy.

    For best performance, I'd suggest OverlapCollider because it gives you a count of how many (return value) and an array of colliders detected. It's quicker to iterate this array checking for enemies than performing lots of separate queries and/or performing work in callbacks. You can also do it when you want.

    Physics queries that use a specific shape perform the actual query to the physics database so cost more i.e. OverlapCircle, Raycast etc. If they don't use a specific shape then they are just checking existing contacts i.e. IsTouching, IsTouchingLayers, GetContacts etc.

    All these are in the API docs.
     
  10. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,455
    In short, add a trigger Collider2D to your tank/tower. Set the layers so that the Collider2D only contacts enemies. Perform an OverlapCollider on it to give you the collider of the enemies you're in contact with. This will be fast.
     
    lordelmsworth likes this.
  11. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
  12. Tetsubo

    Tetsubo

    Joined:
    Jun 26, 2014
    Posts:
    40
    Thank you @MelvMay and @Kiwasi !

    The script that @Kiwasi is pointing to is very similar to what I initially had. That adding/removing/counting elements is the overhead I wanted to remove.

    As I already described, the trigger zone is just for changing stance - defensive when enemies near, normal when no one near. There is another trigger on the tank, that picks and shoots at the targets that come much closer.

    @MelvMay : should the OverlapCollider be triggereted in an Coroutine or Update every frame? I guess FixedUpdate as you pointed out is not that good, since the simulation hasn't performed at that point of time.. Also, can I provide a null for ContactFilter2D parameter or is just an Initialized ContactFilter enough, so that the collision matrix is directly applied on the contactFilter, without further customisation of the filter?

    Again, thanks a lot for the answers and explanations so far!