UNITY OBJECT POOLING SYSTEM FOR ZERO GC ALLOCATIONS
Eliminate garbage collector hitching during combat by recycling projectiles, missile plumes, and particle explosions with Unity's native UnityEngine.Pool.
1. Why Instantiate & Destroy Cause Mobile Lag
Calling Instantiate() allocates managed heap memory and triggers internal GameObject registration in the native C++ engine. Calling Destroy() leaves behind orphaned memory in the managed heap.
When the managed heap runs out of memory, Unity's Garbage Collector pauses the entire game engine for 10ms to 40ms to reclaim dead objects. On mobile devices at 60 FPS (where the entire frame budget is only 16.6ms), a GC freeze causes an abrupt, noticeable frame stutter.
The Solution: Object Pooling pre-instantiates a collection of GameObjects at startup, disabling them when not in use (SetActive(false)) and re-enabling them when needed, generating 0 bytes of garbage collection pressure during active gameplay.
2. Production Bullet Pool using UnityEngine.Pool
In Unity 2021 and newer, Unity provides the official high-performance UnityEngine.Pool.ObjectPool<T> API:
using UnityEngine;
using UnityEngine.Pool;
public class PooledBulletSpawner : MonoBehaviour
{
[Header("Pool Configuration")]
[SerializeField] private PooledProjectile bulletPrefab;
[SerializeField] private int defaultCapacity = 20;
[SerializeField] private int maxCapacity = 100;
private IObjectPool<PooledProjectile> bulletPool;
private void Awake()
{
// Initialize the pool with constructor callbacks
bulletPool = new ObjectPool<PooledProjectile>(
createFunc: CreateBullet,
actionOnGet: OnTakeBulletFromPool,
actionOnRelease: OnReturnBulletToPool,
actionOnDestroy: OnDestroyBullet,
collectionCheck: true,
defaultCapacity: defaultCapacity,
maxSize: maxCapacity
);
}
private PooledProjectile CreateBullet()
{
PooledProjectile bullet = Instantiate(bulletPrefab);
bullet.SetPool(bulletPool); // Give bullet a reference to return itself
return bullet;
}
private void OnTakeBulletFromPool(PooledProjectile bullet)
{
bullet.gameObject.SetActive(true);
}
private void OnReturnBulletToPool(PooledProjectile bullet)
{
bullet.gameObject.SetActive(false);
}
private void OnDestroyBullet(PooledProjectile bullet)
{
Destroy(bullet.gameObject);
}
public void Shoot(Vector3 position, Quaternion rotation)
{
PooledProjectile bullet = bulletPool.Get();
bullet.transform.SetPositionAndRotation(position, rotation);
bullet.Launch();
}
}
And here is the companion projectile script that returns itself to the pool after a timeout or on collision:
using UnityEngine;
using UnityEngine.Pool;
public class PooledProjectile : MonoBehaviour
{
[SerializeField] private float speed = 45f;
[SerializeField] private float lifeTime = 3.0f;
private IObjectPool<PooledProjectile> pool;
private float timer;
public void SetPool(IObjectPool<PooledProjectile> targetPool)
{
pool = targetPool;
}
public void Launch()
{
timer = lifeTime;
}
private void Update()
{
transform.Translate(Vector3.forward * speed * Time.deltaTime);
timer -= Time.deltaTime;
if (timer <= 0f)
{
ReturnToPool();
}
}
private void OnTriggerEnter(Collider other)
{
// Apply damage if target has HealthSystem
if (other.TryGetComponent<HealthSystem>(out HealthSystem health))
{
health.TakeDamage(20f);
}
ReturnToPool();
}
private void ReturnToPool()
{
if (pool != null && gameObject.activeSelf)
{
pool.Release(this);
}
}
}
Related Mobile Optimization Guides
Frequently Asked Questions
SetActive(false) on complex GameObjects with deep child hierarchies invokes OnDisable callbacks on all attached MonoBehaviours and updates transform bounding hierarchies. For ultra-high frequency particles (e.g. 500 bullets/sec), developers sometimes keep objects active and move them off-screen or disable only their MeshRenderer and Collider components.