El problema#
Cada vez que haces Instantiate, Unity reserva memoria; cada Destroy deja "basura" que el recolector (GC) tendrá que limpiar más tarde. Cuando esto pasa constantemente (balas, casquillos, impactos, partículas, enemigos que reaparecen), el GC salta a destiempo y provoca tirones (frame drops). Además, crear y destruir repite trabajo que podríamos ahorrarnos.
El recolector de basura de .NET libera de golpe la memoria de los objetos que ya nadie usa. Si generas mucha basura, esas limpiezas son más largas y frecuentes, y cada una puede "comerse" varios milisegundos de un fotograma, justo lo que se percibe como un micro-tirón. El pooling ataca la causa: si no creas basura, el GC no tiene que limpiarla.
La idea del pooling#
En vez de crear y destruir, creas un conjunto de objetos una vez y los reutilizas: cuando necesitas una bala, coges una "dormida" del pool y la activas; cuando termina, la desactivas y la devuelves al pool en lugar de destruirla.
| Instantiate + Destroy | Object Pool | |
|---|---|---|
| Memoria en runtime | Reserva y libera constantemente | Reutiliza la misma |
| Basura para el GC | Mucha | Casi ninguna |
| Coste por objeto | Crear + destruir cada vez | Activar/desactivar |
| Ideal para | Objetos raros | Objetos muy frecuentes |
El ObjectPool de Unity 6#
No hace falta escribir el pool a mano: Unity incluye ObjectPool<T> en UnityEngine.Pool.
using UnityEngine;
using UnityEngine.Pool;
public class PoolBalas : MonoBehaviour
{
[SerializeField] private Bala balaPrefab;
private IObjectPool<Bala> pool;
void Awake()
{
pool = new ObjectPool<Bala>(
createFunc: () => {
Bala b = Instantiate(balaPrefab);
b.SetPool(pool); // la bala sabe a qué pool volver
return b;
},
actionOnGet: b => b.gameObject.SetActive(true),
actionOnRelease: b => b.gameObject.SetActive(false),
actionOnDestroy: b => Destroy(b.gameObject),
defaultCapacity: 30,
maxSize: 100
);
}
public Bala Pedir() => pool.Get(); // en vez de Instantiate
}Y la bala se devuelve sola al terminar (al salir de pantalla o tras un tiempo):
using UnityEngine;
using UnityEngine.Pool;
public class Bala : MonoBehaviour
{
private IObjectPool<Bala> pool;
public void SetPool(IObjectPool<Bala> p) => pool = p;
void OnEnable() => Invoke(nameof(Devolver), 3f); // vida de 3 s
void Devolver()
{
CancelInvoke();
pool.Release(this); // en vez de Destroy
}
}Cuándo NO usar pooling#
El pooling añade complejidad: hay que resetear el estado del objeto al reutilizarlo (posición, velocidad, vida...). Para objetos que aparecen una vez o rara vez, Instantiate/Destroy normal es más simple y suficiente. Reserva el pooling para lo que se crea muchísimo. Y antes de optimizar, mide con el Profiler: quizá tu cuello de botella esté en otro sitio.
Errores frecuentes#
- Olvidar resetear el estado al reutilizar (una bala vuelve con la velocidad de la anterior).
- No devolver los objetos al pool (se quedan "en uso" y el pool crea de más).
- Poolear objetos raros y complicar el código sin ganancia.
- Optimizar por intuición sin medir antes.
Ponte a prueba#
Resumen#
Crear y destruir objetos sin parar genera basura y tirones. El object pooling reutiliza un conjunto fijo activando/desactivando en vez de Instantiate/Destroy. Unity 6 trae ObjectPool<T> listo para usar. Aplícalo a lo que se crea muchísimo (balas, partículas), resetea siempre el estado al reutilizar y mide con el Profiler antes de optimizar.