GameObject.FindGameObjectsWithTag("Enemy").Length off by one for some reason?

Viewed 240

so I'm wanting to pause the game once the amount of enemies hits 0. So I'm using GameObject.FindGameObjectsWithTag("Enemy").Length to find the number of enemies. I put this in a function that's called right when the enemies are instantiated so I can see the length go to 4, as there's 4 enemies spawning. When an enemy is killed the function is called again where the length is printed to console again. For some reason, on the first enemy killed the count repeats with a 4 again despite there only being 3 enemies. Once another enemy is killed it reports 3 when there's actually 2 and so on until I get to 1 when there's 0 enemies.

Here's the first snippet of code:

    public class EnemyList : MonoBehaviour
{
    public List<GameObject> weakMobs = new List<GameObject>();
    public List<GameObject> mediumMobs = new List<GameObject>();
    public List<GameObject> bossMobs = new List<GameObject>();
    public List<Transform> spawningChildren = new List<Transform>();
    public static int mobCount;

    void Start()
    {
        for (int i = 0; i < spawningChildren.Count; i++)
        {
            GameObject newWeakMob = Instantiate(weakMobs[0], spawningChildren[Random.Range(0, 4)]) as GameObject;
        }
        
        CheckMobCount();
    }
    public void CheckMobCount()
    {
        mobCount = GameObject.FindGameObjectsWithTag("Enemy").Length;
            print(mobCount);

}

The next piece of code is where the enemy is killed and the CheckMobCount() is called again.

public void TakeDamage()

{
    enemyCurrentHealth -= 25;
    enemyHealthBar.SetHealth(enemyCurrentHealth);
    if (enemyCurrentHealth == 0)
    {
        Destroy(this.gameObject);

        enemyList.CheckMobCount();
        //needs death animations
    }
}

Here's the console messages: Console of printed lengths

I'm self taught so I apologize if this is elementary. I've tried doing this several different ways and this is the closest I've been but I'm open to new ideas as well.

Thank you!!

6 Answers

As noted in this answer, the object is not actually destroyed in the current frame.

From the documentation:

The object obj is destroyed immediately after the current Update loop… Actual object destruction is always delayed until after the current Update loop, but is always done before rendering.

I also agree that using DestroyImmediate() is a bad idea.

Ultimately, your question seems to really be about pausing the game when the enemy count reaches 0, which unfortunately hasn't actually been answered yet.

In fact, you don't really need to do anything different except move the check for the enemy count to the beginning of the Update() method, and pause the game there if it's 0. Then you'll find that the component for the enemy has been destroyed at that point.

Presumably enemies are spawned before the update loop starts (i.e. before the first frame), but if not then you can use whatever logic you're already using to decide that new enemies need to be spawned, to detect the fact that you haven't spawned any yet and avoid pausing before the enemies have spawned.

Destroy is actually executed at the end of the frame. There is DestroyImmediate that is executed immidiatelly but it's not recommended to be used. What I would do is to add a field or a property to identify whether the enemy is still alive and then to check against it. Something like:

class Enemy : MonoBehaviour
{
    public bool IsAlive { get; set; } = true;
}

public class EnemyList : MonoBehaviour
{
  //...
  public void CheckMobCount()
  {
      mobCount = GameObject.FindGameObjectsWithTag("Enemy").Select(x => x.GetComponent<Enemy>()).Count(x => x.IsAlive);
      print(mobCount);

  }
}

And then:

public void TakeDamage()
{
  enemyCurrentHealth -= 25;
  enemyHealthBar.SetHealth(enemyCurrentHealth);
  if (enemyCurrentHealth == 0)
  {
      Destroy(this.gameObject);
      this.GetComponent<Enemy>().IsAlive = false;

      enemyList.CheckMobCount();
      //needs death animations
  }
}

This can be further optimized to store the Enemy somewhere and not use GetComponent every time but you get the idea.

Here you have attached your script to your enemy instances. And they are still alive when you are querying for the number of enemies left.

You should do the following:

public class Enemy: MonoBehaviour
{
       public static int EnemyCount = 0;
       
       private void Start()
       {
           EnemyCount++;
       }
       private void OnDestroy()
       {
          EnemyCount--;
       }
}

And then you can query the enemy count from anywhere but just excessing the EnemyCount by Enemy.EnemyCount.

If you want to get a more difficult example then you can check out this Game Dev tutorial: https://www.youtube.com/watch?v=LPBRLg4c5F8&t=134s

As already mentioned by others the issue is that Destroy is executed delayed.

Actual object destruction is always delayed until after the current Update loop, but is always done before rendering.

You could simply count only the GameObjects that are still alive, those for which the bool operator is true.

Does the object exist?

It will be false for objects destroyed in that same frame.

E.g. using Linq Count

using System.Linq;

....

mobCount = GameObject.FindGameObjectsWithTag("Enemy").Count(e => e);

which basically equals doing

mobCount = 0;
foreach(e in GameObject.FindGameObjectsWithTag("Enemy"))
{
    if(e) mobCount++;
}

There is no need for an additional property or Component.

I am suggesting you to use “DestroyImmediate” instead of “Destroy”,Then look at the result. I have a better idea, why not just use static variables when spawning enemies?

 void Start()
  {
    for (int i = 0; i < spawningChildren.Count; i++)
    {
        GameObject newWeakMob = Instantiate(weakMobs[0], 
        spawningChildren[Random.Range(0, 4)]) as GameObject;
        mobCount++;
    }
    
 }
  • Do not use Linq
  • Do not use DestroyImmediate (it will freeze and bug your game, probably)
  • Avoid FindGameObjectsWithTag in loops, only in initialization.
  • Track your enemies in an array or list
  • When you destroy an enemy, remove it's reference from the list
  • Use the list count/length to get the real actual number.
Related