¿Qué estás buscando?
Games

Optimice el rendimiento de sus juegos para móviles: Consejos sobre perfiles, memoria y arquitectura de código de los mejores ingenieros de Unity.

THOMAS KROGH-JACOBSEN / UNITY TECHNOLOGIESProduct Marketing Core Tech
Jun 23, 2021|15 minutos
Optimice el rendimiento de sus juegos para móviles: Consejos sobre perfiles, 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 Accelerate Solutions conoce a la perfección el código fuente y ofrece asistencia a un gran número de clientes de Unity para que puedan sacar el máximo partido al motor. En su trabajo, profundizan en los proyectos de los creadores para ayudar a identificar los puntos en los que se podría optimizar el rendimiento para lograr mayor velocidad, estabilidad y eficacia. Nos sentamos con este equipo, formado por los ingenieros de software más veteranos de Unity, y les pedimos que compartieran algunos de sus conocimientos sobre optimización de juegos para móviles.

Cuando nuestros ingenieros empezaron a compartir sus conocimientos sobre la optimización de juegos para móviles, enseguida nos dimos cuenta de que había demasiada información para la entrada que habíamos planeado. En su lugar, hemos decidido convertir su montaña de conocimientos en un libro electrónico completo (que puede descargar aquí), así como en una serie de entradas de blog que destacan algunos de estos más de 75 consejos prácticos.

Empezamos el primer post de esta serie haciendo un zoom sobre cómo puedes mejorar el rendimiento de tu juego con la creación de perfiles, la memoria y la arquitectura del código. En las próximas semanas, publicaremos dos artículos más: el primero tratará sobre la física de la interfaz de usuario, seguido de otro sobre audio y recursos, configuración del proyecto y gráficos.

¿Quiere ver la serie completa ahora? Descargue gratis el libro electrónico completo.

Vamos allá.

Perfil

¿Qué mejor lugar para empezar que la elaboración de perfiles y el proceso de recopilación de datos sobre el rendimiento de los móviles y la actuación en consecuencia? Aquí es donde empieza realmente la optimización del rendimiento móvil.

Perfilar pronto, a menudo y en el dispositivo de destino

Unity Profiler proporciona información esencial sobre el rendimiento de su aplicación, pero no puede ayudarle si no lo utiliza. Perfile su proyecto en las primeras fases de desarrollo, no sólo cuando esté a punto de comercializarlo. Investiga los fallos o picos en cuanto aparezcan. A medida que desarrolle una "firma de rendimiento" para su proyecto, podrá detectar nuevos problemas con mayor facilidad.

Mientras que la creación de perfiles en el editor puede darte una idea del rendimiento relativo de los distintos sistemas de tu juego, la creación de perfiles en cada dispositivo te ofrece la oportunidad de obtener información más precisa. Perfile una versión de desarrollo en los dispositivos de destino siempre que sea posible. No olvide perfilar y optimizar tanto para los dispositivos de mayor como de menor especificación que vaya a utilizar.

Junto con Unity Profiler, puedes aprovechar las herramientas nativas de iOS y Android para realizar más pruebas de rendimiento en sus respectivos motores:

Determinado hardware puede beneficiarse de herramientas de perfilado adicionales (por ejemplo, ARM Mobile Studio, Intel VTune y Snapdragon Profiler). Vea Perfilando Aplicaciones Hechas con Unity para más información.

Centrarse en optimizar las áreas adecuadas

No hagas conjeturas ni suposiciones sobre lo que está ralentizando el rendimiento de tu juego. Utilice el Unity Profiler y las herramientas específicas de la plataforma para localizar el origen preciso de un retraso.

Por supuesto, no todas las optimizaciones descritas aquí se aplicarán a su aplicación. Algo que funciona bien en un proyecto puede no trasladarse al suyo. Identifique los verdaderos cuellos de botella y concentre sus esfuerzos en lo que beneficia a su trabajo.

Entender cómo funciona el perfilador de Unity

El Unity Profiler puede ayudarle a detectar las causas de cualquier retraso o congelación en tiempo de ejecución y entender mejor lo que está sucediendo en un fotograma específico, o punto en el tiempo. Habilitar las pistas CPU y Memoria por defecto. Puedes monitorizar módulos adicionales de Profiler como Renderer, Audio y Física, según lo necesites para tu juego (por ejemplo, juegos con mucha física o basados en música).

Utilice el Unity Profiler para probar el rendimiento y la asignación de recursos de su aplicación.
Utilice el Unity Profiler para probar el rendimiento y la asignación de recursos de su aplicación.

Cree la aplicación para su dispositivo marcando Development Build y Autoconnect Profiler, o conéctese manualmente para acelerar el tiempo de inicio de la aplicación.

Crear ajustes en el editor

Elija el objetivo de la plataforma que desea perfilar. El botón Grabar registra varios segundos de la reproducción de tu aplicación (300 fotogramas por defecto). Vaya a Unity > Preferences > Analysis > Profiler > Frame Count para aumentar esto hasta 2000 si necesita capturas más largas. Aunque 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 su escenario específico.

Este es un perfilador basado en la 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 específicas a la API). Además, cuando se utiliza el Deep Profilingsetting, Unity puede perfilar el principio y el final de cada llamada de función en su código de script para decirle exactamente qué parte de su aplicación está causando una ralentización.

Vista Timeline en el editor
Utiliza la vista Timeline para determinar si estás limitado por la CPU o por la GPU.

Al perfilar tu juego, te recomendamos que cubras tanto los picos como el coste de una trama media en tu juego. Comprender y optimizar las operaciones costosas que se producen en cada fotograma puede ser más útil para las aplicaciones que se ejecutan por debajo de la tasa de fotogramas objetivo. Cuando busque picos, explore primero las operaciones caras (por ejemplo, física, IA, animación) y la recogida de basura.

Haga clic en la ventana para analizar un fotograma concreto. A continuación, utilice la vista Timeline o Jerarquía para lo siguiente:

  • Timeline muestra el desglose visual de la temporización de un fotograma específico. Esto le permite visualizar cómo se relacionan las actividades entre sí y a través de diferentes hilos. Utilice esta opción para determinar si está conectado a la CPU o a la GPU.
  • Jerarquía muestra la jerarquía de ProfileMarkers, agrupados. Permite ordenar las muestras en función del coste temporal en milisegundos(Tiempo ms y Auto ms). También puede contar el número de llamadas a una función y la memoria heap gestionada(GC Alloc) en el marco.
Clasificación de ProfileMarkers por coste de tiempo
La vista de jerarquía permite ordenar los ProfileMarkers por coste de tiempo.

Lea una descripción completa de Unity Profiler aquí. Aquellos que son nuevos en la creación de perfiles también pueden ver esta Introducción a la creación de perfiles de Unity.

Antes de optimizar nada en su proyecto, guarde el archivo .datafile de Profiler. Aplique sus cambios y compare los .datos guardados antes y después de la modificación. Confíe en este ciclo para mejorar el rendimiento: perfilar, optimizar y comparar. Luego, enjuague y repita.

Utilizar el Profile Analyzer

Esta herramienta permite agregar varios fotogramas de datos de Profiler y, a continuación, localizar los fotogramas de interés. ¿Quieres ver qué le ocurre a Profiler después de hacer un cambio en tu proyecto? La vista Comparar le permite cargar y diferenciar dos conjuntos de datos, para que pueda probar los cambios y mejorar su resultado. El Profile Analyzer está disponible a través del Package Manager de Unity.

Profundización en el editor interno Profile Analyzer
Profundice aún más en los datos de fotogramas y marcadores con Profile Analyzer, que complementa al Profiler existente.

Trabajar con un presupuesto de tiempo específico por fotograma

Cada fotograma tendrá un presupuesto de tiempo basado en tu objetivo de fotogramas por segundo (FPS). Lo ideal es que una aplicación que funcione a 30 FPS permita aproximadamente 33,33 ms por fotograma (1000 ms / 30 FPS). Del mismo modo, un objetivo de 60 FPS deja 16,66 ms por fotograma.

Los dispositivos pueden superar este presupuesto durante breves periodos de tiempo (por ejemplo, para escenas o secuencias de carga), pero no durante un tiempo prolongado.

Tener en cuenta la temperatura del aparato

Para móviles, sin embargo, no recomendamos utilizar este tiempo máximo de forma constante, ya que el dispositivo puede sobrecalentarse y el sistema operativo puede acelerar térmicamente la CPU y la GPU. Te recomendamos que utilices sólo un 65% del tiempo disponible para permitir el enfriamiento entre fotogramas. Un presupuesto de fotogramas típico 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 los de sobremesa. Los niveles de calor físico pueden afectar directamente al rendimiento.

Si el dispositivo se está calentando, Profiler podría percibir y notificar un rendimiento deficiente, aunque no sea motivo de preocupación a largo plazo. Para combatir el sobrecalentamiento del perfilado, perfile en ráfagas cortas. Esto enfría el aparato y simula las condiciones del mundo real. Nuestra recomendación general es mantener el dispositivo frío durante 10-15 minutos antes de volver a realizar el perfil.

Determina si estás ligado a la GPU o a la CPU

El Perfilador puede decirte si tu CPU está tardando más de lo que tienes asignado o si la culpable es tu GPU. Lo hace emitiendo marcadores prefijados con Gfx de la siguiente manera:

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

Unity emplea la gestión automática de memoria para su código y scripts generados por el usuario. Las piezas pequeñas de datos, como las variables locales de tipo valor, se asignan a la pila. Los datos de mayor tamaño y el almacenamiento a largo plazo se asignan al montón gestionado.

El recolector de basura identifica y reasigna periódicamente la memoria de montón no utilizada. Aunque esto se ejecuta automáticamente, el proceso de examinar todos los objetos de la pila puede hacer que el juego se entrecorte o funcione con lentitud.

Optimizar el uso de la memoria significa ser consciente de cuándo se asigna y se desasigna memoria heap, y cómo se minimiza el efecto de la recolección de basura. Consulte Comprender el montón gestionado para obtener más información.

Un vistazo al editor interno de Memory Profiler
Captura, inspecciona y compara instantáneas en el Memory Profiler.

Utilizar el Memory Profiler

Este complemento independiente (disponible como paquete Experimental o Vista previa en el Gestor de paquetes) puede tomar una instantánea de la memoria de montón gestionada, para ayudarle a identificar problemas como la fragmentación y las fugas de memoria.

Haga clic en la vista de mapa de árbol para rastrear una variable hasta el objeto nativo que se aferra a la memoria. Aquí puede identificar problemas comunes de consumo de memoria, como texturas excesivamente grandes o activos duplicados.

Aprenda a aprovechar el Memory Profiler en Unity para mejorar el uso de la memoria. También puede consultar la documentación oficial de Memory Profiler.

Reducir el impacto de la recogida de basuras (GC)

Unity utiliza el recolector de basura Boehm-Demers-Weiser, que detiene la ejecución del código de su programa y sólo reanuda la ejecución normal una vez que ha completado su trabajo.

Tenga cuidado con ciertas asignaciones de heap innecesarias, que podrían causar picos de GC:

  • Cuerdas: En C#, las cadenas son tipos de referencia, no tipos de valor. Reducir la creación o manipulación innecesaria de cadenas. Evite analizar archivos de datos basados en cadenas, como JSON y XML; en su lugar, almacene los datos en ScriptableObjects o formatos como MessagePack o Protobuf. Utilice la clase StringBuilder si necesita construir cadenas en tiempo de ejecución.
  • Llamadas a funciones de Unity: Algunas funciones crean asignaciones de heap. Almacene en caché las referencias a las matrices en lugar de asignarlas en medio de un bucle. Además, aprovecha ciertas funciones que evitan la generación de basura. Por ejemplo, utilice GameObject.CompareTag en lugar de comparar manualmente una cadena con GameObject.tag (ya que devolver una nueva cadena crea basura).
  • Boxeo: Evite pasar una variable de tipo valor en lugar de una variable de tipo referencia. Esto crea un objeto temporal, y la basura potencial que viene con él implícitamente convierte el tipo de valor a un objeto de tipo (por ejemplo, int i = 123; objeto o = i). En su lugar, trate de proporcionar anulaciones concretas con el tipo de valor que desea pasar. También se pueden utilizar genéricos para estas anulaciones.
  • Coroutines: Aunque yield no produce basura, la creación de un nuevo objeto WaitForSeconds sí lo hace. Almacena en caché y reutiliza el objeto WaitForSeconds en lugar de crearlo en la línea yield.
  • LINQ y expresiones regulares: Ambos generan basura del boxeo entre bastidores. Evite LINQ y las expresiones regulares si el rendimiento es un problema. Escribir bucles for y utilizar listas como alternativa a la creación de nuevas matrices.

Si es posible, programar la recogida de basuras

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

Consulte Comprender la gestión automática de memoria para ver ejemplos de cómo utilizarla en su beneficio.

Utiliza el recolector de basura incremental para dividir la carga de trabajo de GC

En lugar de crear una única y larga interrupción durante la ejecución de su programa, la recogida de basura incremental utiliza múltiples interrupciones mucho más cortas que distribuyen la carga de trabajo en muchas tramas. Si la recolección de basura está afectando al rendimiento, pruebe a activar esta opción para ver si puede reducir el problema de los picos de GC. Utilice el Profile Analyzer para comprobar las ventajas que aporta a su aplicación.

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

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

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

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

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

Puedes optimizar tus scripts con los siguientes consejos y trucos.

Comprender 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 eventos en un orden predeterminado. Debes 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 de los scripts para conocer el orden específico de ejecución de las funciones de eventos.

Minimizar el código que se ejecuta en cada fotograma

Considere si el código debe ejecutarse en cada fotograma. Eliminar la lógica innecesaria de Update, LateUpdate y FixedUpdate. Estas funciones de evento son lugares convenientes para poner código que debe actualizarse cada frame, mientras se extrae cualquier lógica que no necesite actualizarse con esa frecuencia. Siempre que sea posible, ejecute la lógica sólo cuando las cosas cambien.

Si necesita utilizar Actualizar, considere la posibilidad de ejecutar el código cada n fotogramas. Esta es una forma de aplicar el time slicing, una técnica habitual para distribuir una carga de trabajo pesada entre varios fotogramas. En este ejemplo, ejecutamos la ExampleExpensiveFunction una vez cada tres fotogramas:

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.

Evitar la lógica pesada en Inicio/Despertar

Cuando se carga la primera escena, se llama a estas funciones para cada objeto:

  • Awake
  • OnEnable
  • Inicio

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

Consulte el orden de ejecución de las funciones de eventos para más detalles sobre la primera carga de escenas.

Evitar los eventos vacíos de Unity

Incluso los MonoBehaviour vacíos requieren recursos, por lo que deberías eliminar los métodos Update o LateUpdate vacíos.

Utilice directivas de preprocesador si emplea estos métodos para las pruebas:

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.

Aquí, usted puede utilizar libremente la actualización en el editor para las pruebas sin sobrecarga innecesaria deslizarse en su construcción.

Eliminar declaraciones del registro de depuración

Las sentencias de registro (especialmente en Update, LateUpdate o FixedUpdate) pueden reducir el rendimiento. Desactive sus declaraciones Log antes de realizar una compilación.

Para hacerlo más fácilmente, considere la posibilidad de crear un atributo Condicional junto con una directiva de preprocesamiento. Por ejemplo, cree una clase personalizada como ésta:

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.

Una vista de ENABLE_LOG
Añadir una directiva de preprocesador personalizada le permite particionar sus scripts.

Genere su mensaje de registro con su clase personalizada. Si desactivas el preprocesador ENABLE_LOG en la Configuración del reproductor, todas tus sentencias Log desaparecerán de un plumazo.

Utilizar valores hash en lugar de parámetros de cadena

Unity no utiliza nombres de cadena para direccionar internamente propiedades de Animator, Material, y Shader. Para mayor rapidez, todos los nombres de propiedades se convierten en identificadores de propiedades, y estos identificadores se utilizan para direccionar las propiedades.

Cuando utilices un método Set o Get en un Animator, Material o Shader, utiliza el método integer-valued en lugar de los métodos string-valued. Los métodos de cadena simplemente realizan el hash de la cadena y luego envían el ID hash a los métodos de valores enteros.

Utilice Animator.StringToHash para los nombres de las propiedades de Animator y Shader.PropertyToID para los nombres de las propiedades de Material y Shader.

Elegir la estructura de datos adecuada

La elección de la estructura de datos influye en la eficiencia, ya que se itera miles de veces por fotograma. ¿No está seguro de si utilizar una lista, una matriz o un diccionario para su colección? Siga la guía MSDN de estructuras de datos en C# como guía general para elegir la estructura correcta.

Evite añadir componentes en tiempo de ejecución

Invocar AddComponent en tiempo de ejecución tiene algún coste. Unity debe comprobar si hay componentes duplicados u otros componentes necesarios siempre que añada componentes en tiempo de ejecución.

Instanciar un Prefab con los componentes deseados ya configurados suele ser más eficaz.

Caché de GameObjects y componentes

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

He aquí un ejemplo que demuestra el uso ineficiente de una llamada repetida a GetComponent:

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.

En su lugar, invoque GetComponent sólo una vez, ya que el resultado de la función se almacena en caché. El resultado almacenado en caché puede reutilizarse en Update sin necesidad de realizar más llamadas a GetComponent.

Tipo de bloque desconocido "codeBlock", especifique un serializador para él en la propiedad `serializers.types`.

Utilizar piscinas de objetos

Instanciar y Destruir puede generar picos de basura y de recolección de basura (GC), y generalmente es un proceso lento. En lugar de instanciar y destruir GameObjects con regularidad (por ejemplo, disparando balas de una pistola), utilice pools de objetos preasignados que puedan reutilizarse y reciclarse.

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

Crea las instancias reutilizables en un momento del juego (por ejemplo, durante una pantalla de menú) en el que el pico de CPU sea menos perceptible. Rastrea este "pool" de objetos con una colección. Durante el juego, basta con activar la siguiente instancia disponible cuando sea necesario, desactivar objetos en lugar de destruirlos y devolverlos a la reserva.

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

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

Aprenda a crear un sistema simple de Object Pooling en Unity aquí.

Usar ScriptableObjects

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

Cree campos en el ScriptableObject para almacenar sus valores o configuraciones, luego haga referencia al ScriptableObject en sus MonoBehaviours.

Diagrama de flujo que muestra un ScriptableObject llamado Inventory que contiene ajustes para varios GameObjects
ScriptableObject llamado Inventory contiene ajustes para varios GameObjects

Utilizar esos campos desde el ScriptableObject puede evitar la duplicación innecesaria de datos cada vez que instancias un objeto con ese MonoBehaviour.

Vea este tutorial de Introducción a ScriptableObjects para ver cómo ScriptableObjects puede ayudar a su proyecto. También puede encontrar la documentación pertinente aquí.

Descargar la lista completa de consejos sobre rendimiento móvil

En la próxima entrada del blog, analizaremos más a fondo la optimización de los gráficos y la GPU. No obstante, si desea acceder ahora a toda la lista de consejos y trucos del equipo, nuestro libro electrónico completo está disponible aquí.

Portada del ebook, "Optimice el rendimiento de sus juegos para móviles"

Descargue nuestro libro electrónico

Si está interesado en obtener más información sobre los servicios de asistencia integrada y desea ofrecer a su equipo acceso directo a ingenieros, asesoramiento experto y orientación sobre las mejores prácticas para sus proyectos, consulte los planes de éxito de Unity aquí.

No te pierdas más consejos sobre rendimiento

Queremos ayudarte a que tus aplicaciones Unity rindan al máximo, así que si hay algún tema de optimización sobre el que te gustaría saber más, por favor, mantennos informados en los comentarios.