【Unity】Essential Optimization: Eliminating Instantiate and Destroy with Object Pooling

Created: 2025-12-07

Object Pooling is an essential technique for dramatically improving game performance. Learn why Instantiate and Destroy are expensive, and how to implement a pooling system that reuses objects—with diagrams and code examples.

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."

  1. 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).
  2. 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.
  3. 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.

  • Instantiate and Destroy are 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.