Optimiza el rendimiento de tu juego para dispositivos móviles: Consejos sobre perfilado, memoria y arquitectura de código de los mejores ingenieros de Unity

THOMAS KROGH-JACOBSEN / UNITY TECHNOLOGIESSenior Technical Content Marketing Manager
Jun 23, 2021|15 min.
Optimiza el rendimiento de tu juego para dispositivos móviles: Consejos sobre perfilado, memoria y arquitectura de código de los mejores ingenieros de Unity
Para tu comodidad, tradujimos esta página mediante traducción automática. No podemos garantizar la precisión ni la confiabilidad del contenido traducido. Si tienes alguna duda sobre la precisión del contenido traducido, consulta la versión oficial en inglés de la página web.
Nuestro equipo de producción de Unity Studio conoce el código fuente a la perfección y apoya a una multitud de clientes de Unity para que puedan aprovechar al máximo el motor. En su trabajo, se sumergen profundamente en los proyectos de los creadores para ayudar a identificar puntos donde se podría optimizar el rendimiento para una mayor velocidad, estabilidad y eficiencia. Nos sentamos con este equipo, compuesto por los ingenieros de software más experimentados de Unity, y les pedimos que compartieran parte de su experiencia sobre la optimización de juegos móviles.

A medida que nuestros ingenieros comenzaron a compartir su perspectiva sobre la optimización de juegos móviles, nos dimos cuenta rápidamente de que había demasiada información valiosa para el único post de blog que habíamos planeado. En su lugar, decidimos convertir su montaña de conocimientos en un e-book completo (que puedes descargar aquí), así como en una serie de publicaciones de blog que destacan algunos de estos más de 75 consejos prácticos.

Iniciamos la primera publicación de esta serie enfocándonos en cómo puedes mejorar el rendimiento de tu juego con perfilado, memoria y arquitectura de código. En las próximas semanas, seguiremos con dos publicaciones más: la primera cubriendo la física de la interfaz de usuario, seguida de otra sobre audio y activos, configuración del proyecto y gráficos.

¿Quieres ver la serie completa ahora? Descarga el e-book completo gratis.

¡Vamos a profundizar!

Perfilado

¿Qué mejor lugar para comenzar que el perfilado y el proceso de recopilación y actuación sobre los datos de rendimiento móvil? Aquí es donde realmente comienza la optimización del rendimiento móvil.

Perfila temprano, a menudo y en el dispositivo objetivo

El Unity Profiler proporciona información esencial sobre el rendimiento de tu aplicación, pero no puede ayudarte si no lo usas. Perfila tu proyecto temprano en el desarrollo, no solo cuando estés cerca de lanzarlo. Investiga los fallos o picos tan pronto como aparezcan. A medida que desarrollas una "firma de rendimiento" para tu proyecto, podrás detectar nuevos problemas más fácilmente.

Mientras que el perfilado en el Editor puede darte una idea del rendimiento relativo de diferentes sistemas en tu juego, el perfilado en cada dispositivo te brinda la oportunidad de obtener información más precisa. Perfila una versión de desarrollo en los dispositivos de destino siempre que sea posible. Recuerda perfilar y optimizar tanto para los dispositivos de especificaciones más altas como para los más bajas que planeas soportar.

Junto con el Profiler de Unity, puedes aprovechar herramientas nativas de iOS y Android para pruebas de rendimiento adicionales en sus respectivos motores:

Ciertos hardware pueden aprovechar herramientas de perfilado adicionales (por ejemplo, Arm Mobile Studio, Intel VTune, y Snapdragon Profiler). Consulta Perfilado de Aplicaciones Hechas con Unity para más información.

Enfócate en optimizar las áreas correctas

No adivines ni hagas suposiciones sobre lo que está ralentizando el rendimiento de tu juego. Usa el Profiler de Unity y herramientas específicas de la plataforma para localizar la fuente precisa de un retraso.

Por supuesto, no todas las optimizaciones descritas aquí se aplicarán a tu aplicación. Algo que funciona bien en un proyecto puede no traducirse al tuyo. Identifica cuellos de botella genuinos y concentra tus esfuerzos en lo que beneficia tu trabajo.

Entiende cómo funciona el profiler de Unity

El Profiler de Unity puede ayudarte a detectar las causas de cualquier retraso o congelamiento en tiempo de ejecución y a entender mejor lo que está sucediendo en un fotograma específico, o punto en el tiempo. Habilita las pistas de CPU y Memoria por defecto. Puedes monitorear Módulos de Profiler suplementarios como Renderizador, Audio y Física, según sea necesario para tu juego (por ejemplo, juegos pesados en física o basados en música).

Usa el Profiler de Unity para probar el rendimiento y la asignación de recursos para tu aplicación.
Usa el Profiler de Unity para probar el rendimiento y la asignación de recursos para tu aplicación.

Construye la aplicación en tu dispositivo marcando Versión de Desarrollo y Autoconectar Profiler, o conéctate manualmente para acelerar el tiempo de inicio de la aplicación.

Configuraciones de compilación en el editor

Elige el objetivo de plataforma para perfilar. El botón Grabar rastrea varios segundos de la reproducción de tu aplicación (300 fotogramas por defecto). Ve a Unity > Preferencias > Análisis > Profiler > Conteo de fotogramas para aumentar esto hasta 2000 si necesitas capturas más largas. Si bien esto significa que el Editor de Unity tiene que hacer más trabajo de CPU y ocupar más memoria, puede ser útil dependiendo de tu escenario específico.

Este es un perfilador basado en instrumentación que perfila los tiempos de código explícitamente envueltos en ProfileMarkers (como los métodos Start o Update de MonoBehaviour, o llamadas a API específicas). Además, al usar la configuración de Perfilado Profundo, Unity puede perfilar el inicio y el final de cada llamada a función en tu código de script para decirte exactamente qué parte de tu aplicación está causando una desaceleración.

Vista de línea de tiempo en el editor
Usa la vista de línea de tiempo para determinar si estás limitado por la CPU o por la GPU.

Al perfilar tu juego, recomendamos que cubras tanto los picos como el costo de un fotograma promedio en tu juego. Entender y optimizar operaciones costosas que ocurren en cada fotograma puede ser más útil para aplicaciones que se ejecutan por debajo de la tasa de fotogramas objetivo. Al buscar picos, explora primero las operaciones costosas (por ejemplo, física, IA, animación) y la recolección de basura.

Haz clic en la ventana para analizar un fotograma específico. A continuación, usa la vista Línea de tiempo o Jerarquía para lo siguiente:

  • Línea de tiempo muestra el desglose visual del tiempo para un fotograma específico. Esto te permite visualizar cómo las actividades se relacionan entre sí y a través de diferentes hilos. Usa esta opción para determinar si estás limitado por la CPU o por la GPU.
  • Jerarquía muestra la jerarquía de ProfileMarkers, agrupados juntos. Esto te permite ordenar las muestras según el costo de tiempo en milisegundos (Tiempo ms y Auto ms). También puedes contar el número de Llamadas a una función y la memoria del montón administrado (GC Asignado) en el fotograma.
Ordenando los ProfileMarkers por costo de tiempo
La vista de Jerarquía te permite ordenar los ProfileMarkers por costo de tiempo.

Lee una visión completa del Unity Profiler aquí. Los nuevos en la profilación también pueden ver esta Introducción a la Profilación en Unity.

Antes de optimizar cualquier cosa en tu proyecto, guarda el archivo .data del Profiler. Implementa tus cambios y compara el .data guardado antes y después de la modificación. Confía en este ciclo para mejorar el rendimiento: perfilar, optimizar y comparar. Luego, enjuaga y repite.

Usa el Analizador de Perfiles

Esta herramienta te permite agregar múltiples cuadros de datos del Profiler, luego localizar cuadros de interés. ¿Quieres ver qué sucede con el Profiler después de hacer un cambio en tu proyecto? La vista Comparar te permite cargar y diferenciar dos conjuntos de datos, para que puedas probar cambios y mejorar su resultado. El Analizador de Perfiles está disponible a través del Administrador de Paquetes de Unity.

Mirada más profunda al Analizador de Perfiles en el editor
Profundiza aún más en los cuadros y datos de marcadores con el Analizador de Perfiles, que complementa el Profiler existente.

Trabaja con un presupuesto de tiempo específico por cuadro

Cada cuadro tendrá un presupuesto de tiempo basado en tus cuadros por segundo (fps) objetivo. Idealmente, una aplicación que funcione a 30 fps permitirá aproximadamente 33.33 ms por cuadro (1000 ms / 30 fps). Del mismo modo, un objetivo de 60 fps deja 16.66 ms por cuadro.

Los dispositivos pueden exceder este presupuesto por cortos períodos de tiempo (por ejemplo, para escenas cinemáticas o secuencias de carga), pero no por una duración prolongada.

Ten en cuenta la temperatura del dispositivo

Sin embargo, para dispositivos móviles, no recomendamos usar este tiempo máximo de manera constante, ya que el dispositivo puede sobrecalentarse y el sistema operativo puede reducir la velocidad térmica de la CPU y la GPU. Recomendamos que uses solo alrededor del 65% del tiempo disponible para permitir el enfriamiento entre fotogramas. Un presupuesto típico de fotogramas será de aproximadamente 22 ms por fotograma a 30 fps y 11 ms por fotograma a 60 fps.

La mayoría de los dispositivos móviles no tienen refrigeración activa como sus contrapartes de escritorio. Los niveles de calor físico pueden impactar directamente en el rendimiento.

Si el dispositivo está funcionando a altas temperaturas, el Profiler podría percibir e informar un rendimiento deficiente, incluso si no es motivo de preocupación a largo plazo. Para combatir el sobrecalentamiento durante el perfilado, perfila en ráfagas cortas. Esto enfría el dispositivo y simula condiciones del mundo real. Nuestra recomendación general es mantener el dispositivo fresco durante 10-15 minutos antes de perfilar nuevamente.

Determina si estás limitado por la GPU o por la CPU

El Profiler puede decirte si tu CPU está tardando más de lo que se asignó en el presupuesto de fotogramas, o si el culpable es tu GPU. Lo hace emitiendo marcadores con el prefijo Gfx de la siguiente manera:

  • Si ves el marcador Gfx.WaitForCommands, significa que el hilo de renderizado está listo, pero podrías estar esperando un cuello de botella en el hilo principal.
  • Si frecuentemente encuentras Gfx.WaitForPresent, significa que el hilo principal estaba listo pero estaba esperando que la GPU presentara el fotograma.
Memoria

Unity emplea gestión automática de memoria para tu código y scripts generados por el usuario. Pequeñas piezas de datos, como variables locales de tipo valor, se asignan a la pila. Piezas de datos más grandes y almacenamiento a largo plazo se asignan al montón administrado.

El recolector de basura identifica y desasigna periódicamente la memoria del montón no utilizada. Mientras esto se ejecuta automáticamente, el proceso de examinar todos los objetos en el montón puede hacer que el juego se entrecorte o funcione lentamente.

Optimizar el uso de memoria significa ser consciente de cuándo asignas y desasignas memoria del montón, y cómo minimizas el efecto de la recolección de basura. Consulta Entendiendo el montón administrado para más información.

Una mirada al Memory Profiler en el editor
Captura, inspecciona y compara instantáneas en el Memory Profiler.

Usa el Memory Profiler

Este complemento separado (disponible como un paquete Experimental o de Vista Previa en el Administrador de Paquetes) puede tomar una instantánea de tu memoria del montón administrado, para ayudarte a identificar problemas como la fragmentación y las fugas de memoria.

Haz clic en la vista del Mapa de Árbol para rastrear una variable hasta el objeto nativo que retiene la memoria. Aquí, puedes identificar problemas comunes de consumo de memoria, como texturas excesivamente grandes o activos duplicados.

Aprende a aprovechar el Memory Profiler en Unity para mejorar el uso de memoria. También puedes consultar nuestra documentación oficial del Memory Profiler.

Reduce el impacto de la recolección de basura (GC)

Unity utiliza el recolector de basura Boehm-Demers-Weiser, que detiene la ejecución de tu código de programa y solo reanuda la ejecución normal una vez que su trabajo está completo.

Ten en cuenta ciertas asignaciones innecesarias en el montón, que podrían causar picos de GC:

  • Cadenas: En C#, las cadenas son tipos de referencia, no tipos de valor. Reduce la creación o manipulación innecesaria de cadenas. Evita analizar archivos de datos basados en cadenas como JSON y XML; almacena datos en ScriptableObjects o formatos como MessagePack o Protobuf en su lugar. Usa la clase StringBuilder si necesitas construir cadenas en tiempo de ejecución.
  • Llamadas a funciones de Unity: Algunas funciones crean asignaciones en el montón. Cachea referencias a arreglos en lugar de asignarlos en medio de un bucle. Además, aprovecha ciertas funciones que evitan generar basura. Por ejemplo, usa GameObject.CompareTag en lugar de comparar manualmente una cadena con GameObject.tag (ya que devolver una nueva cadena crea basura).
  • Boxeo: Evita pasar una variable de tipo valor en lugar de una variable de tipo referencia. Esto crea un objeto temporal, y la posible basura que viene con ello convierte implícitamente el tipo de valor en un tipo objeto (por ejemplo, int i = 123; object o = i). En su lugar, intenta proporcionar sobrecargas concretas con el tipo de valor que deseas pasar. Los genéricos también se pueden usar para estas sobrecargas.
  • Corutinas: Aunque yield no produce basura, crear un nuevo objeto WaitForSeconds sí lo hace. Cachea y reutiliza el objeto WaitForSeconds en lugar de crearlo en la línea yield.
  • LINQ y Expresiones Regulares: Ambos generan basura debido al boxeo detrás de escena. Evita LINQ y Expresiones Regulares si el rendimiento es un problema. Escribe bucles for y usa listas como alternativa a crear nuevos arreglos.

Recoge basura de tiempo si es posible

Si estás seguro de que una pausa de recolección de basura no afectará un punto específico en tu juego, puedes activar la recolección de basura con System.GC.Collect.

Consulta Entendiendo la Gestión Automática de Memoria para ejemplos de cómo usar esto a tu favor.

Usa el recolector de basura incremental para dividir la carga de trabajo del GC

En lugar de crear una única y larga interrupción durante la ejecución de su programa, la recolección de basura incremental utiliza múltiples interrupciones mucho más cortas que distribuyen la carga de trabajo a lo largo de muchos fotogramas. Si la recolección de basura está afectando el rendimiento, intente habilitar esta opción para ver si puede reducir el problema de los picos de GC. Utilice el Analizador de Perfiles para verificar su beneficio para su aplicación.

Una mirada al Recolector de Basura Incremental
Utilice el Recolector de Basura Incremental para reducir los picos de GC.
Programación y arquitectura de código

El PlayerLoop de Unity contiene funciones para interactuar con el núcleo del motor del juego. Esta estructura incluye varios sistemas que manejan la inicialización y las actualizaciones por fotograma. Todos sus scripts dependerán de este PlayerLoop para crear la jugabilidad.

Al perfilar, verá el código de usuario de su proyecto bajo el PlayerLoop (con componentes de Editor bajo el EditorLoop).

Vista ampliada de un perfilador
El Perfilador mostrará sus scripts personalizados, configuraciones y gráficos en el contexto de la ejecución de todo el motor.
Una vista del PlayerLoop

Conozca el PlayerLoop y el ciclo de vida de un script.

Puede optimizar sus scripts con los siguientes consejos y trucos.

Entender el PlayerLoop de Unity

Asegúrese de entender el orden de ejecución del bucle de fotogramas de Unity. Cada script de Unity ejecuta varias funciones de evento en un orden predeterminado. Debería entender la diferencia entre Awake, Start, Update y otras funciones que crean el ciclo de vida de un script.

Consulte el Diagrama de Flujo del Ciclo de Vida del Script para el orden específico de ejecución de las funciones de evento.

Minimizar el código que se ejecuta cada fotograma

Considera si el código debe ejecutarse cada fotograma. Mueve la lógica innecesaria fuera de Update, LateUpdate y FixedUpdate. Estas funciones de evento son lugares convenientes para poner código que debe actualizarse cada fotograma, mientras extraes cualquier lógica que no necesita actualizarse con esa frecuencia. Siempre que sea posible, ejecuta la lógica solo cuando las cosas cambien.

Si necesitas usar Update, considera ejecutar el código cada n fotogramas. Esta es una forma de aplicar el corte de tiempo, una técnica común para distribuir una carga de trabajo pesada a través de múltiples fotogramas. En este ejemplo, ejecutamos ExampleExpensiveFunction una vez cada tres fotogramas:

private int interval = 3;

void Update()
{
    if (Time.frameCount % interval == 0)
    {
        ExampleExpensiveFunction();
    }
}

Evitar lógica pesada en Start/Awake

Cuando se carga tu primera escena, estas funciones se llaman para cada objeto:

  • Awake
  • OnEnable
  • Start

Evita lógica costosa en estas funciones hasta que tu aplicación renderice su primer fotograma. De lo contrario, podrías encontrar tiempos de carga más largos de lo necesario.

Consulta el orden de ejecución para funciones de evento para obtener detalles sobre la carga de la primera escena.

Evitar eventos vacíos de Unity

Incluso los MonoBehaviours vacíos requieren recursos, así que deberías eliminar métodos Update o LateUpdate en blanco.

Usa directivas de preprocesador si estás empleando estos métodos para pruebas:

#if UNITY_EDITOR
void Update()
{
}
#endif

Aquí, puedes usar libremente el Update en el Editor para pruebas sin que sobrecargas innecesarias se cuelen en tu compilación.

Eliminar declaraciones de registro de depuración

Las declaraciones de registro (especialmente en Actualizar, ActualizarTarde o ActualizarFijo) pueden afectar el rendimiento. Desactiva tus declaraciones de registro antes de hacer una compilación.

Para hacer esto más fácilmente, considera crear un atributo condicional junto con una directiva de preprocesamiento. Por ejemplo, crea una clase personalizada como esta:

public static class Logging
{
    [System.Diagnostics.Conditional("ENABLE_LOG")]
    static public void Log(object message)
    {
        UnityEngine.Debug.Log(message);
    }
}
Una vista de HABILITAR_REGISTRO
Agregar una directiva de preprocesador personalizada te permite particionar tus scripts.

Genera tu mensaje de registro con tu clase personalizada. Si desactivas el HABILITAR_REGISTRO preprocesador en los Configuraciones del Jugador, todas tus declaraciones de registro desaparecen de un solo golpe.

Usa valores hash en lugar de parámetros de cadena

Unity no utiliza nombres de cadena para dirigirse a las propiedades de Animator, Material y Shader internamente. Por velocidad, todos los nombres de propiedades se convierten en IDs de propiedad, y estos IDs se utilizan realmente para dirigirse a las propiedades.

Al usar un método Set o Get en un Animator, Material o Shader, utiliza el método de valor entero en lugar de los métodos de valor de cadena. Los métodos de cadena simplemente realizan un hash de cadena y luego envían el ID hash a los métodos de valor entero.

Usa Animator.StringToHash para nombres de propiedades de Animator y Shader.PropertyToID para nombres de propiedades de Material y Shader.

Elige la estructura de datos correcta

Tu elección de estructura de datos impacta la eficiencia mientras iteras miles de veces por cuadro. ¿No estás seguro si usar una Lista, Arreglo o Diccionario para tu colección? Sigue la guía de MSDN sobre estructuras de datos en C# como una guía general para elegir la estructura correcta.

Evita agregar componentes en tiempo de ejecución

Invocar AddComponent en tiempo de ejecución conlleva algún costo. Unity debe verificar si hay componentes duplicados u otros componentes requeridos cada vez que se agregan componentes en tiempo de ejecución.

Instanciar un Prefab con los componentes deseados ya configurados es generalmente más eficiente.

Cachear GameObjects y componentes

GameObject.Find, GameObject.GetComponent y Camera.main (en versiones anteriores a 2020.2) pueden ser costosos, por lo que es mejor evitar llamarlos en Update métodos. En su lugar, llámalos en Start y almacena los resultados.

Aquí hay un ejemplo que demuestra el uso ineficiente de una llamada repetida a GetComponent:

void Update()
{
    Renderer myRenderer = GetComponent<Renderer>();
    ExampleFunction(myRenderer);
}

En su lugar, invoca GetComponent solo una vez, ya que el resultado de la función se almacena en caché. El resultado en caché se puede reutilizar en Update sin más llamadas a GetComponent.

private Renderer myRenderer;

void Start()
{
    myRenderer = GetComponent<Renderer>();
}

void Update()
{
    ExampleFunction(myRenderer);
}

Usar grupos de objetos

Instanciar y Destruir pueden generar basura y picos de recolección de basura (GC), y generalmente es un proceso lento. En lugar de instanciar y destruir GameObjects regularmente (por ejemplo, disparar balas de una pistola), usa grupos de objetos preasignados que se pueden reutilizar y reciclar.

Una vista ampliada del ObjectPool
En este ejemplo, el ObjectPool crea 20 instancias de PlayerLaser para reutilización.

Crea las instancias reutilizables en un punto del juego (por ejemplo, durante una pantalla de menú) cuando un pico de CPU es menos notable. Rastrea este "grupo" de objetos con una colección. Durante el juego, simplemente habilita la siguiente instancia disponible cuando sea necesario, desactiva objetos en lugar de destruirlos y devuélvelos al grupo.

Una vista ampliada de la jerarquía de SampleScene
El grupo de objetos PlayerLaser está inactivo y listo para disparar.

Esto reduce el número de asignaciones gestionadas en tu proyecto y puede prevenir problemas de recolección de basura.

Aprende a crear un sistema simple de agrupamiento de objetos en Unity aquí.

Usar ScriptableObjects

Almacena valores o configuraciones invariables en un ScriptableObject en lugar de un MonoBehaviour. El ScriptableObject es un activo que vive dentro del proyecto y que solo necesitas configurar una vez. No se puede adjuntar directamente a un GameObject.

Crea campos en el ScriptableObject para almacenar tus valores o configuraciones, luego referencia el ScriptableObject en tus MonoBehaviours.

Diagrama de flujo que muestra un ScriptableObject llamado Inventario que contiene configuraciones para varios GameObjects
ScriptableObject llamado Inventario contiene configuraciones para varios GameObjects

Usar esos campos del ScriptableObject puede prevenir la duplicación innecesaria de datos cada vez que instancias un objeto con ese MonoBehaviour.

Mira este Introducción a ScriptableObjects tutorial para ver cómo los ScriptableObjects pueden ayudar a tu proyecto. También puedes encontrar documentación relevante aquí.

Descarga la lista completa de consejos para el rendimiento móvil

En la próxima publicación del blog, echaremos un vistazo más de cerca a la optimización de gráficos y GPU. Sin embargo, si deseas acceder a la lista completa de consejos y trucos del equipo ahora, nuestro e-book completo está disponible aquí.

Portada del e-book, "Optimiza el rendimiento de tu juego móvil"

Descarga nuestro e-book

Si estás interesado en aprender más sobre los servicios de Soporte Integrado y deseas dar a tu equipo acceso directo a ingenieros, asesoramiento experto y orientación sobre mejores prácticas para tus proyectos, entonces consulta los planes de éxito de Unity aquí.

Mantente atento para más consejos de rendimiento

Queremos ayudarte a hacer que tus aplicaciones de Unity sean lo más eficientes posible, así que si hay algún tema de optimización sobre el que te gustaría saber más, por favor háznoslo saber en los comentarios.