I’m starting a new series of blogs where I’ll share the tips and tricks I use to optimize games for better FPS. I’ll only share the techniques that I learn myself or actually use in my projects.
Today, let’s talk about one of the biggest game optimization techniques in Unity: Object Pooling.
Problem: Instantiate and Destroy Everywhere
For reference, here’s a scenario from one of my games.
I was working on a bombing and cannon fire mechanism. Bombs drop from above at fixed intervals but from random locations, and cannons fire cannonballs at fixed intervals sequentially. I wrote a script where I assigned the bomb/cannonball prefab, instantiated it at runtime, and fired it. Once the bomb/cannonball touched anything, it would self-destruct using the Destroy method.
On PC, this worked fine — I was getting around 200 FPS. But when I built the APK and ran it on my mobile, FPS dropped to barely 30 and my phone was heating a lot.
After some research, I learned why: instantiation and destruction are among the heaviest operations you can perform at runtime. And since I was doing this every 3–5 seconds, it was tanking mobile performance.
Every time you instantiate an object Unity allocates memory for:
- The GameObject itself
- All its components
- Meshes, colliders, scripts, etc.
- Clone all component data
- Initialize references
- Register it with the physics system (if it has Rigidbody/Collider)
- Register it with a rendering system.
Every time you destroy an object using Unity it has to:
- Remove it from the Scene hierarchy
- Clean up references
- Free memory
- Unregister physics/rendering data. (This might be cheaper than instantiation however, GC spikes can happen later, which adds hidden cost.)
Solution: Reuse Instead of Recreate
There are two main ways to solve this problem.
Approach 1: Enable/Disable
- Add the bomb or cannonball object to the scene and keep it hidden.
- When needed, place it in the correct location, enable it, and fire/drop it.
- When it collides, disable it instead of destroying it.
- Repeat the process.
This works great if you only need one object at a time. For example, in my scenario, only one cannon fires at a time, so I could reuse the same cannonball.
But I plan to add multiple cannons and bomb drops later, so this approach wouldn’t scale.
Approach 2: Object Pooling
This is where pooling comes in.
In pooling, we create a “pool” of objects at the start. When we need an object, we fetch it from the pool. When we are done using the object, we return it to the pool instead of destroying it.
There are different ways to implement pooling in Unity. Let’s look at two common ones.
Example 1: Pooling with a Queue
- Create a
Queue
of the object type you want to pool. - Decide the pool size (total number of objects in the pool).
- Instantiate that many objects at startup, do the setup, disable them, and enqueue them.
- When needed, dequeue an object, enable it, and use it.
- When done, disable it and enqueue it back.
This works well for simple pooling cases. (I used this for cannonballs — I’ve attached the example below in my project code.)
Example 2: Pooling with Unity’s ObjectPool (Unity 2021+)
Unity also provides a built-in ObjectPool class. It’s more powerful and flexible.
- Create an
ObjectPool
for the type you want to pool. - In
Start
orAwake
, set up the pool. Provide callback functions for: - - Creating an object
- - Getting an object from the pool
- - Releasing an object back to the pool
- - Destroying an object
- - Setting pool parameters (capacity, max size, etc.)
- Call
Get()
when you need an object. - Call
Release()
when the object’s done.
(I used this approach for bombs — example attached below.)
A Note on Collection Check
When you create an ObjectPool
, there’s a collectionCheck
parameter. By default, it’s true.
- If
true
, Unity checks if the object you’re trying to release is already in the pool. If so, it throws an exception to prevent double releases. - If
false
, Unity skips the check. If you accidentally release the same object twice, it’ll silently add it again, which can cause bugs later when callingGet()
.
This check adds a bit of overhead. It’s O(1), but it’s still extra work. For small pools, leaving it true is fine. For large pools with frequent releases, you might want it off — but only if you’re confident in managing releases correctly.
Key Takeaway:
Instantiation and destruction are heavy operations. If you have objects that appear frequently (bullets, bombs, enemies, projectiles), don’t instantiate/destroy them every time.
Instead, reuse them with enable/disable or pooling.
- For simple cases, enable/disable works.
- For larger or frequent spawns, use pooling (Queue or Unity’s ObjectPool).
Pooling won’t make your logic faster, but it will prevent FPS drops and heating on mobile devices. In my case, using pooling fixed my performance issue — from 30fps on mobile back to smooth gameplay.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CannonHandler : MonoBehaviour { [SerializeField] GameObject cannonBallCopy; [SerializeField] GameObject cannonBallSpawnPoint; [SerializeField] float forceAmount = 500f; [SerializeField] int poolSize = 10; // total pool size Queue<GameObject> cannonBallPool = new Queue<GameObject>(); // pool to store objects void Start() { for (int i = 0; i < poolSize; i++) { // instantiate object GameObject obj = Instantiate(cannonBallCopy); // perform required operation [can vary based on your game] obj.SetActive(false); Rigidbody rb = obj.AddComponent<Rigidbody>(); rb.isKinematic = true; // Add them to the queue cannonBallPool.Enqueue(obj); } } // This method is called by another class after given interval public void FireCannon() { if (cannonBallPool.Count > 0) { // remove from the queue GameObject ball = cannonBallPool.Dequeue(); // perform required operations [can vary based on your game] ball.transform.position = cannonBallSpawnPoint.transform.position; ball.transform.rotation = Quaternion.identity; ball.transform.localScale = new Vector3(27f, 27f, 27f); ball.SetActive(true); Rigidbody rb = ball.GetComponent<Rigidbody>(); rb.isKinematic = false; rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; ball.GetComponent<CannonBallHandler>().cannonHandler = this; Vector3 localForce = new Vector3(0f, 0f, forceAmount); Vector3 worldForce = transform.TransformDirection(localForce); rb.AddForce(worldForce, ForceMode.Impulse); } } // This method is called by the cannon ball itself when it comes into contact with anything. public void ReturnToPool(GameObject obj) { // perform required operations [can vary based on your game] obj.SetActive(false); Rigidbody rb = obj.GetComponent<Rigidbody>(); rb.isKinematic = true; CannonBallHandler handler = obj.GetComponent<CannonBallHandler>(); handler.meshRenderer.enabled = true; handler.sphereCollider.enabled = true; // Add it back to the queue cannonBallPool.Enqueue(obj); } }
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Pool; using UnityEngine.UIElements; public class BombDropper : MonoBehaviour { [SerializeField] GameObject bombCopy; [SerializeField] float delayInBombing; [SerializeField] GameDataSave gameDataSave; bool isLevelCompleted = false; private ObjectPool<GameObject> bombPool; MeshFilter meshFilter; Vector3[] vertices; void Start() { // Get mesh filter meshFilter = GetComponent<MeshFilter>(); vertices = meshFilter.mesh.vertices; gameDataSave.E_LevelCompleted += SetIsLevelCompleted; // create pool and add callback functions bombPool = new ObjectPool<GameObject>( createFunc: () => Instantiate(bombCopy), actionOnGet: (obj) => { obj.SetActive(true); //perform required set of operations [can vary based on your game] var handler = obj.GetComponent<BombHandler>(); handler.ResetForPool(); handler.pool = bombPool; }, actionOnRelease: (obj) => obj.SetActive(false), // Release() is called when bomb collides with anything. actionOnDestroy: (obj) => Destroy(obj), collectionCheck: false, defaultCapacity: 10, maxSize: 100 ); StartCoroutine(DropBombs()); } private void SetIsLevelCompleted() { isLevelCompleted = true; } private void OnDisable() { gameDataSave.E_LevelCompleted -= SetIsLevelCompleted; } IEnumerator DropBombs() { while (!isLevelCompleted) { int randomVertexIndex = UnityEngine.Random.Range(0, vertices.Length); Vector3 vertexPos = transform.TransformPoint(vertices[randomVertexIndex]); GameObject bomb = bombPool.Get(); // fetch the object when needed. bomb.transform.position = vertexPos; bomb.transform.rotation = Quaternion.Euler(0f, 0f, 180f); yield return new WaitForSeconds(delayInBombing); } } }
Top comments (0)