Un object pool es un patrón de diseño pensado para conservar el uso de memoria, evitar picos de subida y bajada memoria, también evita que la memoria internamente se desordene, esto es útil en plataformas muy limitadas, principalmente si compilas en Webgl, donde no se permite un consumo por encima del límite. En esta ocasión les enseñaré uno muy simple de implementar.
Esta semana me quedé sin internet y necesitaba un pool de objetos para un viejo videojuego que estoy reescribiendo de nuevo, inicialmente pensaba hacer un tutorial muy bueno de la web de Cat Like Coding, pero sin internet no tenia mas opción que hacerlo por mi cuenta, asi que les mostraré mi resultado, creo que quedó muy pulido pero aún hay errores que se me escapan, si alguien tiene la solucion y me puede ayudar a corregirlo, estaria muy agradecido.
Cómo funciona un object pool, un pool es una lista que toma objetos que se van a instanciar, pero que en lugar de eliminarse, se reciclan, cada vez que un personaje dispara balas, en vez de crearlas, le pide prestada al pool una bala ya existente. Y cuando esta choca en vez de destruirse, simplemente la devuelve al pool.
En primer lugar haremos una clase serializable, que nos sirve en unity para ordenar de mejor forma los objetos, esta clase solo va a contener el prefab que queremos que instancie y la cantidad estimada que posiblemente necesitemos. Y como ultima variable un buffer de objetos, que será una lista que contendrá todos los objetos ya instanciados, de esta lista los objetos serán agregados y retirados cada vez nuestro personaje o nuestros enemigos requieran usar objetos o devolverlos.
/// <summary>
/// Clase que contiene las listas de objetos serán parte del pool de objetos
/// </summary>
[System.Serializable]
public class PooleableObject
{
public GameObject prefab;
public int amount;
//[HideInInspector]
public List<GameObject> buffer;
public PooleableObject(GameObject prefab, int amount)
{
this.prefab = prefab;
this.amount = amount;
}
}
Ahora empezaremos otra clase que contendrá una lista de todas las clases diferentes de objetos que se usará en el transcurso del nivel y una variable de un mínimo de objetos a instanciar si se nos olvida poner la cantidad en el PooleableObject. Esta clase será un singleton para evitar que otras clases llamen a distintas pools y comience a haber duplicidad de peticiones, para ello usamos el singleton del blog anterior.
public class ObjectPoolLocal : Singleton<ObjectPoolLocal>
{
public List<PooleableObject> pools;
public int defaultBufferAmount = 3;
private void Start()
{
gameObject.name = "Pool";
GenerateCompletePool();
}
El primer método a implementar seria La generación completa de todos los objetos al iniciar la escena. Porque hago la llamada en el método Start y no en Awake, por la sencilla razón que la clase Singleton apenas está registrando nuestra clase y pudiera haber errores al perderse la pista de todos los objetos por un cambio de referencias.
Como pueden ver el método solo hace un recorrido por los elementos de la lista de objetos pooleables y los genera uno a uno, pero hace uso de otro método.
/// <summary>
/// Crea todos los GameObjects contenidos en PooleableObject
/// </summary>
private void GenerateCompletePool()
{
foreach (PooleableObject pool in pools)
{
GeneratePool(pool);
}
}
El método Generate pool crea una lista nueva en el buffer, determina que cantidad instanciar dependiendo del valor mayor entre el monto del pool o la cantidad predeterminada. Luego recorre un bucle en el que irá instanciando uno a uno los objetos y los registra en el buffer.
/// <summary>
/// Genera un nuevo pool de objetos desde un parametro de tipo PooleableObject
/// </summary>
/// <param name="pool"></param>
private void GeneratePool(PooleableObject pool)
{
pool.buffer = new List<GameObject>();
int amount = defaultBufferAmount;
if (pool.amount > defaultBufferAmount) amount = pool.amount;
for (int i = 0; i < amount; i++)
{
GameObject readyToList = Instantiate(pool.prefab) as GameObject;
readyToList.name = pool.prefab.name;
readyToList.transform.SetParent(transform, false);
readyToList.SetActive(false);
pool.buffer.Add(readyToList);
}
}
Seguro se estarán preguntando porqué haber metido un método dentro de otro si era más fácil incluirlo en uno solo. Resulta que habilité la posibilidad de que cualquier clase que requiera registrar un nuevo pool de objetos pueda hacerlo. Este método toma el prefab y la cantidad desde la otra clase, dentro es comparado para averiguar si el nuevo pool que se piensa crear ya existe dentro de la lista de pools y si no, crea los objetos nuevos, agrega el pool a la lista.
/// <summary>
/// Crea un nuevo PooleableObject a partir de un Prefab y una cantidad
/// </summary>
/// <param name="prefab"></param>
/// <param name="amount"></param>
public void CreatePooleableObject(GameObject prefab, int amount = 0)
{
PooleableObject _pooleable = new PooleableObject(prefab, amount);
if (!pools.Contains(_pooleable))
{
pools.Add(_pooleable);
GeneratePool(_pooleable);
}
else
{
Debug.Log("Ya existe un pool con el nombre de: " + prefab.name);
}
}
Prácticamente eso es todo, ahora lo que queda son los métodos que podrá usar las otras clases para crear y devolver objetos.
El método Spawn, su función es similar a Instantiate, en este caso la clase que lo requiera deberá enviar el nombre del prefab y opcional si quiere que el GameObject que necesita sea creado en caso de no haber ninguno en el buffer.
Los objetos siempre serán engendrados o spawneados siempre tomando el primer objeto de la lista, a esto se le conoce como FIFO (First In First Out) el primer objeto que entra, es el primero en salir, cada vez que el objeto sale, se retira de la lista del buffer
/// <summary>
/// Similar a Instantiate, devuelve un objeto de alguno de los pools que coincida con el nombre del objeto enviado.
/// </summary>
/// <param name="name"></param>
/// <param name="onlyPooled"></param>
/// <returns></returns>
public GameObject Spawn(string name, bool onlyPooled = true)
{
GameObject expectedObject = null;
foreach (PooleableObject pool in pools)
{
if (pool.prefab.name == name)
{
if (pool.buffer.Count > 0 && pool.buffer != null)
{
expectedObject = pool.buffer[0];
pool.buffer.RemoveAt(0);
}
else
{
if (!onlyPooled)
{
expectedObject = Instantiate(pool.prefab) as GameObject;
}
}
expectedObject.transform.SetParent(null, false);
expectedObject.SetActive(true);
}
}
return expectedObject;
}
Por último queda la función de devolver el objeto al pool, esto es un método que llama a una corrutina, la intención del método Despawn es que funcione similar al método Destroy que tiene la posibilidad de eliminar el objeto instantáneamente o eliminarlo con un retraso.
Si le parece raro este método es lo que se conoce como un método lamda, un método que solo ejecuta una línea, es algo curioso de usar
/// <summary>
/// Similar a Destroy. Desvincula un objeto y lo envía de nuevo a la pila.
/// </summary>
/// <param name="obj"></param>
/// <param name="time"></param>
public void Despawn(GameObject obj, float time = 0) => StartCoroutine(WaitAndDespawn(obj, time));
El metodo WaitAndDespawn es una corrutina que espera un tiempo determinado y luego compara si el objeto a devolverse tiene el mismo nombre de alguno de los pools y lo devuelve al buffer, lo emparenta y lo desactiva
/// <summary>
/// Si el objeto devuelto a la pila pertenece a alguno de los pools lo ingresa
/// </summary>
/// <param name="obj"></param>
/// <param name="time"></param>
/// <returns></returns>
private IEnumerator WaitAndDespawn(GameObject obj, float time)
{
yield return new WaitForSeconds(time);
foreach (PooleableObject pool in pools)
{
if (pool.prefab.name == obj.name)
{
obj.transform.SetParent(transform, false);
obj.SetActive(false);
pool.buffer.Add(obj);
}
}
}
Pudo haber parecido largo, pero esta implementación es simple, tiene algunos problemas que aun no entiendo como se produce, a la hora de usar Despawn, al principio funciona bien sin errores y luego después de ciertos intentos, ya no logra mandar los objetos al pool.
No siendo mas nos vemos en otro Blog.
Comments