Overview
Shooting game bullets, enemy swarms, destruction effects... Games frequently spawn large numbers of objects in short periods and destroy them when no longer needed. Many beginners implement this straightforwardly using Instantiate() (creation) and Destroy() (destruction) methods.
However, frequent Instantiate/Destroy calls are two of Unity's biggest performance killers. These methods involve memory allocation (heap allocation) and deallocation, creating significant CPU load and triggering garbage collection (GC)—the primary cause of serious stuttering (spikes).
Object Pooling is the essential technique used in professional game development to solve this problem. This article covers the concept of object pooling and its concrete implementation.
What Is Object Pooling?
The object pooling concept is simple: "Don't discard objects once created—reuse them."
- Pre-population: At game start or other low-impact times, pre-
Instantiate()the expected number of objects (e.g., 30 bullets). Set all created objects to inactive (SetActive(false)) and store them in a "pool" (list or queue). - Get/Rent: When an object is needed (e.g., firing a bullet), instead of creating new with
Instantiate(), retrieve an inactive object from the pool. Set it active (SetActive(true)), reset position and orientation, and use it. - Return/Release: When an object is no longer needed (e.g., bullet hits enemy or leaves screen), instead of destroying with
Destroy(), set it inactive again and return it to the pool. The object then waits for its next use.
Repeating this cycle reduces Instantiate() and Destroy() calls during gameplay to zero, dramatically improving performance.
Simple Object Pooling Implementation
Let's implement a generic object pooling system.
Step 1: Create the Pool Class
First, create a class managing the object pool for a specific Prefab. We use a Queue to efficiently manage inactive objects.
// ObjectPool.cs
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
public GameObject objectToPool; // Prefab to pool
public int amountToPool; // Initial spawn count
private Queue<GameObject> pooledObjects;
void Start()
{
pooledObjects = new Queue<GameObject>();
for (int i = 0; i < amountToPool; i++)
{
GameObject obj = Instantiate(objectToPool);
obj.SetActive(false);
pooledObjects.Enqueue(obj);
}
}
// Method to get object from pool
public GameObject GetPooledObject()
{
// If available objects in pool, retrieve one
if (pooledObjects.Count > 0)
{
GameObject obj = pooledObjects.Dequeue();
obj.SetActive(true);
return obj;
}
// If pool is empty (reserves exhausted)
// Option: return null, or dynamically create new object
// return Instantiate(objectToPool); // For dynamic expansion
return null;
}
// Method to return object to pool
public void ReturnObjectToPool(GameObject obj)
{
obj.SetActive(false);
pooledObjects.Enqueue(obj);
}
}
Step 2: Implement the Class Using the Pool
Next, create a script that fires bullets using this ObjectPool.
// PlayerShooter.cs
using UnityEngine;
public class PlayerShooter : MonoBehaviour
{
public ObjectPool bulletPool; // Set bullet pool in Inspector
public Transform firePoint;
public float fireRate = 0.5f;
private float nextFireTime = 0f;
void Update()
{
if (Input.GetButton("Fire1") && Time.time >= nextFireTime)
{
nextFireTime = Time.time + fireRate;
Fire();
}
}
void Fire()
{
// Get bullet from pool
GameObject bullet = bulletPool.GetPooledObject();
if (bullet != null)
{
bullet.transform.position = firePoint.position;
bullet.transform.rotation = firePoint.rotation;
// Pass pool reference to bullet script
// so bullet can return itself to pool
Bullet bulletScript = bullet.GetComponent<Bullet>();
if (bulletScript != null)
{
bulletScript.SetPool(bulletPool);
}
}
}
}
Step 3: Implement Return-to-Pool Logic
Finally, implement the bullet returning to pool when hitting a wall or after time elapsed.
// Bullet.cs
using UnityEngine;
public class Bullet : MonoBehaviour
{
private ObjectPool myPool;
public void SetPool(ObjectPool pool)
{
myPool = pool;
}
void OnCollisionEnter(Collision collision)
{
// Return to pool on collision
if (myPool != null)
{
myPool.ReturnObjectToPool(gameObject);
}
else
{
// Fallback if pool not set
Destroy(gameObject);
}
}
}
Toward More Advanced Pooling Systems
The above is a basic implementation. Real projects often create more generic pooling managers with additional features:
- Managing multiple different Prefabs in one manager (using
Dictionary<string, Queue<GameObject>>, etc.). - Dynamically expanding the pool when it empties by creating new objects.
- Events or interfaces for calling specific initialization/cleanup processing on Get and Return.
Summary
Object pooling may seem complex at first, but its principle is simple, and the performance benefits are immense.
InstantiateandDestroyare expensive operations that cause GC.- Object pooling pre-creates objects and reuses them to avoid these calls.
- "Pre-populate," "Get," and "Return" are the three basic pooling steps.
Especially in mobile game development, object pooling isn't a "recommended technique"—it's essential. If you're experiencing performance issues, first check for places overusing Instantiate and consider implementing object pooling.