Al implementar patrones de diseño de programación de juegos comunes en su proyecto de Unity, puede crear y mantener de manera eficiente una base de código limpia, organizada y legible. Los patrones de diseño no solo reducen la refactorización y el tiempo dedicado a las pruebas, sino que también aceleran los procesos de incorporación y desarrollo, lo que contribuye a una base sólida para hacer crecer su juego, su equipo de desarrollo y su negocio.
Piense en los patrones de diseño no como soluciones terminadas que puede copiar y pegar en su código, sino como herramientas adicionales que, cuando se usan correctamente, pueden ayudarlo a crear aplicaciones más grandes y escalables.
Esta página explica la agrupación de objetos y cómo puede ayudar a mejorar el rendimiento de tu juego. Incluye un ejemplo de cómo implementar el sistema de agrupación de objetos integrado de Unity en sus proyectos.
El contenido aquí se basa en el libro electrónico gratuito Sube de nivel tu código con patrones de programación de juegos, que explica patrones de diseño conocidos y comparte ejemplos prácticos para usarlos en tu proyecto Unity.
Otros artículos de la serie de patrones de programación de juegos de Unity están disponibles en el centro de mejores prácticas de Unity o haga clic en los siguientes enlaces:
La agrupación de objetos es un patrón de diseño que puede proporcionar optimización del rendimiento al reducir la potencia de procesamiento requerida por la CPU para ejecutar llamadas repetitivas de creación y destrucción. En cambio, con la agrupación de objetos, los GameObjects existentes se pueden reutilizar una y otra vez.
La función clave del pooling de objetos es crear objetos por adelantado y almacenarlos en un pool, en lugar de crearlos y destruirlos según demanda. Cuando se necesita un objeto, se toma del grupo y se usa, y cuando ya no se necesita, se devuelve al grupo en lugar de destruirlo.
La imagen de arriba ilustra un caso de uso común para la agrupación de objetos, el de disparar proyectiles desde una torreta. Analicemos este ejemplo paso a paso.
En lugar de crear y luego destruir, el patrón del grupo de objetos utiliza un conjunto de objetos inicializados que se mantienen listos y esperando en un grupo desactivado. Luego, el patrón crea instancias previas de todos los objetos necesarios en un momento específico antes del juego. El grupo debe activarse en un momento oportuno en el que el jugador no notará el tartamudeo, como durante una pantalla de carga.
Una vez que se han utilizado los GameObjects del grupo, quedan desactivados y listos para usarse cuando el juego los necesite nuevamente. Cuando se necesita un objeto, su aplicación no necesita crear una instancia de él primero. En cambio, puede solicitarlo del grupo, activarlo y desactivarlo, y luego devolverlo al grupo en lugar de destruirlo.
Este patrón puede reducir el costo del trabajo pesado necesario desde la administración de memoria para ejecutar la recolección de basura, como se explica en la siguiente sección.
Antes de pasar a los ejemplos de cómo aprovechar la agrupación de objetos, veamos brevemente el problema raíz que ayuda a abordar.
La técnica de agrupación no solo es útil para reducir los ciclos de CPU dedicados a las operaciones de creación de instancias y destrucción. También optimiza la gestión de la memoria al reducir la sobrecarga de creación y destrucción de objetos, lo que requiere que se asigne y desasigne memoria, y que se llame a constructores y destructores.
Memoria administrada en Unity
El entorno de scripting C# de Unity ofrece un sistema de memoria administrada. Ayuda a gestionar la liberación de memoria, por lo que no es necesario solicitarla manualmente a través de su código. El sistema de administración de memoria también ayuda a proteger el acceso a la memoria, asegurando que la memoria que ya no usa se libere y evitando el acceso a la memoria que no es válida para su código.
Unity utiliza un recolector de basura para recuperar memoria de los objetos que su aplicación y Unity ya no usan. Sin embargo, esto también afecta el rendimiento en tiempo de ejecución, porque la asignación de memoria administrada requiere mucho tiempo para la CPU y la recolección de basura (GC) puede impedir que la CPU realice otros trabajos hasta que complete su tarea.
Cada vez que crea un nuevo objeto o destruye uno existente en Unity, la memoria se asigna y desasigna. Aquí es donde entra en juego la agrupación de objetos: Reduce la tartamudez que puede resultar de los picos de recolección de basura. Los picos de GC a menudo acompañan a la creación o destrucción de una gran cantidad de objetos debido a la asignación de memoria. Además de las recolecciones prematuras de basura, el proceso también puede causar fragmentación de la memoria, lo que dificulta la búsqueda de regiones de memoria contiguas libres.
Al reciclar los mismos objetos existentes, desactivándolos y activándolos, puedes crear un efecto, como disparar cientos de balas fuera de la pantalla, cuando en realidad simplemente los desactivas y los reciclas.
Obtenga más información sobre la administración de memoria en nuestraguía de creación de perfiles avanzada.
Aunque puede crear su propio sistema personalizado para implementar la agrupación de objetos, existe una clase ObjectPool incorporada en Unity que puede usar para implementar este patrón de manera eficiente en su proyecto (disponible en Unity 2021 LTS y posteriores).
Veamos cómo aprovechar el sistema de agrupación de objetos integrado utilizando la API UnityEngine.Pool con este proyecto de muestra que está disponible en Github. Una vez en la página de Github, vaya a Activos >7 Grupo de objetos >Scripts > Ejemplo de uso2021 para ver los archivos.
Nota: Puede consultar este tutorial de Unity Learn para ver un ejemplo de agrupación de objetos de una versión anterior de Unity.
Este ejemplo consiste en una torreta que dispara proyectiles rápidamente (establecidos en 10 proyectiles por segundo de forma predeterminada) cuando se presiona el botón del mouse. Cada proyectil viaja a través de la pantalla y debe ser destruido cuando sale de la pantalla. Sin la agrupación de objetos, esto puede crear un lastre considerable para la CPU y la administración de la memoria, como se explicó en la sección anterior.
Al utilizar la agrupación de objetos, parece como si se dispararan cientos de balas fuera de la pantalla cuando, en realidad, simplemente se desactivan y se reciclan una y otra vez.
El código del script de ejemplo ayuda a garantizar que el tamaño del grupo sea lo suficientemente grande como para mostrar los objetos activos simultáneamente, camuflando así el hecho de que los mismos objetos se reutilizan constantemente.
Si ha utilizado el sistema de partículas de Unity, entonces tiene experiencia de primera mano con un grupo de objetos. El componente Sistema de partículas contiene una configuración para el número máximo de partículas. Esto recicla las partículas disponibles, evitando que el efecto supere un número máximo. El grupo de objetos funciona de manera similar, pero con cualquier GameObject de tu elección.
Echemos un vistazo al código en RevisedGun.cs que se encuentra en la demostración de Github en Assets>7 Object Pool >Scripts >ExampleUsage2021.
Lo primero que hay que notar es la inclusión del espacio de nombres del grupo:
using UnityEngine.Pool;
Al utilizar la API UnityEngine.Pool, obtienes una clase ObjectPool basada en pila para rastrear objetos con el patrón del grupo de objetos. Dependiendo de sus necesidades, también puede utilizar una clase CollectionPool (List, HashSet, Dictionary, etc.)
Luego, aplica configuraciones específicas para las características de disparo de su arma, incluido el Prefab para generar (llamado proyectilPrefab del tipo RevisedProjectile).
Se hace referencia a la interfaz ObjectPool desde RevisedProjectile.cs (que se explica en la siguiente sección) y se inicializa en la función Awake.
vacío privado Despierto ()
{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile,
OnGetFromPool, OnReleaseToPool,
OnDestroyPooledObject,collectionCheck, defaultCapacity, maxSize);
}
Si explora el constructor ObjectPool<T0> , verá que incluye la útil capacidad de configurar algo de lógica cuando:
Primero creando un elemento agrupado para poblar el grupo
Sacar un objeto de la piscina
Devolver un artículo al pool
Destruir un objeto agrupado (por ejemplo, si alcanza un límite máximo)
Tenga en cuenta que la clase ObjectPool incorporada también incluye opciones para un tamaño de grupo predeterminado y máximo, siendo este último el número máximo de elementos almacenados en el grupo. Se activa cuando llamas a Release y, si el grupo está lleno, se destruye.
Veamos cómo el ejemplo de código realiza varias acciones que especifican cómo Unity debe manejar la agrupación de objetos de manera eficiente según su caso de uso específico.
Primero, se pasa createFunc que se usa para crear una nueva instancia cuando el grupo está vacío, que en este caso es CreateProjectile() que crea una instancia de un nuevo perfil Prefab.
private RevisedProjectile CreateProjectile()
{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;
devolver instancia de proyectil;
}
Se llama a OnGetFromPool cuando solicita una instancia de GameObject, por lo que habilita el GameObject que obtiene del grupo de forma predeterminada.
vacío privado OnGetFromPool (Objeto agrupado de proyecto revisado)
{
pooledObject.gameObject.SetActive(true);
}
OnReleaseToPool se utiliza cuando GameObject ya no es necesario y se devuelve al grupo; en este ejemplo, es simplemente cuestión de desactivarlo nuevamente.
vacío privado OnReleaseToPool (Objeto agrupado de proyecto revisado)
{
pooledObject.gameObject.SetActive(false);
}
Se llama a OnDestroyPooledObject cuando se excede el número máximo de elementos agrupados permitidos. Con la piscina ya llena, el objeto será destruido.
vacío privado OnDestroyPooledObject (RevisedProjectile pooledObject)
{
Destroy(pooledObject.gameObject);
}
collectionChecks se utiliza para inicializar IObjectPool y generará una excepción cuando intente liberar un GameObject que ya ha sido devuelto al administrador del grupo, pero esta verificación solo se realiza en el Editor. Al desactivarlo, puedes ahorrar algunos ciclos de CPU, aunque corres el riesgo de que te devuelvan un objeto que ya ha sido reactivado.
Como su nombre lo indica, defaultCapacity es el tamaño predeterminado de la pila/lista que contendrá sus elementos y, por lo tanto, cuánta asignación de memoria desea comprometer por adelantado. maxPoolSize será el tamaño máximo de la pila, y los GameObjects agrupados creados nunca deben exceder este tamaño. Eso significa que si devuelves un artículo a un grupo que está lleno, el artículo será destruido.
Luego, en FixUpdate() obtendrás un objeto agrupado en lugar de crear una instancia de un nuevo proyectil cada vez que ejecutes la lógica para disparar una bala.
RevisedProjectile bulletObject = objectPool.Get();
Es tan simple como eso.
Ahora echemos un vistazo al script RevisedProjectile.cs .
Además de configurar una referencia a ObjectPool, lo que hace que devolver el objeto al grupo sea más conveniente, hay algunos detalles de interés.
El timeoutDelay se utiliza para realizar un seguimiento de cuándo se ha "usado" el proyectil y se puede devolver al grupo de juego nuevamente; esto sucede de forma predeterminada después de tres segundos.
La función Deactivate() activa una corrutina llamada DeactivateRoutine(retraso de flotación), que no solo libera el proyectil de regreso a la piscina con objectPool.Release(this), sino que también restablece los parámetros de velocidad del Rigidbody en movimiento.
Este proceso aborda el problema de los “elementos sucios”: objetos que se utilizaron en el pasado y que deben restablecerse debido a su estado no deseado.
Como puede ver en este ejemplo, la API UnityEngine.Pool hace que la configuración de grupos de objetos sea eficiente, porque no es necesario reconstruir el patrón desde cero, a menos que tenga un caso de uso específico para hacerlo.
No estás limitado únicamente a GameObjects. La agrupación es una técnica de optimización del rendimiento para reutilizar cualquier tipo de entidad de C#: un GameObject, un Prefab instanciado, un diccionario de C#, etc. Unity ofrece algunas clases de agrupación alternativas para otras entidades, como DictionaryPool<T0,T1> que ofrece soporte para diccionarios y HashSetPool<T0> para HashSets. Obtenga más información sobre estos en la documentación.
LinkedPool utiliza una lista vinculada para contener una colección de instancias de objetos para su reutilización, lo que puede llevar a una mejor administración de la memoria (según su caso), ya que solo usa memoria para los elementos que realmente están almacenados en el grupo.
Compare esto con ObjectPool, que simplemente usa una pila de C# y una matriz de C# debajo y, como tal, contiene una gran cantidad de memoria contigua. El inconveniente es que gasta más memoria por elemento y más ciclos de CPU para administrar esta estructura de datos en LinkedPool que en ObjectPool, donde puede utilizar defaultSize y maxSize para configurar sus necesidades.
La forma de utilizar los grupos de objetos variará según la aplicación, pero el patrón suele aparecer cuando un arma necesita disparar varios proyectiles, como se ilustra en el ejemplo anterior.
Una buena regla general es crear un perfil de su código cada vez que crea una instancia de una gran cantidad de objetos, ya que corre el riesgo de provocar un pico de recolección de basura. Si detecta picos significativos que ponen su juego en riesgo de tartamudear, considere usar un grupo de objetos. Solo recuerde que el agrupamiento de objetos puede agregar más complejidad a su código base debido a la necesidad de administrar los múltiples ciclos de vida de los grupos. Además, también puedes terminar reservando memoria que tu juego no necesariamente necesita al crear demasiados grupos prematuros.
Como se mencionó anteriormente, existen otras formas de implementar la agrupación de objetos además del ejemplo incluido en este artículo. Una forma es crear su propia implementación que pueda personalizar según sus necesidades. Pero deberá tener en cuenta las complicaciones de la seguridad de tipos y subprocesos, así como definir la asignación/desasignación de objetos personalizados.
Afortunadamente, Unity Asset Store ofrece excelentes alternativas para ahorrarle tiempo.
Recursos más avanzados para la programación en Unity
El libro electrónico, Sube de nivel tu código con patrones de programación de juegos, proporciona un ejemplo más completo de un sistema simple de grupo de objetos personalizados. Unity Learn también ofrece una introducción a la agrupación de objetos, que puede encontrar aquí, y un tutorial completo para usar el nuevo sistema de agrupación de objetos integrado en 2021 LTS.
Todos los libros electrónicos y artículos técnicos avanzados están disponibles sobre lasmejores prácticas de Unity.centro. Los libros electrónicos también están disponibles en elpáginade mejores prácticas avanzadasen la documentación.