Mr TOTO / blog
· Unity · Game Development · Performance · 4 min read

Object Pool Pattern for a Mobile Game

Reuse expensive game objects instead of constantly creating and destroying them—a practical Unity example from a mobile prototype.

A Unity mobile game prototype with birds moving across the screen
The prototype repeatedly spawned birds on one side of the screen and removed them on the other.

Historical note: this example was written for Unity 4. Modern Unity versions have different APIs and include their own pooling utilities, but the trade-off behind the pattern remains useful.

The idea behind the Object Pool pattern is simple. Some systems repeatedly create and destroy many objects of the same type. When creation is expensive, keeping a set of inactive objects and reusing them can cost less than allocating new ones every time.

This matters on resource-constrained devices such as phones and tablets, although pooling is not automatically an optimisation. Retaining too many objects consumes memory and adds management overhead. The pattern should be used after understanding the workload and measuring the result.

A box of puppets

Imagine a puppeteer with a box of characters ready beside the stage. When a puppet is needed, it is taken from the box. When its scene ends, it goes back into the box rather than being thrown away and rebuilt for the next performance.

In a game, the same approach can work for projectiles, particles, enemies, or scenery that appears and disappears frequently.

In my Unity mobile prototype, birds were generated on the right side of the screen and removed after moving beyond the left edge. Instantiating a GameObject can involve meshes, materials, textures, components, and engine bookkeeping. Repeating that work can produce frame-time spikes and garbage-collection pressure.

Watch the Unity prototype on YouTube.

Instead, each bird can be disabled when it leaves the screen, returned to a pool, reset, and enabled again when needed.

How the pool behaves

The original component exposed three main settings and operations:

  1. A list of prefabs the pool knows how to manage.
  2. An initial buffer size for each prefab type.
  3. Methods to retrieve an available object and return it when no longer needed.

The calling code remains small:

GameObject bird = pool.Get("Bird", createIfMissing: true);
bird.transform.position = spawnPosition;

// Later, when the bird leaves the screen:
pool.Release(bird);

A compact version of the underlying idea looks like this:

using System.Collections.Generic;
using UnityEngine;

public sealed class SimplePool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int initialSize = 3;

    private readonly Queue<GameObject> available = new();

    private void Awake()
    {
        for (int i = 0; i < initialSize; i++)
            available.Enqueue(Create());
    }

    public GameObject Get()
    {
        GameObject item = available.Count > 0
            ? available.Dequeue()
            : Create();

        item.SetActive(true);
        return item;
    }

    public void Release(GameObject item)
    {
        item.SetActive(false);
        item.transform.SetParent(transform);
        available.Enqueue(item);
    }

    private GameObject Create()
    {
        GameObject item = Instantiate(prefab, transform);
        item.SetActive(false);
        return item;
    }
}

Real pools also need a clear reset contract. A reused object may retain velocity, animation state, timers, event subscriptions, particle state, or references from its previous life. Returning an object safely means restoring everything the next user expects—not only setting it inactive.

When pooling helps

Pooling is a strong candidate when objects are created frequently, construction is expensive, lifetimes are short, and the maximum active count is reasonably predictable. It may be unnecessary when creation is rare, objects are cheap, or the pool would keep a large amount of memory alive without evidence of a performance problem.

The pattern is not about eliminating every allocation. It is about moving repeated work away from a sensitive path and making object lifetime more predictable. Measure first, pool the expensive cases, and keep the reset logic explicit.