1
\$\begingroup\$

I am working on my first Tower Defense game and I have a problem where multiple enemies get hit by a projectile instead of one.

Enemy with its collider:

Enemy

The problem is that when the enemies get close to each other, their colliders overlap and one arrow deals damage to multiple units.

Arrow and Enemy both have trigger colliders 2D.

Code for Arrow.cs component:

    private void OnTriggerEnter2D(Collider2D collision) {
    Enemy enemy = collision.GetComponent<Enemy>();

         if (enemy != null) {
             IDamagable damagable = enemy.GetComponent<IDamagable>();
             if (damagable != null) {
                 damagable.ReceiveDamage(DamageType.PHYSICAL, attackType, damage);
             } else {
                 Debug.Log("Unit is missing the IDamagable script");
             }
             Destroy(gameObject);
        }
    }

Seems like collision detection and OnTriggerEnter2D is called in the same step and this function runs multiple times even if the game object is destroyed.

\$\endgroup\$
6
  • 1
    \$\begingroup\$ How I would handle it: Do not let physics take care of the problem. The arrow is just the visual representation. If it is too much recoding, your IDamagble could include a 4th parameter: target. only if the target matches with what the tower wanted to hit will damage occur/ arrow be destroyed \$\endgroup\$
    – Zibelas
    Commented Nov 9, 2022 at 7:49
  • \$\begingroup\$ I'm really not sure, but one thing I would try is to check inside the callback if this==null. Seems absurd, but that is just checking if the object has been marked as destroyed. The thing is that MonoBehaviours don't immediately stop existing when you call Destroy(), so it's possible that multiple callbacks that are happening on the same physics step have already been queued up by the time you destroy the object, and they all end up executing anyways. \$\endgroup\$
    – PepeOjeda
    Commented Nov 9, 2022 at 7:53
  • \$\begingroup\$ @Zibelas thanks! The approach that you suggested worked, I just added this check inside if (enemy != null) \$\endgroup\$ Commented Nov 9, 2022 at 7:56
  • 1
    \$\begingroup\$ @TarasFityo you can post an answer yourself with the code that did the trick for you. Keep in mind you should assign a max life time to your arrows, in case enemy gets destroyed by a second tower beforehand. Or a workaround is to partially destroy it (hide the visuals and what would damage the main target and let the arrow hit the now invisible arrow) \$\endgroup\$
    – Zibelas
    Commented Nov 9, 2022 at 8:20
  • 1
    \$\begingroup\$ You can totally keep the colliders. What I meant with removing physics was for using it as the main logic of how the arrow works (but even that depends on the exact type of tower defense game, in one where you would have arced shots, unaimed shots or whatever, physics might be the easier way). Leaving the collider for detecting if arrow reached enemy is ok and using for sticking the arrow there. \$\endgroup\$
    – Zibelas
    Commented Nov 9, 2022 at 10:54

2 Answers 2

2
\$\begingroup\$

As I have target set during initialization

private GameObject targetEnemy;
private int damage;
private AttackType attackType;

public void Initialize(GameObject targetEnemy, int damage, AttackType attackType) {
    this.targetEnemy = targetEnemy;
    this.damage = damage;
    this.attackType = attackType;
}

I just had to add additional check if collision happened for taget enemy, not some enemy that was close to he target.

    private void OnTriggerEnter2D(Collider2D collision) {
    Enemy enemy = collision.GetComponent<Enemy>();

    if (enemy != null && targetEnemy == collision.gameObject) {
        ...receive damage
    }
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Little optimization: I would start with the check targetEnemy == collision.gameObject and only then obtain the enemy component. Checking two references for equality is cheaper than acquiring a component via GetComponent. And there is no need to acquire that component anyway when you already know that it's not the target you are looking for. And considering that you are apparently not doing anything with the Enemy component and only with the IDamageable, you might not even need to check for that component. \$\endgroup\$
    – Philipp
    Commented Nov 9, 2022 at 11:02
  • \$\begingroup\$ thanks @Philipp \$\endgroup\$ Commented Nov 10, 2022 at 9:40
1
\$\begingroup\$

I disagree with the suggestion to keep track of the target. What you can do instead is have some bookkeeping in the projectile about whether it already hit a target or not (a boolean field should do the job). And if it already has, then you can just short-circuit in the collision code and not apply damage.

So something like this:

private bool hasHitAnEnemy = false;

private void OnTriggerEnter2D(Collider2D collision) {
    if(hasHitAnEnemy) { 
        return;
    }

    Enemy enemy = collision.GetComponent<Enemy>();

    if (enemy != null) {
         IDamagable damagable = enemy.GetComponent<IDamagable>();
         if (damagable != null) {
             damagable.ReceiveDamage(DamageType.PHYSICAL, attackType, damage);
             hasHitAnEnemy = true;
         } else {
             Debug.Log("Unit is missing the IDamagable script");
         }
         Destroy(gameObject);
    }
}
\$\endgroup\$
6
  • 1
    \$\begingroup\$ One trouble with this is that you don't have a guarantee of the order the OnTriggerEnter2D messages get fired. The enemy on the "far" side could get hit "first" if the projectile ends up overlapping both in the same physics step. \$\endgroup\$
    – DMGregory
    Commented Nov 9, 2022 at 15:00
  • \$\begingroup\$ Hi S. Tarık Çetin, thanks for your suggestion. DMGregory is correct, I think that just simply checking if target is the one that was hit should be enough. \$\endgroup\$ Commented Nov 10, 2022 at 9:39
  • \$\begingroup\$ @DMGregory Why would that matter? If it hits, it hits. \$\endgroup\$ Commented Nov 10, 2022 at 9:46
  • \$\begingroup\$ It looks strange if I shoot arrows at a group of enemies and the one at the back, farthest from my shooting position, is the one that dies. (This can also happen with the target-based check). The way to fix this would be a raycast or shape cast each physics step, to ensure the first target hit is the one that takes the damage. \$\endgroup\$
    – DMGregory
    Commented Nov 10, 2022 at 12:13
  • \$\begingroup\$ @DMGregory I still don't understand your point. In order for an enemy to take damage, it needs to be hit. In order for it to be hit, it needs to be touching the projectile. It doesn't matter where its centre point is. Nothing would look weird in that case. \$\endgroup\$ Commented Nov 10, 2022 at 12:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .