Resolver LMKs en tu juego de Unity es un proceso sistemático:

Cómo obtener una instantánea de memoria
Usa el Generador de perfiles de Unity para obtener una instantánea de la memoria administrada por Unity. En la figura 2, se muestran las capas de administración de memoria que usa Unity para controlar la memoria en tu juego.

Memoria administrada
La administración de memoria de Unity implementa una capa de memoria controlada que usa un montón administrado y un recolector de basura para asignar y asignar memoria automáticamente. El sistema de memoria administrada es un entorno de secuencias de comandos en C# basado en Mono o IL2CPP. El beneficio del sistema de memoria administrada es que utiliza un recolector de elementos no utilizados para liberar automáticamente las asignaciones de memoria.
Memoria no administrada de C#
La capa de memoria C# no administrada proporciona acceso a la capa de memoria nativa, lo que permite un control preciso sobre las asignaciones de memoria mientras se usa código C#. Se puede acceder a esta capa de administración de memoria a través del espacio de nombres Unity.Collections y con funciones como UnsafeUtility.Malloc y UnsafeUtility.Free.
Memoria nativa
El núcleo interno de C/C++ de Unity usa un sistema de memoria nativo para administrar escenas, recursos, APIs de gráficos, controladores, subsistemas y búferes de complementos. Si bien el acceso directo está restringido, puedes manipular datos de forma segura con la API de C# de Unity y beneficiarte del código nativo eficiente. La memoria nativa rara vez requiere interacción directa, pero puedes supervisar su impacto en el rendimiento con el Generador de perfiles y ajustar la configuración para optimizar el rendimiento.
La memoria no se comparte entre C# y el código nativo, como se muestra en la figura 3. Los datos que requiere C# se asignan en el espacio de memoria administrado cada vez que se necesitan.
Para que el código del juego administrado (C#) acceda a los datos de memoria nativa del motor, por ejemplo, una llamada a GameObject.transform realiza una llamada nativa para acceder a los datos de memoria en el área nativa y, luego, devuelve valores a C# con Bindings. Las vinculaciones garantizan las convenciones de llamada adecuadas para cada plataforma y controlan la serialización automática de los tipos administrados en sus equivalentes nativos.
Esto solo sucede la primera vez, ya que la shell administrada para acceder a la propiedad transform se conserva en el código nativo. Almacenar en caché la propiedad de transformación puede reducir la cantidad de llamadas de ida y vuelta entre el código administrado y el nativo, pero la utilidad del almacenamiento en caché depende de la frecuencia con la que se usa la propiedad. Además, ten en cuenta que Unity no copia partes de la memoria nativa en la memoria administrada cuando accedes a estas APIs.

Para obtener más información, consulta la Introducción a la memoria en Unity.
Además, establecer un presupuesto de memoria es fundamental para que el juego se ejecute sin problemas, y la implementación de un sistema de informes o análisis del consumo de memoria garantiza que cada nueva versión no exceda el presupuesto de memoria. Integrar las pruebas de modo de juego en tu integración continua (CI) para verificar el consumo de memoria en áreas específicas del juego es otra estrategia para obtener mejores estadísticas.
Cómo administrar activos
Esta es la parte más impactante y práctica del consumo de memoria. Genera el perfil lo antes posible.
El uso de memoria en los juegos para Android puede variar significativamente según el tipo de juego, la cantidad y los tipos de recursos, y las estrategias de optimización de la memoria. Sin embargo, los colaboradores comunes del uso de la memoria suelen incluir texturas, mallas, archivos de audio, sombreadores, animaciones y secuencias de comandos.
Detecta recursos duplicados
El primer paso es detectar los recursos mal configurados y los recursos duplicados con el Memory Profiler, una herramienta de informes de compilación o el Project Auditor.
Texturas
Analiza la compatibilidad de tu juego con los dispositivos y decide el formato de textura correcto. Puedes dividir los paquetes de texturas para dispositivos de gama alta y gama baja con Play Asset Delivery, Addressable o un proceso más manual con un AssetBundle.
Sigue las recomendaciones más conocidas disponibles en Optimize Your Mobile Game Performance y en la publicación del foro Optimising Unity Texture Import Settings. Luego, prueba estas soluciones:
Comprime las texturas con formatos ASTC para reducir el uso de memoria y experimenta con una tasa de bloques más alta, como 8 x 8.
Si es necesario usar ETC2, empaqueta tus texturas en Atlas. Colocar varias texturas en una sola garantiza su potencia de dos (POT), puede reducir las llamadas de dibujo y acelerar la renderización.
Optimiza el formato y el tamaño de la textura de RenderTarget. Evita las texturas de resolución innecesariamente alta. Usar texturas más pequeñas en dispositivos móviles ahorra memoria.
Usa el empaquetado de canales de texturas para ahorrar memoria de texturas.
Mallas y modelos
Comienza por verificar la configuración fundamental (página 27) y verifica estos parámetros de configuración de importación de malla:
- Combina las mallas redundantes y más pequeñas.
- Reduce el recuento de vértices de los objetos en las escenas (por ejemplo, objetos estáticos o distantes).
- Genera grupos de nivel de detalle (LOD) para los recursos con una gran cantidad de geometría.
Materiales y sombreadores
- Quita las variantes de sombreador no utilizadas de forma programática durante el proceso de compilación.
- Consolida las variantes de sombreadores que se usan con frecuencia en uber shaders para evitar la duplicación de sombreadores.
- Habilita la carga dinámica de sombreadores para abordar la gran cantidad de memoria que ocupan los sombreadores precargados en la VRAM o la RAM. Sin embargo, presta atención si la compilación del sombreador está causando interrupciones en los fotogramas.
- Usa la carga dinámica de sombreadores para evitar que se carguen todas las variantes. Para obtener más información, consulta la entrada de blog Mejoras en los tiempos de compilación y el uso de memoria de los sombreadores.
- Usa la creación de instancias de materiales de forma adecuada aprovechando
MaterialPropertyBlocks
.
Audio
Comienza por verificar la configuración fundamental (página 41) y verifica estos parámetros de configuración de importación de malla:
- Quita las referencias
AudioClip
redundantes o sin usar cuando emplees motores de audio de terceros, como FMOD o Wwise. - Precarga datos de audio. Inhabilita la carga previa de los clips que no se requieren de inmediato durante el tiempo de ejecución o el inicio de la escena. Esto ayuda a reducir la sobrecarga de memoria durante la inicialización de la escena.
Animaciones
- Ajusta la configuración de compresión de animación de Unity para minimizar la cantidad de fotogramas clave y eliminar los datos redundantes.
- Reducción de fotogramas clave: Quita automáticamente los fotogramas clave innecesarios
- Compresión de cuaterniones: Comprime los datos de rotación para reducir el uso de memoria
Puedes ajustar la configuración de compresión en Animation Import Settings en la pestaña Rig o Animation.
Reutiliza los clips de animación en lugar de duplicarlos para diferentes objetos.
Usa Animator Override Controllers para reutilizar un Animator Controller y reemplazar clips específicos para diferentes personajes.
Genera animaciones basadas en la física: Si tus animaciones se basan en la física o son procedimentales, genera clips de animación para evitar cálculos en el tiempo de ejecución.
Optimiza la estructura esquelética: Usa menos huesos en la estructura para reducir la complejidad y el consumo de memoria.
- Evita usar demasiados huesos para objetos pequeños o estáticos.
- Si ciertos huesos no están animados o no son necesarios, quítalos del esqueleto.
Se redujo la duración del clip de animación.
- Recorta los clips de animación para incluir solo los fotogramas necesarios. Evita almacenar animaciones sin usar o demasiado largas.
- Usa animaciones en bucle en lugar de crear clips largos para movimientos repetidos.
Asegúrate de que solo haya un componente de animación conectado o activado. Por ejemplo, inhabilita o quita los componentes de Legacy animation si usas Animator.
Evita usar Animator si no es necesario. Para los efectos visuales simples, usa bibliotecas de interpolación o implementa el efecto visual en una secuencia de comandos. El sistema de animación puede consumir muchos recursos, en especial en dispositivos móviles de gama baja.
Usa el sistema de trabajos para las animaciones cuando manejes una gran cantidad de animaciones, ya que este sistema se rediseñó por completo para ser más eficiente en el uso de la memoria.
Ambientes
Cuando se cargan escenas nuevas, se incorporan recursos como dependencias. Sin embargo, sin una administración adecuada del ciclo de vida de los recursos, los contadores de referencia no supervisan estas dependencias. Como resultado, es posible que los recursos permanezcan en la memoria incluso después de que se hayan descargado las escenas no utilizadas, lo que provoca la fragmentación de la memoria.
- Usa el agrupamiento de objetos de Unity para reutilizar instancias de GameObject para elementos recurrentes del juego, ya que el agrupamiento de objetos usa una pila para contener una colección de instancias de objetos para su reutilización y no es seguro para subprocesos. Minimizar
Instantiate
yDestroy
mejora el rendimiento de la CPU y la estabilidad de la memoria. - Descarga de recursos:
- Descarga los recursos de forma estratégica durante los momentos menos críticos, como las pantallas de presentación o de carga.
- El uso frecuente de
Resources.UnloadUnusedAssets
provoca picos en el procesamiento de la CPU debido a las grandes operaciones internas de supervisión de dependencias. - Verifica si hay picos grandes de CPU en el marcador de perfil GC.MarkDependencies.
Quita o reduce su frecuencia de ejecución y, en su lugar, descarga manualmente recursos específicos con Resources.UnloadAsset en lugar de depender de
Resources.UnloadUnusedAssets()
, que abarca todo.
- Reestructura las escenas en lugar de usar constantemente Resources.UnloadUnusedAssets.
- Llamar a
Resources.UnloadUnusedAssets()
paraAddressables
puede descargar sin querer los paquetes cargados de forma dinámica. Administra con cuidado el ciclo de vida de los recursos cargados de forma dinámica.
Varios
Fragmentación causada por transiciones de escena: Cuando se llama al método
Resources.UnloadUnusedAssets()
, Unity hace lo siguiente:- Libera memoria para los recursos que ya no se usan
- Ejecuta una operación similar a un recolector de basura para verificar si hay activos no utilizados en el montón de objetos administrados y nativos, y los descarga.
- Limpia la memoria de texturas, mallas y activos, siempre que no exista ninguna referencia activa.
AssetBundle
oAddressable
: Realizar cambios en esta área es complejo y requiere un esfuerzo colectivo del equipo para implementar las estrategias. Sin embargo, una vez que se dominan estas estrategias, mejoran significativamente el uso de la memoria, reducen el tamaño de la descarga y disminuyen los costos de la nube. Para obtener más información sobre la administración de recursos en Unity conAddressables
, consultaAddressables
.Dependencias compartidas centralizadas: Agrupa las dependencias compartidas, como sombreadores, texturas y fuentes, de forma sistemática en paquetes o grupos
Addressable
dedicados. Esto reduce la duplicación y garantiza que los recursos innecesarios se descarguen de manera eficiente.Usa
Addressables
para el seguimiento de dependencias: Addressables simplifica la carga y descarga, y puede descargar automáticamente las dependencias a las que ya no se hace referencia. La transición aAddressables
para la administración de contenido y la resolución de dependencias puede ser una solución viable, según el caso específico del juego. Analiza las cadenas de dependencias con la herramienta Analyze para identificar duplicados o dependencias innecesarias. Como alternativa, consulta las herramientas de datos de Unity si usas AssetBundles.TypeTrees
: Si los objetosAddressables
yAssetBundles
de tu juego se compilan y se implementan con la misma versión de Unity que el reproductor, y no requieren compatibilidad con versiones anteriores con otras compilaciones del reproductor, considera inhabilitar la escritura deTypeTree
, lo que debería reducir el tamaño del paquete y la huella de memoria del objeto de archivo serializado. Modifica el proceso de compilación en el parámetro de configuración del paquete local Addressables ContentBuildFlags a DisableWriteTypeTree.
Escribe código compatible con el recolector de elementos no utilizados
Unity utiliza la recolección de elementos no utilizados (GC) para administrar la memoria, ya que identifica y libera automáticamente la memoria no utilizada. Si bien la GC es esencial, puede causar problemas de rendimiento (por ejemplo, picos en la velocidad de fotogramas) si no se controla de forma adecuada, ya que este proceso puede pausar el juego momentáneamente, lo que genera problemas de rendimiento y una experiencia del usuario no óptima.
Consulta el manual de Unity para conocer técnicas útiles para reducir la frecuencia de las asignaciones de montón administrado y la UnityPerformanceTuningBible, página 271, para ver ejemplos.
Reduce las asignaciones del recolector de basura:
- Evita LINQ, las expresiones lambda y los cierres, que asignan memoria del montón.
- Usa
StringBuilder
para cadenas mutables en lugar de la concatenación de cadenas. - Reutiliza las colecciones llamando a
COLLECTIONS.Clear()
en lugar de volver a instanciarlas.
Puedes obtener más información en el libro electrónico Ultimate Guide to Profiling Unity Games.
Administra las actualizaciones del lienzo de la IU:
- Cambios dinámicos en los elementos de la IU: Cuando se actualizan elementos de la IU, como las propiedades de Text, Image o
RectTransform
(por ejemplo, cambiar el contenido del texto, cambiar el tamaño de los elementos o animar las posiciones), el motor puede asignar memoria para objetos temporales. - Asignaciones de cadenas: Los elementos de la IU, como Text, a menudo requieren actualizaciones de cadenas, ya que las cadenas son inmutables en la mayoría de los lenguajes de programación.
- Lienzo sucio: Cuando cambia algo en un lienzo (por ejemplo, se cambia el tamaño, se habilitan y se inhabilitan elementos, o se modifican las propiedades de diseño), es posible que todo el lienzo o una parte de él se marquen como sucios y se vuelvan a compilar. Esto puede activar la creación de estructuras de datos temporales (por ejemplo, datos de malla, búferes de vértices o cálculos de diseño), lo que aumenta la generación de basura.
- Actualizaciones complejas o frecuentes: Si el lienzo tiene una gran cantidad de elementos o se actualiza con frecuencia (por ejemplo, en cada fotograma), estas recompilaciones pueden generar una pérdida significativa de memoria.
- Cambios dinámicos en los elementos de la IU: Cuando se actualizan elementos de la IU, como las propiedades de Text, Image o
Habilita la GC incremental para reducir los picos de recopilación grandes distribuyendo las limpiezas de asignación en varios fotogramas. Genera un perfil para verificar si esta opción mejora el rendimiento y la huella de memoria de tu juego.
Si tu juego requiere un enfoque controlado, establece el modo de recolección de basura en manual. Luego, en un cambio de nivel o en otro momento sin juego activo, llama a la recolección de basura.
Invoca llamadas de recolección de elementos no utilizados manuales GC.Collect() para las transiciones de estado del juego (por ejemplo, el cambio de nivel).
Optimiza los arrays comenzando con prácticas de código simples y, si es necesario, usando arrays nativos o otros contenedores nativos para arrays grandes.
Supervisa los objetos administrados con herramientas como el Generador de perfiles de memoria de Unity para hacer un seguimiento de las referencias de objetos no administrados que persisten después de la destrucción.
Usa un marcador de Profiler para enviarlo a la herramienta de informes de rendimiento y obtener un enfoque automatizado.
Evita las fugas y la fragmentación de la memoria
Fugas de memoria
En el código de C#, cuando existe una referencia a un objeto de Unity después de que se destruyó el objeto, el objeto wrapper administrado, conocido como Managed Shell, permanece en la memoria. La memoria nativa asociada a la referencia se libera cuando se descarga la escena o cuando se destruye el GameObject al que se adjunta la memoria, o cualquiera de sus objetos principales, a través del método Destroy()
. Sin embargo, si no se borraron otras referencias a Scene o GameObject, es posible que la memoria administrada persista como un objeto Shell filtrado. Para obtener más detalles sobre los objetos de Managed Shell, consulta el manual de Objetos de Managed Shell.
Además, las pérdidas de memoria pueden deberse a suscripciones a eventos, expresiones lambda y cierres, concatenaciones de cadenas y una administración inadecuada de objetos agrupados:
- Para comenzar, consulta Cómo encontrar pérdidas de memoria para comparar correctamente las instantáneas de memoria de Unity.
- Verifica si hay suscripciones a eventos y pérdidas de memoria. Si los objetos se suscriben a eventos (por ejemplo, a través de delegados o UnityEvents), pero no se anulan la suscripción correctamente antes de destruirse, el administrador o publicador de eventos puede conservar referencias a esos objetos. Esto evita que se recolecten esos objetos como elementos no utilizados, lo que genera fugas de memoria.
- Supervisa los eventos de clase singleton o global que no se anulan al destruir el objeto. Por ejemplo, anular la suscripción o desvincular delegados en destructores de objetos.
- Asegúrate de que la destrucción de los objetos agrupados anule por completo las referencias a los componentes de malla de texto, las texturas y los GameObjects principales.
- Ten en cuenta que, cuando compares instantáneas del Generador de perfiles de memoria de Unity y observes una diferencia en el consumo de memoria sin una razón clara, la diferencia puede deberse al controlador de gráficos o al sistema operativo en sí.
Fragmentación de memoria
La fragmentación de la memoria se produce cuando se liberan muchas asignaciones pequeñas en un orden aleatorio. Las asignaciones de montón se realizan de forma secuencial, lo que significa que se crean nuevos fragmentos de memoria cuando el fragmento anterior se queda sin espacio. Por lo tanto, los objetos nuevos no rellenan las áreas vacías de los fragmentos antiguos, lo que genera fragmentación. Además, las asignaciones temporales grandes pueden causar una fragmentación permanente durante la sesión de un juego.
Este problema es particularmente grave cuando se realizan asignaciones grandes de corta duración cerca de asignaciones de larga duración.
Agrupa las asignaciones según su vida útil. Idealmente, las asignaciones de larga duración deben realizarse juntas, al principio del ciclo de vida de la aplicación.
Observadores y administradores de eventos
- Además del problema mencionado en la sección (Fugas de memoria)77, con el tiempo, las fugas de memoria pueden contribuir a la fragmentación, ya que dejan memoria sin usar asignada a objetos que ya no están en uso.
- Asegúrate de que la destrucción de los objetos agrupados anule por completo las referencias a los componentes de malla de texto, las texturas y el
GameObjects
principal. - Los administradores de eventos suelen crear y almacenar listas o diccionarios para administrar las suscripciones a eventos. Si estos crecen y se reducen de forma dinámica durante el tiempo de ejecución, pueden contribuir a la fragmentación de la memoria debido a las asignaciones y desasignaciones frecuentes.
Código
- A veces, las corrutinas asignan memoria, lo que se puede evitar fácilmente almacenando en caché la instrucción de devolución de IEnumerator en lugar de declarar una nueva cada vez.
- Supervisa continuamente los estados del ciclo de vida de los objetos agrupados para evitar mantener referencias fantasma de
UnityEngine.Object
.
Recursos
- Usa sistemas de resguardo dinámicos para experiencias de juegos basadas en texto y evita precargar todas las fuentes para casos en varios idiomas.
- Organiza los recursos (por ejemplo, texturas y partículas) por tipo y ciclo de vida esperado.
- Condensa los recursos con atributos de ciclo de vida inactivo, como imágenes de la IU redundantes y mallas estáticas.
Asignaciones basadas en el ciclo de vida
- Asigna recursos de larga duración al inicio del ciclo de vida de la aplicación para garantizar asignaciones compactas.
- Usa NativeCollections o asignadores personalizados para estructuras de datos transitorias o que consumen mucha memoria (por ejemplo, clústeres de física).
Acción de memoria relacionada con código y ejecutables
El ejecutable y los complementos del juego también afectan el uso de memoria.
Metadatos de IL2CPP
IL2CPP genera metadatos para cada tipo (por ejemplo, clases, tipos genéricos y delegados) en el momento de la compilación, que luego se usan en el tiempo de ejecución para la reflexión, la verificación de tipos y otras operaciones específicas del tiempo de ejecución. Estos metadatos se almacenan en la memoria y pueden contribuir de manera significativa a la huella de memoria total de la aplicación. La caché de metadatos de IL2CPP contribuye de manera significativa a los tiempos de carga e inicialización. Además, IL2CPP no elimina duplicados de ciertos elementos de metadatos (por ejemplo, tipos genéricos o información serializada), lo que puede generar un uso excesivo de la memoria. Esto se agrava por el uso repetitivo o redundante de tipos en el proyecto.
Los metadatos de IL2CPP se pueden reducir de las siguientes maneras:
- Evita el uso de APIs de reflexión, ya que pueden ser un factor importante en las asignaciones de metadatos de IL2CPP.
- Cómo inhabilitar los paquetes integrados
- Implementación del uso compartido genérico completo de Unity 2022, que debería ayudar a reducir la sobrecarga causada por los genéricos. Sin embargo, para ayudar a reducir aún más las asignaciones, disminuye el uso de genéricos.
Eliminación de código
Además de reducir el tamaño de la compilación, la eliminación de código también disminuye el uso de memoria. Cuando se compila con el backend de secuencias de comandos IL2CPP, la eliminación de código de bytes administrado (que se activa de forma predeterminada) quita el código sin usar de los ensamblados administrados. El proceso funciona definiendo ensamblados raíz y, luego, usando el análisis de código estático para determinar qué otro código administrado usan esos ensamblados raíz. Se quita cualquier código al que no se pueda acceder. Si deseas obtener más información sobre la eliminación de código administrado, consulta la entrada de blog TTales from the optimization trenches: Better managed code stripping with Unity 2020 LTS y la documentación de eliminación de código administrado.
Asignadores nativos
Experimenta con asignadores de memoria nativos para ajustar los asignadores de memoria. Si el juego tiene poca memoria, usa bloques de memoria más pequeños, incluso si esto implica asignadores más lentos. Consulta el ejemplo de asignador de pila dinámico para obtener más información.
Administra complementos y SDKs nativos
Busca el complemento problemático: Quita cada complemento y compara las instantáneas de la memoria del juego. Esto implica inhabilitar gran parte de la funcionalidad del código con Símbolos de definición de secuencias de comandos y refactorizar clases altamente acopladas con interfaces. Consulta Mejora tu código con patrones de programación de juegos para facilitar el proceso de inhabilitación de dependencias externas sin que tu juego deje de ser jugable.
Comunícate con el autor del complemento o del SDK: La mayoría de los complementos no son de código abierto.
Reproduce el uso de memoria del complemento: Puedes escribir un complemento simple (usa este complemento de Unity como referencia) que realice asignaciones de memoria. Inspecciona las instantáneas de memoria con Android Studio (ya que Unity no hace un seguimiento de estas asignaciones) o llama a la clase
MemoryInfo
y al métodoRuntime.totalMemory()
en el mismo proyecto.
Un complemento de Unity asigna memoria nativa y de Java. A continuación, se explica cómo hacerlo:
Java
byte[] largeObject = new byte[1024 * 1024 * megaBytes];
list.add(largeObject);
Nativa
char* buffer = new char[megabytes * 1024 * 1024];
// Random data to fill the buffer
for (int i = 1; i < megabytes * 1024 * 1024; ++i) {
buffer[i] = 'A' + (i % 26); // Fill with letters A-Z
}