Patrones de diseño – Object Pool

En este artículo voy a hablarte del patrón de optimización Object Pool, o también conocido como Object Pooling, y vamos a ver cómo implementarlo en Unity.

En la versión 2021 de Unity ya han incorporado este patrón de forma nativa, pero le tendremos que hacer un Adapter si queremos generalizar su uso, también lo veremos en este post.

Objectivos del Object Pool

En todos los juegos tenemos la problemática de que instanciar/crear nuestros objetos es una operación pesada y requiere de memoria dinámica.

Si empezamos a crear y destruir objetos, ya sean GameObjects u objetos puros de C#, es posible que empecemos a sufrir problemas de fragmentación de memoria, algo nada deseado.

Con el Object Pool buscamos reciclar los objetos, los crearemos 1 vez y luego los reutilizaremos apagándolos cuando no sean necesarios, y activándolos cuando los queramos reutilizar.

De esta forma no estaremos creando y destruyendo memoria con cada objeto nuevo, y reduciremos el tiempo de instanciación de los objetos, que si hablamos de muchos objetos podría afectar a los FPS.

¿Qué es la fragmentación de memoria?

Antes de seguir con el Object Pooling, me gustaría explicarte que es la fragmentación de memoria y porqué es un problema.

En la siguiente imagen tenemos una representación de la memoria, cada bloque naranja es un objeto que hemos instanciado.

En el primer paso tenemos un objeto de tipo ObjetoA, en el segundo hemos instanciado un objeto del tipo ObjetoB y para esto el gestor de memoria a tenido que localizar el siguiente espacio vacío donde quepa el objeto.

unity pool

En el paso 3 instanciamos otro objeto del tipo ObjetoA, y en el cuarto paso hemos eliminado el objeto de tipo ObjetoB. Como ves, se ha quedado un espacio vacío donde antes estaba el objeto tipo ObjetoB.

Si ahora intentamos instanciar un ObjetoA, dado que no cabe dónde estaba el ObjetoB, lo tendremos que almacenar al final de la memoria, con lo que se ha quedado un espacio vacío.

Esto justo es la fragmentación de memoria, pequeños espacios de memoria que no son suficientes para albergar un objeto y que vamos perdiendo.

Para solucionar este problema, existe una técnica que es la desfragmentación y consiste en reordenar la memoria para corregir estos huecos. Cómo habrás imaginado, esto es una operación muy pesada.

Object Pooling y la fragmentación de memoria

Gracias a que en el objeto pool vamos a crear los objetos una vez, y no los destruiremos hasta el final, reduciremos la cantidad de fragmentación.

Además, en algunos lenguajes como C++, podríamos reservar un espacio de memoria contiguo y ahí dentro ir creando nuestros objetos, de esta forma no tendremos nada de fragmentación.

Por desgracia esto no es posible con Unity hablando de los GameObjects ya que es Unity el encargado de reservar la memoria e instanciarlos.

Implementación

Para implementar una pool, nos hará falta una colección donde iremos guardando los objetos reciclados y listos para volver a ser utilizados, y necesitaremos otra para controlar los objetos que están actualmente en uso.

Además si lo vamos a hacer para GameObjecs, nos hará falta el prefab que utilizaremos para instanciar los objetos.

En la inicialización podríamos crear un número inicial de objetos, así cuando queramos utilizar alguno ya los tendremos preparados.

public class ObjectPool
    {
        private readonly RecyclableObject _prefab;
        private readonly HashSet<RecyclableObject> _instantiateObjects;
        private Queue<RecyclableObject> _recycledObjects;

        public ObjectPool(RecyclableObject prefab)
        {
            _prefab = prefab;
            _instantiateObjects = new HashSet<RecyclableObject>();
        }

        public void Init(int numberOfInitialObjects)
        {
            _recycledObjects = new Queue<RecyclableObject>(numberOfInitialObjects);
            
            for (var i = 0; i < numberOfInitialObjects; i++)
            {
                var instance = InstantiateNewInstance();
                instance.gameObject.SetActive(false);
                _recycledObjects.Enqueue(instance);
            }
        }

        private RecyclableObject InstantiateNewInstance()
        {
            var instance = Object.Instantiate(_prefab);
            instance.Configure(this);
            return instance;
        }

...
    }

Para utilizar un objeto, primero comprobaremos si existe algún objeto reciclado y lo podemos utilizar, y en el caso de no encontrar ninguno tenemos 2 opciones: podemos lanzar una excepción, o podemos instanciar un objeto extra y añadirlo a la pool.

public class ObjectPool
    {
       ...

        public T Spawn<T>()
        {
            var recyclableObject = GetInstance();
            _instantiateObjects.Add(recyclableObject);
            recyclableObject.gameObject.SetActive(true);
            recyclableObject.Init();
            return recyclableObject.GetComponent<T>();
        }

        private RecyclableObject GetInstance()
        {
            if (_recycledObjects.Count > 0)
            {
                return _recycledObjects.Dequeue();
            }
            
            Debug.LogWarning($"Not enough recycled objets for {_prefab.name} consider increase the initial number of objets");
            var instance = InstantiateNewInstance();
            return instance;
        }
...
    }

Por último nos falta la operación de reciclado, que no es otra cosa que desactivar el objeto y actualizar las colecciones.

public class ObjectPool
    {
       ...

        public void RecycleGameObject(RecyclableObject gameObjectToRecycle)
        {
            var wasInstantiated = _instantiateObjects.Remove(gameObjectToRecycle);
            Assert.IsTrue(wasInstantiated, $"{gameObjectToRecycle.name} was not instantiate on {_prefab.name} pool");
            
            gameObjectToRecycle.gameObject.SetActive(false);
            gameObjectToRecycle.Release();
            _recycledObjects.Enqueue(gameObjectToRecycle);
        }
    }

RecycleGameObject

Esta clase abstracta nos permitirá tener los métodos Init y Release en todos los objetos que formen parte de la pool.

Dado que solo vamos a crear los objetos 1 vez, los métodos Awake y Start no nos sirven para reiniciar el estado de nuestro objeto, por eso necesitamos la función Init.

Lo mismo ocurre con el OnDestroy, solo se va a llamar al final y necesitamos la función Release por si tenemos que desuscribirnos de algún evento antes de reciclar el objeto.

public abstract class RecyclableObject : MonoBehaviour
    {
        private ObjectPool _objectPool;

        internal void Configure(ObjectPool objectPool)
        {
            _objectPool = objectPool;
        }

        public void Recycle()
        {
            _objectPool.RecycleGameObject(this);
        }
        internal abstract void Init();
        internal abstract void Release();
    }

También tendremos el método Recycle para poder reciclar este objeto de una forma fácil.

ObjectPool en Unity 2021

En la versión 2021 Unity añadió de forma nativa el Object pool y es bastante sencillo de utilizar, aunque tiene alguna limitación.

Para utilizarla es tan fácil como crear un objeto del tipo IObjectPool<T> y pasarle las funciones de creación, de obtención de objetos y de reciclado.

private readonly IObjectPool<RecyclableObject> _pool;

   public MyObjectPool(RecyclableObject prefab)
        {
            _prefab = prefab;
            _pool = new ObjectPool<RecyclableObject>(CreateFunc,
                                                     OnTakeFromPool,
                                                     OnReturnedToPool);
        }

        private RecyclableObject CreateFunc()
        {
            var myObject = Object.Instantiate(_prefab);
            myObject.Configure(_pool);
            return myObject;
        }

        private void OnTakeFromPool(RecyclableObject myObject)
        {
            myObject.gameObject.SetActive(true);
            myObject.Init();
        }

        private void OnReturnedToPool(RecyclableObject myObject)
        {
            myObject.Release();
            myObject.gameObject.SetActive(false);
        }

Por desgracia esto lo tendremos que hacer con cada tipo de objeto que queramos almacenar en la pool, pero tenemos una fácil solución que consiste en crear un Adapter. Demos gracias a los patrones de diseño xD.

Adapter del ObjectPool de Unity

public class MyObjectPool
    {
        private readonly RecyclableObject _prefab;
        private readonly IObjectPool<RecyclableObject> _pool;

        public MyObjectPool(RecyclableObject prefab)
        {
            _prefab = prefab;
            _pool = new ObjectPool<RecyclableObject>(CreateFunc,
                                                     OnTakeFromPool,
                                                     OnReturnedToPool);
        }

        private RecyclableObject CreateFunc()
        {
            var myObject = Object.Instantiate(_prefab);
            myObject.Configure(_pool);
            return myObject;
        }

        private void OnTakeFromPool(RecyclableObject myObject)
        {
            myObject.gameObject.SetActive(true);
            myObject.Init();
        }

        private void OnReturnedToPool(RecyclableObject myObject)
        {
            myObject.Release();
            myObject.gameObject.SetActive(false);
        }

        public TComponent Spawn<TComponent>()
        {
            var recyclableObject = _pool.Get();
            return recyclableObject.GetComponent<TComponent>();
        }
    }

Con esto ya tenemos nuestra pool de Unity 2021 lista para ser utilizada de forma genérica y sin tener que copiar estos métodos de creación.

Conclusión

El Object Pool es uno de los patrones más utilizados en videojuegos, porque todos sabemos que el rendimiento en los juegos es algo crítico.

Ya sea con la versión de Unity 2021, o nuestra propia implementación, con este patrón optimizamos el rendimiento y la memoria de nuestro juego.

Este es uno de los patrones que enseñamos en el curso de Patrones de diseño para VIDEOJUEGOS, entre otros patrones muy utilizados. Si quieres conocer más sobre el curso solo tienes que acceder desde el enlace de arriba.

Puedes descargar el código aquí utilizado en este enlace.

Otras entradas

Resumen
➤ Patrones de diseño - Object Pool
Nombre del artículo
➤ Patrones de diseño - Object Pool
Descripción
Aquí vas a aprender cómo implementar el patrón de Optimización OBJECT POOL utilizando Unity 2021. 💪
Autor
Publisher Name
The Power Ups - Learning
Publisher Logo