Search Unity

Object pooling in a tower defense game

Discussion in 'Editor & General Support' started by Gileno, Mar 17, 2021.

  1. Gileno

    Gileno

    Joined:
    Nov 23, 2018
    Posts:
    29
    Hello,

    I did the https://learn.unity.com/tutorial/object-pooling# tutorial to improve the performance of my tower defense game.

    It worked well but my game is a little different from the course game, there are several objects that shoot(the turrests), and these objects can be created or sold.

    And when I sell one of the towers there are several inactive objects in the hierarchy.

    This is the turret script (bullets part)

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class Turret : MonoBehaviour
    6. {
    7. // Object Pooling
    8.     [SerializeField] private int pooledAmount = 0;
    9.     List<GameObject> bulletsList;
    10.  
    11.     void Start()
    12.     {                    
    13.         // Object Pooling
    14.         bulletsList = new List<GameObject>();
    15.         for (int i = 0; i < pooledAmount; i++)
    16.         {
    17.             GameObject obj = (GameObject)Instantiate(bulletPrefab);
    18.             obj.SetActive(false);
    19.             bulletsList.Add(obj);
    20.         }
    21.     }
    22.  
    23.     void Shoot()
    24.     {
    25.             for (int i = 0; i < bulletsList.Count; i++)
    26.             {
    27.                 if(!bulletsList[i].activeInHierarchy)
    28.                 {
    29.                     bulletsList[i].transform.position = firePoint.transform.position;
    30.                     bulletsList[i].transform.rotation = firePoint.transform.rotation;
    31.                     bulletsList[i].SetActive(true);
    32.                     if (bulletsList[i] != null)
    33.                     {
    34.                         Bullet bullet = bulletsList[i].GetComponent<Bullet>();
    35.                         bullet.Seek(target);
    36.                     }
    37.                     break;
    38.                 }
    39.             }      
    40.         }
    41.     }
    42. }

    This is the bullet script (object poolinh part)

    Code (CSharp):
    1. using System.Collections;
    2. using UnityEngine;
    3.  
    4. public class Bullet : MonoBehaviour
    5. {
    6.     void HitTarget()
    7.     {
    8.         GameObject effectIns = (GameObject)Instantiate(impactEffect, transform.position, transform.rotation);
    9.         Destroy(effectIns, 1f);
    10.              
    11.         if (explosionRadius > 0)
    12.         {
    13.             Explode();
    14.         }
    15.         else
    16.         {
    17.             Damage(target);
    18.         }
    19.  
    20.         gameObject.SetActive(false);
    21.     }
    22.  
    23. }

    This is NodeScript (part of the sale of the turrets)

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.EventSystems;
    3.  
    4. public class Node : MonoBehaviour
    5. {
    6.  
    7.     public void SellTurret()
    8.     {      
    9.         //GameObject effect = (GameObject)Instantiate(buildManager.sellEffect, GetBuildPosition(), Quaternion.identity);
    10.         //Destroy(effect, 2f);
    11.              
    12.         Destroy(turret);
    13.      
    14.     }
    15.  
    16. }
    This are SS from my unity screen with 5 turrets created. There you can see the number of inactive objects referring to the bullets of each turret, and everything is ok. It works fine if I don't sell any turrets.



    Now if I sell these turrets and create other turrets, the inactive objects of the sold turrets (destroyed) will remain there, the objects of the new turrets are also there. This can cause many objects at the end of the game, reducing game performance.





    My question is: Is there a way to remove these inactive objects from these sold turrets that are in the Hierarchy?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Of course... you just Destroy() them.

    When you play with object pooling, you basically become the "Inventory Keeper" of all those objects, and you now have to write extra code to do your bidding.

    This code has to be meticulously correct from a bookkeeping standpoint or else you get issues like what you see above.

    For 99% of games, object pooling is unlikely to make a visible performance difference.

    Object pooling can also have a high cost as far as restructuring the lifetime of objects within your game.

    It also greatly increases your engineering burden as far as tracking those lifetimes and reasoning about quantities and excesses and everything else associated such that the "inventory keeper" job continues to be done appropriately, even as your game grows and expands going forward.

    https://forum.unity.com/threads/object-pooling.1329729/#post-8405055
     
    Last edited: Oct 12, 2022
    Joe-Censored likes this.
  3. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    Yeah after doing some object pooling myself, I wouldn't go that route again unless one of the following statements were true

    1) My game can't tolerate any GC
    2) I'm creating/destroying these objects at an extremely high rate
    3) I'm seeing a non-trivial performance hit with each instantiation of this prefab
     
    Kurt-Dekker and Bunny83 like this.
  4. Gileno

    Gileno

    Joined:
    Nov 23, 2018
    Posts:
    29
    "2) I'm creating/destroying these objects at an extremely high rate"

    How much is an extremely high rate?

    In my game in a normal situation, close to the last waves there are at least 30 turrets shooting at enemies.

    The firing rate of the turrets is variable, there are turrets that fire every 4 seconds (rocket turret) and this is the slowest of the turrets. On the other hand, it has a turret that shoots 5 bullets per second (machine gun).

    I think for a mobile game it is a high rate of objects being instantiated and destroyed every second.

    But I really don't know if it is better to continue in the traditional way by instantiating and destroying bullets or in this new way with object pooling.

    This part of having to make a meticulously correct code scared me a little.

    I wanted to make sure it was worth trying to make this extra code.
     
    Nad_B likes this.
  5. Gileno

    Gileno

    Joined:
    Nov 23, 2018
    Posts:
    29
    Nad_B, Bunny83 and Kurt-Dekker like this.
  6. Sluggy

    Sluggy

    Joined:
    Nov 27, 2012
    Posts:
    989
    Yeah, this sounds like the kind of situation you'd want to use pools for. There are problems that occur in Unity as the result of its Garbage Collector outside of simply just getting long hiccups at regular intervals. It's non-generational and non-compacting so if you are making many frequent allocations you will chew up many times more memory and take longer to perform allocations due to memory fragmentation. That being said 'overpooling' is a thing too. If you have too many objects in your pool then that also increases garbage collection times because, again, Unity's GC isn't generational so it has to walk through all of those objects even if it doesn't make sense to. I kinda wish Unity had a way to flag objects as 'GC ignore-able' or something to get around this issue.

    As far as the 'meticulously correct' thing goes - if people are willing to write software in C++ (what I could consider to be a far more dangerous thing than using pooling) then I see no reason why being correct should scare you off from using a technique when warranted. You shouldn't be afraid of doing work because it requires you to do it correctly. No offense to Kurt. He gives a lot of really great info, and he's not entirely off-base with his opinion - a lot of simple games really don't need it. but in cases like what you have it definitely is something you should be using to some degree unless you are using ECS extensively.
     
    Kurt-Dekker likes this.
  7. FramboCeddy

    FramboCeddy

    Joined:
    Feb 3, 2024
    Posts:
    1
    Nobody has really adressed the core of the question in this post so I would like to suggest a solution:

    When creating the bullets in your Turret Start method, make them children of your turret gameobject. This will cause you to destroy all the bullets when a turret is destroyed. This can easily be done on line 17 in your Turret class where the Instantiate for the bullet is.

    The Unity documentation for the Instantiate method has several overloads, some of which take in a Transform parent argument: https://docs.unity3d.com/ScriptReference/Object.Instantiate.html

    This is how the Start() method in your Turret class would look like
    Code (CSharp):
    1.  
    2. void Start()
    3. {                  
    4.     // Object Pooling
    5.     bulletsList = new List<GameObject>();
    6.     for (int i = 0; i < pooledAmount; i++)
    7.     {
    8.         // Note the extra transform causing this gameobject to become the parent of the bullet.
    9.         GameObject obj = (GameObject)Instantiate(bulletPrefab, transform);
    10.         obj.SetActive(false);
    11.         bulletsList.Add(obj);
    12.     }
    13. }
     
  8. Delande

    Delande

    Joined:
    Feb 19, 2022
    Posts:
    2
    Any particular reason you want to create individual pools for each tower? Why not create a generic pool manager and add your projectiles to a global projectile pool that every tower can pull from. That way, you're not instantiating x amount of bullets per tower in individual pools, and don't have to worry about what happens to your pooled objects once you sell the tower.

    One way to implement this would be by creating a pool manager such as this:

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Linq;
    3. using UnityEngine;
    4.  
    5. public class PoolManager : MonoBehaviour
    6. {
    7.     [System.Serializable]
    8.     public struct Pool
    9.     {
    10.         public string tag;
    11.         public GameObject prefab;
    12.         public Transform poolHolder;
    13.     }
    14.     public static PoolManager Instance;
    15.  
    16.     public List<Pool> pools;
    17.     public Dictionary<string, Queue<GameObject>> poolDictionary = new Dictionary<string, Queue<GameObject>>();
    18.  
    19.     private void Awake()
    20.     {
    21.         Instance = this;
    22.     }
    23.     private void Start()
    24.     {
    25.         foreach (Pool pool in pools)
    26.         {
    27.             Queue<GameObject> objectPool = new Queue<GameObject>();
    28.  
    29.             poolDictionary.Add(pool.tag, objectPool);
    30.         }
    31.     }
    32.  
    33.     public GameObject GrabFromPool(string tag)
    34.     {
    35.         if (!poolDictionary.ContainsKey(tag)) throw new System.Exception("Invalid tag passed to pool!");
    36.         if (poolDictionary[tag].Count == 0)
    37.         {
    38.             Pool pool = pools.First(i => i.tag == tag);
    39.  
    40.             GameObject obj = Instantiate(pool.prefab, pool.poolHolder);
    41.             obj.SetActive(false);
    42.             return obj;
    43.         }
    44.  
    45.         return poolDictionary[tag].Dequeue();
    46.     }
    47.  
    48.     public void ReturnToPool(string tag, GameObject obj)
    49.     {
    50.         obj.SetActive(false);
    51.         poolDictionary[tag].Enqueue(obj);
    52.         obj.transform.SetParent(pools.First(i => i.tag == tag).poolHolder);
    53.     }
    54. }
    You add this to a gameobject in your scene, then you set up a pool from the list, giving it a unique tag, a reference to a gameobject prefab the pool will hold and a reference to a transform you want your inactive pool objects to be placed under.

    Then, from your towers, you "GrabFromPool(yourBulletTag)", move it to the starting location, pass along any info and let it do it's thing. When it's done, you "ReturnToPool("yourBulletTag", this.gameObject)" and it will be placed back into the pool to be used again.

    Your pool will grow dynamically as required, rather than by a chunk of bullets every time a tower is placed, and all bullets are shared and recycled between all your towers (that use that particular projectile). Every single instance of your bullet that you instantiate will be used, rather than having a ton of them sit around gathering dust, and you only instantiate what your game requires. Plus, you don't have to destroy them when a tower gets sold.

    You can use this for other things as well, like damage numbers, healthbars (assuming you use a single canvas for your healthbars, rather than dozens of worldspace ones), effects, anything that you instantiate/destroy on a regular basis.

    There are probably better ways to do this, but it works and is easy to use. If you're prone to typos though, you may want to create an enum for your tags rather than use a plain string ;)