Question

How can I add and subtract to a variable from multiple game objects without losing the accuracy?

I have more than 100(close to 200) enemy game objects in my scene and 5-6 being spawned every second. I'm using object pooling and the objects are not being instantiated every time but only when needed. I'm trying to record the stats where I can show total enemies remaining and total enemies taken down. A simple mathematical function of addition and subtraction from respective game objects is not adding up to what it should be. For example: Total enemies spawned 200 (when I didn't kill a single one) I kill a lot of enemies in an instant but when I'm done killing them all, enemies remaining is not always zero.

I tried killing the enemies one by one as they spawn and the addition and subtraction adds up correctly.

So how can I add and subtract to a variable from multiple game objects without loosing the accuracy?

    public void TookDownEnemy(int enemiesTookDown)
    {
        //enemiesAlive -= enemiesTookDown;
        enemiesTakenDown += enemiesTookDown;

        Debug.Log("stats, enemies took down: " + enemiesTakenDown);
        UpdateStats();

    }

    public void SpawnedNewEnemy(int spawned)
    {
        enemiesAlive += spawned;
        UpdateStats();


    }

    public void UpdateStats()
    {
        enemiesTakenDownText.text = ": " + enemiesTakenDown.ToString();
        enemiesAliveText.text = ": " + (enemiesAlive - enemiesTakenDown).ToString();
    }

I'm calling SpawnedNewEnemy from a an enemy spawning game object everytime it spawns a new enemy, TookDownEnemy gets called from the gameobject when they are spawned.

    public void InstantiateNewEnemy()
    {

        StatsManager.Instance.SpawnedNewEnemy(1);

        Enemy instance = ObjectPooler.DequeueObject<Enemy>("Enemy");

        instance.Initialize(10.0f); // get this hp value from the experience manager

        instance.SetThisAlive();

        instance.gameObject.SetActive(true);

        instance.transform.SetParent(enemiesHolder);

        int randomIndex = Random.Range(0, enemySpawnPoints.childCount);

        instance.transform.position = enemySpawnPoints.GetChild(randomIndex).position;

        instance.transform.rotation = Quaternion.identity;
    }

In a similar way when enemy is killed or dies

if(hp <= 0 && !isDead)
{
    //Destroy(this.gameObject); // use pooling here
    isDead = true;

    StatsManager.Instance.TookDownEnemy(1);

    this.GetComponent<Animator>().Play("playDead");

    Invoke("EnqueueGameObject", 0.25f);

}

Instead of directly adding and subtracting from a variable I made three different variables.

  1. enemiesSpawned
  2. enemiesTookDown
  3. enemiesRemaining

where enemiesRemaining = enemiesSpawned - enemiesTookDown

Even this is having a discrepancy. I'm assuming there should be a way to queue the addition and subtraction operation rather than just doing it.

UPDATE: I tried doing it by queuing the addition and subtraction operations and execute those operations at a defined interval.

   public void TookDownEnemy(int enemiesTookDown)
   {
       enemiesTookDownQueue.Enqueue(enemiesTookDown);

   }

   public void SpawnedNewEnemy(int spawned)
   {
       enemiesAliveQueue.Enqueue(spawned);

   }

Now I've also limited the spawning of enemies to 260(based on time I'm spawning a total of 260 enemies). When using Queue to add and subtract I'm sumhow(pun intended) always ending up with a difference of 80. Where 80 enemies remaining and I took down 180.

Here's how I'm adding those numbers up every 2 second.

   public void UpdateStats()
   {

       while(enemiesAliveQueue.Count > 0)
       {
           enemiesAlive += (int)enemiesAliveQueue.Dequeue();
       }

       while(enemiesTookDownQueue.Count > 0)
       {
           enemiesTakenDown += (int)enemiesTookDownQueue.Dequeue();
       }


       enemiesTakenDownText.text = ": " + enemiesTakenDown.ToString();
       coinsCollectedText.text = ": " + coinsCollected.ToString();
       enemiesAliveText.text = ": " + (enemiesAlive - enemiesTakenDown).ToString();
   }
 2  69  2
1 Jan 1970

Solution

 0

So I did manage to solve the issue but still don't know why the inconsistency was being caused.

Previously I was using my own implementation of Object Pool by following a YouTube video in which a generic class was created to create pools which can then be accessed as needed. It works as expected but it is more of a C# solution than a Unity solution to object pooling.

public static void EnqueueObject<T>(T item, string name) where T : Component
{
    if (!item.gameObject.activeSelf)
    {
        return;
    }

    item.transform.position = Vector2.zero;

    poolDictionary[name].Enqueue(item);

    Debug.Log("item.GetType(): " + item.GetType());

    item.gameObject.SetActive(false);

}

public static T DequeueObject<T>(string key) where T : Component
{
    if (poolDictionary[key].TryDequeue(out var item))
    {
        return (T)item;
    }

    return (T)EnqueueNewInstance(poolLookup[key], key);
    //return (T)poolDictionary[key].Dequeue();
}

public static T EnqueueNewInstance<T>(T item, string key) where T : Component
{
    T newInstance = Object.Instantiate(item);
    newInstance.gameObject.SetActive(false);
    newInstance.transform.position = Vector2.zero;
    poolDictionary[key].Enqueue(newInstance);
    return newInstance;
}

Turns out the right way to pool objects in Unity is to use UnityEngine.Pool instead. I followed this tutorial which was essentially doing the same thing but this time I don't have to worry about the queue.

I added these to my EnemySpawner.cs,

private void Start()
{
    _enemyPool = new ObjectPool<Enemy>(() =>
    {
        return Instantiate(enemyPrefab);
    }, enemy =>
    {
        enemy.gameObject.SetActive(true);
    }, enemy =>
    {
        enemy.gameObject.SetActive(false);
    }, enemy =>
    {
        Destroy(enemy.gameObject);
    }, false, 10, 20);

}

public void KillEnemy(Enemy enemy)
{
    _enemyPool.Release(enemy);
}

public void InstantiateNewEnemy()
{

    StatsManager.Instance.SpawnedNewEnemy(1);

    Enemy instance = _enemyPool.Get();

    instance.Initialize(10.0f); // get this hp value from the experience manager

    instance.SetThisAlive();

    instance.gameObject.SetActive(true);

    instance.transform.SetParent(enemiesHolder);

    int randomIndex = Random.Range(0, enemySpawnPoints.childCount);

    instance.transform.position = enemySpawnPoints.GetChild(randomIndex).position;

    instance.transform.rotation = Quaternion.identity;
}

Then when an enemy is killed

public void KillMyself()
{
    StatsManager.Instance.TookDownEnemy(1);
    EnemySpawner.Instance.KillEnemy(this);
}

(originally posted as an edit to the question by OP, rather than as an answer)

2024-07-25
ipodtouch0218