Cuentos desde las trincheras de la optimización: Ahorrar memoria con Addressables

La transmisión eficiente de activos dentro y fuera de la memoria es un elemento clave de cualquier juego de calidad. Como consultor de nuestro equipode Servicios Profesionales, me he esforzado por mejorar el desempeño de muchos proyectos de clientes. Por eso me gustaría compartir algunos consejos sobre cómo aprovechar elsistema de activos direccionables de Unitypara mejorar su estrategia de carga de contenido.
La memoria es un recurso escaso que debe administrarse con cuidado, especialmente al trasladar un proyecto a una nueva plataforma. El uso de Addressables puede mejorar la memoria en tiempo de ejecución al introducir referencias débiles para evitar que se carguen activos innecesarios. Las referencias débiles significan que usted tiene control sobre cuándo se carga dentro y fuera de la memoria el activo referenciado; el sistema direccionable encontrará todas las dependencias necesarias y también las cargará. Este blog cubrirá una serie de escenarios y problemas que puede encontrar al configurar su proyecto para usar Unity Addressable Asset System, y explicará cómo reconocerlos y solucionarlos rápidamente.

Para esta serie de recomendaciones trabajaremos con un ejemplo sencillo que se configura de la siguiente manera:
- Tenemos un script InventoryManager en la escena con referencias a nuestros tres activos de inventario: Prefabricados Espada, Espada de jefe, Escudo.
- Estos activos no son necesarios en todo momento durante el juego.
Puedes descargar los archivos del proyecto para este ejemplo enmi GitHub. Estamos utilizando el paquete de vista previaMemory Profilerpara ver la memoria en tiempo de ejecución. En Unity 2020 LTS, primero debes habilitar los paquetes de vista previa en la Configuración del proyecto antes de instalar este paquete desde el Administrador de paquetes.
Si está utilizando Unity 2021.1, seleccione la opciónAgregar paquete por nombreen el menú adicional (+) en la ventana del Administrador de paquetes. Utilice el nombre “com.unity.memoryprofiler”.
Comencemos con la implementación más básica y luego avancemos hacia el mejor enfoque para configurar nuestro contenido Addressables . Simplemente aplicaremos referencias duras (asignación directa en el inspector, rastreada porGUID) a nuestros prefabs en un MonoBehaviour que exista en nuestra escena.

Cuando se carga la escena, todos los objetos de la escena también se cargan en la memoria junto con sus dependencias. Esto significa que cada prefabricado listado en nuestro InventorySystem residirá en la memoria, junto con todas las dependencias de esos prefabricados (texturas, mallas, audio, etc.)
A medida que creamos una compilación y tomamos una instantánea con el Memory Profiler, podemos ver que las texturas de nuestros recursos ya están almacenadas en la memoria, aunque ninguna de ellas esté instanciada.

Problema: Hay activos en la memoria que actualmente no necesitamos. En un proyecto con una gran cantidad de elementos de inventario, esto generaría una presión considerable en la memoria en tiempo de ejecución.
Para evitar cargar activos no deseados, cambiaremos nuestro sistema de inventario para utilizar Addressables. El uso de referencias de activos en lugar de referencias directas evita que estos objetos se carguen junto con nuestra escena. Muevamos nuestros prefabricados de inventario a un grupo Addressables y cambiemos InventorySystem para instanciar y liberar objetos usando la API Addressables .

Construye el reproductor y toma una instantánea. Tenga en cuenta que ninguno de los activos está todavía en la memoria, lo cual es genial porque no se han instanciado.

Cree una instancia de todos los elementos para verlos aparecer correctamente con sus activos en la memoria.

Problema: Si instanciamos todos nuestros elementos y hacemos desaparecer la espada del jefe, seguiremos viendo la textura de la espada del jefe "BossSword_E" en la memoria, aunque no esté en uso. La razón de esto es que, si bien puedes cargar parcialmente paquetes de activos, es imposible descargarlos parcialmente de forma automática. Este comportamiento puede volverse particularmente problemático para paquetes que contienen muchos activos, como un único AssetBundle que incluye todos nuestros prefabricados de inventario. Ninguno de los activos del paquete se descargará hasta que todo el AssetBundle ya no sea necesario o hasta que llamemos a la costosa operación de CPU Resources.UnloadUnusedAssets().


Para solucionar este problema, debemos cambiar la forma en que organizamos nuestros AssetBundles. Si bien actualmente tenemos un único grupo direccionable que agrupa todos sus activos en un AssetBundle, podemos crear un AssetBundle para cada prefabricado. Estos AssetBundles más granulares alivian el problema de los paquetes grandes que retienen activos en la memoria que ya no necesitamos.
Realizar este cambio es fácil. Seleccione un grupo direccionable, seguido deEmpaquetado y carga de contenido>Opciones avanzadas>Modo de paquete, y vaya alInspectorpara cambiar el Modo de paquete deEmpaquetar juntosaEmpaquetar por separado.
Al utilizarEmpaquetar por separadopara crear este grupo direccionable, puede crear un AssetBundle para cada activo en el grupo direccionable.

Los activos y paquetes se verán así:

Ahora, volvamos a nuestra prueba original: Generar nuestros tres elementos y luego hacer desaparecer la espada del jefe ya no deja activos innecesarios en la memoria. Las texturas de la espada del jefe ahora están descargadas porque ya no es necesario el paquete completo.
Problema: Si generamos nuestros tres elementos y tomamos una captura de memoria, aparecerán activos duplicados en la memoria. Más específicamente, esto dará lugar a múltiples copias de las texturas “Sword_N” y “Sword_D”. ¿Cómo podría suceder esto si solo cambiamos el número de paquetes?

Para responder a esta pregunta, consideremos todo lo que incluye los tres paquetes que creamos. Si bien solo colocamos tres activos prefabricados en paquetes, hay activos adicionales implícitamente incluidos en esos paquetes como dependencias de los prefabricados. Por ejemplo, el recurso prefabricado de espada también tiene recursos de malla, material y textura que deben incluirse. Si estas dependencias no están incluidas explícitamente en otra parte de Addressables, entonces se agregan automáticamente a cada paquete que las necesita.

Los Addressables incluyen una ventana de análisis para ayudar a diagnosticar el diseño del paquete. Abrirventana>Gestión de activos>Addressables>Analizary ejecutar la reglaVista previa del diseño del paquete. Aquí, vemos que el paquete sword incluye explícitamente sword.prefab, pero hay muchas dependencias implícitas también incluidas en este paquete.

En la misma ventana, ejecuteCheck Duplicate Bundle Dependencies. Esta regla resalta los activos incluidos en múltiples paquetes de activos según nuestro diseño de Addressables actual.

Podemos evitar la duplicación de estos activos de dos maneras:
1. Coloque los prefabricados Sword, BossSword y Shield en el mismo paquete para que compartan dependencias, o
2. Incluya explícitamente los activos duplicados en algún lugar de Addressables
Queremos evitar colocar varios prefabricados de inventario en el mismo paquete para evitar que activos no deseados persistan en la memoria. Como tal, agregaremos los activos duplicados a sus propios paquetes (Paquete 4 y Paquete 5).

Además de analizar nuestros paquetes, las reglas de análisis pueden corregir automáticamente los activos infractores a través deCorregir reglas seleccionadas. Presione este botón para crear un nuevo grupo direccionable llamado “Aislamiento de activos duplicados”, que contiene los cuatro activos duplicados. Establezca el modo de paquete de este grupo enEmpaquetar por separadopara evitar que otros activos que ya no sean necesarios persistan en la memoria.

El uso de esta estrategia AssetBundle puede generar problemas a gran escala. Para cada AssetBundle cargado en un momento determinado, existe una sobrecarga de memoria para los metadatos de AssetBundle. Es probable que estos metadatos consuman una cantidad inaceptable de memoria si ampliamos esta estrategia actual a cientos o miles de elementos de inventario. Lea más sobre los metadatos de AssetBundle en losdocumentos de Addressables.
Vea el costo de memoria de metadatos de AssetBundle actual en Unity Profiler. Vaya al módulo de memoria y tome una instantánea de la memoria. Busque en la categoríaOtros>SerializedFile.

Hay una entrada SerializedFile en la memoria para cada AssetBundle cargado. Esta memoria son metadatos de AssetBundle y no los activos reales en los paquetes. Estos metadatos incluyen:
- Dos buffers de lectura de archivos
- Un árbol de tipos que enumera todos los tipos únicos incluidos en el paquete
- Una tabla de contenidos que apunta a los activos
De estos tres elementos, los buffers de lectura de archivos ocupan el mayor espacio. Estos buffers son de 64 KB cada uno en PS4, Switch y Windows RT, y de 7 KB en todas las demás plataformas. En el ejemplo anterior, 1.819 paquetes * 64 KB * 2 búferes = 227 MB solo para búferes.
Teniendo en cuenta que la cantidad de buffers se escala linealmente con la cantidad de AssetBundles, la solución simple para reducir la memoria es tener menos paquetes cargados en tiempo de ejecución. Sin embargo, anteriormente hemos evitado cargar paquetes grandes para impedir que activos no deseados persistan en la memoria. Entonces, ¿cómo reducimos la cantidad de paquetes manteniendo la granularidad?
Un primer paso sólido sería agrupar los activos en función de su uso en la aplicación. Si puede hacer suposiciones inteligentes basadas en su aplicación, entonces puede agrupar activos que sabe que siempre se cargarán y descargarán juntos, como aquellos activos de grupo basados en el nivel de juego en el que se encuentran.
Por otro lado, es posible que usted se encuentre en una situación en la que no pueda hacer suposiciones seguras sobre cuándo se necesitan o no sus activos. Si estás creando un juego de mundo abierto, por ejemplo, entonces no puedes simplemente agrupar todo del bioma del bosque en un solo paquete de activos porque tus jugadores podrían tomar un elemento del bosque y llevarlo entre biomas. Todo el paquete forestal permanece en la memoria porque el jugador todavía necesita un activo del bosque.
Afortunadamente, existe una manera de reducir la cantidad de paquetes manteniendo el nivel deseado de granularidad. Seamos más inteligentes en la forma en que deduplicamos nuestros paquetes.
La regla de análisis de deduplicación incorporada que ejecutamos detecta todos los activos que están en múltiples paquetes y los mueve de manera eficiente a un solo grupo direccionable. Al configurar ese grupo enEmpaquetar por separado, terminamos con un activo por paquete. Sin embargo, hay algunos activos duplicados que podemos agrupar de forma segura sin generar problemas de memoria. Considere el diagrama a continuación:

Sabemos que las texturas “Sword_N” y “Sword_D” son dependencias de los mismos paquetes (Bundle 1 y Bundle 2). Debido a que estas texturas tienen los mismos padres, podemos empaquetarlas juntas de forma segura sin causar problemas de memoria. Ambas texturas de espada deben estar siempre cargadas o descargadas. Nunca existe la preocupación de que una de las texturas pueda persistir en la memoria, ya que nunca hay un caso en el que usemos específicamente una textura y no la otra.
Podemos implementar esta lógica de deduplicación mejorada en nuestra propiaregla de análisis de Addressables. Trabajaremos a partir de la regla CheckForDupeDependencies.cs existente. Puede ver el código de implementación completo en elejemplo del Sistema de inventario. En este sencillo proyecto, simplemente redujimos el número total de paquetes de siete a cinco. Pero imagine un escenario en el que su aplicación tiene cientos, miles o incluso más activos duplicados en Addressables. Mientras trabajaba con Unknown Worlds Entertainment en un contrato de servicios profesionales para su juegoSubnautica, el proyecto inicialmente tenía un total de 8718 paquetes después de usar la regla de análisis de deduplicación incorporada. Redujimos esto a 5199 paquetes después de aplicar la regla personalizada para agrupar los activos deduplicados en función de sus paquetes principales. Puede conocer más sobre nuestro trabajo con el equipo eneste caso práctico.
Esto representa una reducción del 40% en la cantidad de paquetes, aunque siguen teniendo el mismo contenido y manteniendo el mismo nivel de granularidad. Esta reducción del 40% en la cantidad de paquetes redujo de manera similar el tamaño de SerializedFile en tiempo de ejecución en un 40% (de 311 MB a 184 MB).
El uso de Addressables puede reducir significativamente el consumo de memoria. Puede obtener una mayor reducción de memoria organizando sus AssetBundles para adaptarlos a su caso de uso. Después de todo, las reglas de análisis integradas son conservadoras para adaptarse a todas las aplicaciones. Escribir sus propias reglas de análisis puede automatizar el diseño del paquete y optimizarlo para su aplicación. Para detectar problemas de memoria, continúe creando perfiles con frecuencia y verifique la ventana Analizar para ver qué activos están incluidos explícita e implícitamente en sus paquetes. Consulte ladocumentación del Sistema de activos direccionablespara conocer más prácticas recomendadas, una guía para ayudarlo a comenzar y documentación API ampliada.
Si desea obtener más ayuda práctica para aprender cómo mejorar la gestión de su contenido con el Sistema de activos direccionables,comuníquese con Ventas para obtener informaciónsobre un curso de capacitación profesional.