¿Qué es el Object Pooling?
Imagina cualquier videojuego del genero shooter, ya sea en primera persona o tercera persona; hay un objeto que tienen en común tanto el jugador como los enemigos, que es su armamento, específicamente el uso de balas para las diferentes armas. Hay varias maneras de plantear el disparo en este genero, y en Unity se puede hacer con raycast o spawnear un objeto, a manera de prefab, y agregarle una fuerza y una dirección.
Ahora, si optamos por utilizar el crear objetos que tendrán el comportamiento de la bala, lo más normal es que cuando se presione el botón de disparo tengamos un método para crear una bala, esto a través del método Instantiate que nos provee unity, al cual deberemos asignar que objeto se quiere crear, asignar una posición y rotación.
public GameObject bulletPrefab; //El objeto bala que se va a crear
public Transform spawnPoint; //El lugar donde saldrá la bala, cañón de arma
//Este método se manda llamar en cada frame
public void Update()
{
if(Input.GetMouseButtonDown(0))
{
Instantiate(bulletPrefab, spawnPoint.position, spawnPoint.rotation);
}
}
Hay que dejar en claro que este método Instantiate() nos va a estar creando un nuevo objeto cada vez que lo mandemos llamar, en este caso cuando el jugador presione el botón izquierdo del mouse; haciendo los ajustes necesarios, los enemigos, IA, tendrían algo similar para cuando ellos disparen al jugador, lo cual nos va a generar muchos objetos en escena, que vendrían a ser los objetos del tipo bala, que estarán consumiendo memoria y más cuando no determinemos en que momento esa bala no la necesitemos en tiempo de ejecución, esto puede ocurrir en dos escenarios, la bala impacta sobre un objeto que puede recibir daño, sea jugador, enemigos, barriles explosivos, etc., o impactar sobre una pared, un carro o alguna parte del decorado del nivel, y lo que se suele hacer es lo siguiente:
//script que se encuentra en el objeto bala
void OnCollisionEnter(Collision col)
{
if(col.gameObject.CompareTag("Enemy"))
{
Destroy(gameObject, 0f); //Método que destruye a la bala
}
}
Como se muestra en el código de arriba, en cuanto la bala detecte una colisión y se cumpla la condición se destruirá, el método Destroy() quitará de memoria el objeto que reciba como parámetro, hacer esto un par de veces no afecta; en nuestro ejemplo del shooter digamos que nos encontráramos con 10 enemigos cada uno tendrá sus propias balas que pueden tener de referencia el mismo prefab del tipo bala, cuando cada enemigo dispare mandara llamar el método Instantiate() y a su vez el jugador también lo hará para responder al ataque, entonces estamos hablando de que 11 objetos (10 enemigos y 1 jugador) estarán creando nuevos objetos del tipo bala cada X segundos dependiendo de la velocidad de disparo de cada quien, haciendo crecer la cantidad de objetos que se muestran en escena y cada uno tendrá su propio comportamiento y su propia detección de colisión para destruirse cuando llegue el momento. Ya podemos empezar a imaginar la enorme cantidad de procesos que se estarán llevando, ya que el Instantiate() y el Destroy tiene un costo relativamente elevado y al hacerlo X cantidad de veces de de forma rápida puede causar estragos.
Es aquí donde el object pool puede ayudarnos a mejorar el rendimiento de nuestro juego, la idea es tener una "alberca" de un determinado tipo de objetos con una cantidad finita de cuantos existirán desde que inicia el juego, se basa que en vez de destruir y crear objetos, los vamos a estar reutilizando y cuando no los necesitemos los mandaremos a una alberca.
Un patrón de diseño muy fácil de implementar
Vamos a empezar por definir una clase que estará controlando la alberca de objetos:
public class PoolManagerObjects : MonoBehaviour
{
public GameObject objPrefab; //El objeto que vamos a estar reutilizando
public int poolSize; //Cuantos objetos se necesitaran
private Queue<GameObject> objPool; //La "alberca" donde estarán los objetos
}
Utilizamos una cola, Queue, ya que su funcionamiento de primero en entrar, primero en salir viene perfecto para este patrón de diseño, aquí va estar la referencia a los objetos que podremos estar reutilizando en cuanto lo necesitemos.
Inicializamos la alberca:
public class PoolManagerObjects : MonoBehaviour
{
public GameObject objPrefab; //El objeto que vamos a estar reutilizando
public int poolSize; //Cuantos objetos se necesitaran
private Queue<GameObject> objPool; //La "alberca" donde estarán los objetos
void Start()
{
objPool = new Queue<GameObject>(); //Inicializamos la cola
for (int i = 0; i < poolSize; i++) //Vamos a llenar la alberca en base al tamaño
{
//Instanciamos el objeto y lo guardamos en una varible temporal
GameObject newObj = Instantiate(objPrefab);
objPool.Enqueue(newObj); //Lo añadimos a la cola con Enqueue
newObj .SetActive(false); //Lo desactivamos ya que en ese momento no se requiere
}
}
}
Bien, haciendo uso del ciclo for, llenaremos la alberca con una determinada cantidad de objetos en base a la variable poolSize. Es verdad que estamos haciendo uso del Instantiate() pero solo se va a realizar una cantidad finita de veces y al inicio del juego, razón de que esto pase en el método Start().
Ahora, vamos a crear dos métodos que vendrán a sustituir el método Instantiate() y Destroy().
public class PoolManagerObjects : MonoBehaviour
{
public GameObject objPrefab; //El objeto que vamos a estar reutilizando
public int poolSize; //Cuantos objetos se necesitaran
private Queue<GameObject> objPool; //La "alberca" donde estarán los objetos
void Start()
{
objPool = new Queue<GameObject>(); //Inicializamos la cola
for (int i = 0; i < poolSize; i++) //Vamos a llenar la alberca en base al tamaño
{
//Instanciamos el objeto y lo guardamos en una varible temporal
GameObject newObj = Instantiate(objPrefab);
objPool.Enqueue(newObj); //Lo añadimos a la cola con Enqueue
newObj .SetActive(false); //Lo desactivamos ya que en ese momento no se requiere
}
}
public GameObject GetObjFromPool(Vector3 newPosition, Quaternion newRotation)
{
//Se obtiene el 1er objeto disponible en la cola
GameObject newObj = objPool.Dequeue();
//Activamos el objeto, se activa su comportamiento
newObj.SetActive(true);
//Le damos la posición y rotación, en donde se necesita que este
newObj.transform.SetPositionAndRotation(newPosition, newRotation);
return newObj;
}
public void ReturnObjToPool(GameObject go)
{
go.SetActive(false); //Lo desactivamos
objPool.Enqueue(go); //Lo volvemos a añadir a la cola para reutilizarlo
}
}
Para terminar, el método GetObjFromPool() sustituye al Instantiate(), pero en este caso solo nos interesa asignar la posición y rotación del lugar donde necesitemos el objeto de la alberca, eso si, tendremos que tener acceso a la clase PoolManagerObjects para hacer uso del mismo. Ojo aquí, que el método nos va a retornar el objeto.
Esto ocurre de igual forma con el método ReturnObjToPool() que sustituye al Destroy(), recibe de parámetro el objeto que queremos regresar a la alberca, se desactiva y se vuelve a encolar.
Volviendo al ejemplo cuando se presiona el botón del mouse, quedaría de la siguiente manera:
public PoolManagerObjects poolManager; //Hay que asignar desde el inspector el objeto que tenga el script PoolManagerObjects
public Transform spawnPoint; //El lugar donde saldrá la bala, cañón de arma
//Este método se manda llamar en cada frame
public void Update()
{
if(Input.GetMouseButtonDown(0))
{
//Tenemos una referencia del objeto que tomamos de la alberca,
// esto puede ayudarnos para modificar valores de la bala, acceder a sus componentes, etc.
GameObject theBullet = poolManager.GetObjFromPool(spawnPoint.position, spawnPoint.Rotation);
}
}
Y para el caso cuando ya no necesitemos el objeto:
//script que se encuentra en el objeto bala
public PoolManagerObjects poolManager; //Hay que asignar desde el inspector el objeto que tenga el script PoolManagerObjects
void OnCollisionEnter(Collision col)
{
if(col.gameObject.CompareTag("Enemy"))
{
//En cuanto exista una colisión y se cumpla la condición del if
// mandaremos llamar el método para regresar la bala a la alberca y la podamos reutilizar
poolManager.ReturnObjToPool(this.gameObject);
}
}
Entonces, estamos mejorando bastante el rendimiento de nuestro juego, ya que pasamos de que 11 objetos (enemigos y jugador), estén creando y destruyendo X cantidad de balas, a que todos tengan acceso a una alberca con una cantidad finita de objetos y todos estén utilizando los mismos objetos en cuanto lo requieran.
Se puede mejorar bastante este patrón, añadiendo un par de elementos para que se vuelva más útil y permita utilizar distintas albercas con objetos diferentes cada una.
Espero quede entendible este patrón, cualquier comentario que ayude a mejorar es bienvenido.
Notas importante:
-A la variable poolSize, debemos darle un valor considerable para que no llegue a pasar el error de que se estén utilizando todos los objetos y la alberca este vacía, hay que poner un valor no muy alto pero tampoco tan bajo.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.