Every mobile game I’ve shipped has had the same frame-time spike: a burst of bullets, coins or particles, each one created with Instantiate on the spot and thrown away with Destroy a second later. Creating objects is slow, and the garbage they leave behind comes back later as a hitch when the collector runs.
Object pooling fixes it by reusing instances instead of creating new ones. You don’t need a plugin for that any more: Unity 2021.1 added UnityEngine.Pool, and it covers almost everything a game needs.
The minimal pool
using UnityEngine;
using UnityEngine.Pool;
public class CoinSpawner : MonoBehaviour
{
[SerializeField] Coin prefab;
ObjectPool<Coin> pool;
void Awake()
{
pool = new ObjectPool<Coin>(
createFunc: () => Instantiate(prefab),
actionOnGet: coin => coin.gameObject.SetActive(true),
actionOnRelease: coin => coin.gameObject.SetActive(false),
actionOnDestroy: coin => Destroy(coin.gameObject),
collectionCheck: true,
defaultCapacity: 32,
maxSize: 256);
}
public void Spawn(Vector3 position)
{
Coin coin = pool.Get();
coin.transform.position = position;
coin.Init(pool);
}
}Get() hands you an inactive instance if one is waiting and only calls createFunc when the pool is empty. Release() puts an instance back. maxSize caps how many inactive objects the pool keeps; anything released beyond that is destroyed instead of stored, so a one-off explosion of 2,000 coins doesn’t pin 2,000 objects in memory forever.
Let the object return itself
The spawner shouldn’t have to track every coin. Give the pooled object its pool and let it release itself when it’s done:
public class Coin : MonoBehaviour
{
IObjectPool<Coin> pool;
float life;
public void Init(IObjectPool<Coin> owner)
{
pool = owner;
life = 3f;
}
void Update()
{
life -= Time.deltaTime;
if (life <= 0f) pool.Release(this);
}
}Depending on IObjectPool<Coin> rather than the concrete class also means you can swap in a different pool, or a fake one in tests, without touching Coin.
Reset state on the way out
A pooled object comes back exactly as you left it: velocity, trail, colour, the lot. Reset it when it’s released, or in its Init, so a recycled coin can never inherit the last one’s state. Physics is the classic trap — zero the Rigidbody’s linearVelocity (velocity before Unity 6) and angularVelocity, or the coin flies off the moment it reappears. Call Clear() on any TrailRenderer too, or it draws a streak from wherever the object last died.
Prewarm before the first burst
A pool that starts empty still creates objects during your first big wave. Fill it behind a loading screen instead:
var warm = new List<Coin>();
for (int i = 0; i < 32; i++) warm.Add(pool.Get());
foreach (Coin coin in warm) pool.Release(coin);The mistake that corrupts a pool
Releasing the same instance twice breaks a pool quietly: two later Get() calls return the same object, and one of them gets yanked away mid-flight. With collectionCheck: true, Unity throws the moment it happens, which turns a baffling bug into a stack trace. Leave it on while you develop; it costs a lookup per release, so switching it off for release builds is reasonable once the game is stable.