¡Vea cómo optimizar el rendimiento de la interfaz de usuario en Unity usando esta guía detallada con numerosos experimentos, consejos prácticos y pruebas de rendimiento para respaldarlo!
¡Hola! Soy Sergey Begichev, desarrollador de clientes en Pixonic (MY.GAMES). En esta publicación, hablaré sobre la optimización de la interfaz de usuario en Unity3D. Si bien renderizar un conjunto de texturas puede parecer simple, puede generar problemas de rendimiento importantes. Por ejemplo, en nuestro proyecto War Robots, las versiones de la interfaz de usuario no optimizadas representaron hasta el 30 % de la carga total de la CPU, ¡una cifra asombrosa!
Por lo general, este problema surge en dos circunstancias: una, cuando hay numerosos objetos dinámicos, y la otra, cuando los diseñadores crean diseños que priorizan el escalamiento confiable en diferentes resoluciones. Incluso una interfaz de usuario pequeña puede generar una carga notable en estas circunstancias. Exploremos cómo funciona esto, identifiquemos las causas de la carga y analicemos posibles soluciones.
Primero, repasemos
Si bien los puntos 2 y 3 son intuitivamente claros, el resto de las recomendaciones pueden resultar problemáticas de imaginar en la práctica. Por ejemplo, el consejo de "dividir los lienzos en sublienzos" es ciertamente valioso, pero Unity no proporciona pautas claras sobre los principios detrás de esta división. Hablando por mí, en términos prácticos, quiero saber dónde tiene más sentido implementar sublienzos.
Tenga en cuenta el consejo de “evitar los grupos de diseño”. Si bien pueden contribuir a una gran carga de trabajo en la interfaz de usuario, muchas interfaces de usuario grandes vienen con varios grupos de diseño y rehacer todo puede llevar mucho tiempo. Además, los diseñadores de diseño que evitan los grupos de diseño pueden encontrarse dedicando mucho más tiempo a sus tareas. Por lo tanto, sería útil comprender cuándo se deben evitar dichos grupos, cuándo pueden ser beneficiosos y qué acciones tomar si no podemos eliminarlos.
Esta ambigüedad en las recomendaciones de Unity es un problema central: a menudo no está claro qué principios debemos aplicar a estas sugerencias.
Para optimizar el rendimiento de la interfaz de usuario, es fundamental comprender cómo Unity construye la interfaz de usuario. Comprender estas etapas es fundamental para optimizar la interfaz de usuario de manera eficaz en Unity. Podemos identificar, en líneas generales, tres etapas clave en este proceso:
Diseño . Inicialmente, Unity organiza todos los elementos de la interfaz de usuario en función de sus tamaños y posiciones designadas. Estas posiciones se calculan en relación con los bordes de la pantalla y otros elementos, formando una cadena de dependencias.
Procesamiento por lotes . A continuación, Unity agrupa elementos individuales en lotes para lograr una representación más eficiente. Dibujar un elemento grande siempre es más eficiente que renderizar varios más pequeños. (Para obtener más información sobre el procesamiento por lotes, consulte
Renderizado . Por último, Unity dibuja los lotes recopilados. Cuantos menos lotes haya, más rápido será el proceso de renderizado.
Si bien hay otros elementos involucrados en el proceso, estas tres etapas representan la mayoría de los problemas, así que por ahora, centrémonos en ellas.
Lo ideal es que, cuando nuestra interfaz de usuario permanece estática (es decir, nada se mueve ni cambia), podamos crear el diseño una vez, crear un único lote grande y renderizarlo de manera eficiente.
Sin embargo, si modificamos la posición de un solo elemento, debemos recalcular su posición y reconstruir el lote afectado. Si otros elementos dependen de esta posición, entonces necesitaremos recalcular también sus posiciones, lo que provocará un efecto en cascada en toda la jerarquía. Y cuantos más elementos necesiten ajuste, mayor será la carga de procesamiento por lotes.
Por lo tanto, los cambios en el diseño pueden generar un efecto dominó en toda la interfaz de usuario, y nuestro objetivo es minimizar la cantidad de cambios. (Alternativamente, podemos intentar aislar los cambios para evitar una reacción en cadena).
Como ejemplo práctico, este problema es especialmente evidente cuando se utilizan grupos de diseño. Cada vez que se reconstruye un diseño, cada elemento de diseño realiza una operación GetComponent, que puede consumir muchos recursos.
Examinemos una serie de ejemplos para comparar los resultados de rendimiento. (Todas las pruebas se realizaron con la versión 2022.3.24f1 de Unity en un dispositivo Google Pixel 1).
En esta prueba, crearemos un grupo de diseño que incluye un solo elemento y analizaremos dos escenarios: uno en el que cambiamos el tamaño del elemento y otro en el que utilizamos la propiedad FillAmount.
Cambios en RectTransform:
Cambios en FlllAmount:
En el segundo ejemplo, intentaremos hacer lo mismo, pero en un grupo de diseño con 8 elementos. En este caso, solo cambiaremos un elemento.
Cambios en RectTransform:
Cambios en FlllAmount:
Si, en el ejemplo anterior, los cambios en RectTransform generaron una carga de 0,2 ms en el diseño, esta vez la carga aumenta a 0,7 ms. De manera similar, la carga de las actualizaciones por lotes aumenta de 0,65 ms a 1,10 ms.
Aunque todavía estamos modificando solo un elemento, el aumento del tamaño del diseño afecta significativamente la carga durante la reconstrucción.
Por el contrario, cuando ajustamos FillAmount de un elemento, no observamos ningún aumento en la carga, incluso con una mayor cantidad de elementos. Esto se debe a que la modificación de FillAmount no desencadena una reconstrucción del diseño, lo que da como resultado solo un ligero aumento en la carga de actualización por lotes.
Claramente, usar FillAmount es la opción más eficiente en este escenario. Sin embargo, la situación se vuelve más compleja cuando modificamos la escala o la posición de un elemento. En estos casos, es difícil reemplazar los mecanismos integrados de Unity que no activan la reconstrucción del diseño.
Aquí es donde entran en juego los subcanvases. Examinemos los resultados cuando encapsulamos un elemento modificable dentro de un subcanvas.
Crearemos un grupo de diseño con 8 elementos, uno de los cuales estará alojado dentro de un SubCanvas, y luego modificaremos su transformación.
Cambios de RectTransform en SubCanvas:
Como indican los resultados, encapsular un solo elemento dentro de un SubCanvas casi elimina la carga en el diseño; esto se debe a que SubCanvas aísla todos los cambios, evitando una reconstrucción en los niveles superiores de la jerarquía.
Sin embargo, es importante tener en cuenta que los cambios dentro del lienzo no influirán en la posición de los elementos fuera de él. Por lo tanto, si ampliamos demasiado los elementos, existe el riesgo de que se superpongan con elementos vecinos.
Procedamos a envolver 8 elementos de diseño en un SubCanvas:
El ejemplo anterior demuestra que, si bien la carga en el diseño sigue siendo baja, la actualización por lotes se ha duplicado. Esto significa que, si bien dividir los elementos en varios sublienzos ayuda a reducir la carga en la creación del diseño, aumenta la carga en el ensamblaje por lotes. En consecuencia, esto podría llevarnos a un efecto negativo neto en general.
Ahora, realicemos otro experimento. Primero, crearemos un grupo de diseño con 8 elementos y luego modificaremos uno de los elementos de diseño con el animador.
El animador ajustará RectTransform a un nuevo valor:
Aquí vemos el mismo resultado que en el segundo ejemplo, donde cambiamos todo manualmente. Esto es lógico porque no importa lo que usemos para cambiar RectTransform.
El animador cambia RectTransform a un valor similar:
Anteriormente, los animadores se enfrentaban a un problema en el que sobrescribían continuamente el mismo valor en cada fotograma, incluso si ese valor permanecía sin cambios. Esto desencadenaba inadvertidamente una reconstrucción del diseño. Afortunadamente, las versiones más nuevas de Unity han resuelto este problema, eliminando la necesidad de cambiar a alternativas.
Ahora, examinemos cómo se comporta el cambio del valor del texto dentro de un grupo de diseño con 8 elementos y si desencadena una reconstrucción del diseño:
Vemos que la reconstrucción también se activa.
Ahora, cambiaremos el valor de TextMechPro en el grupo de diseño de 8 elementos:
TextMechPro también activa una reconstrucción del diseño, e incluso parece que pone más carga en el procesamiento por lotes y la representación que el texto normal.
Cambiar el valor de TextMechPro en SubCanvas en un grupo de diseño de 8 elementos:
SubCanvas ha aislado eficazmente los cambios, lo que evita la reconstrucción del diseño. Sin embargo, aunque la carga de las actualizaciones por lotes ha disminuido, sigue siendo relativamente alta. Esto se convierte en un problema cuando se trabaja con texto, ya que cada letra se trata como una textura independiente. La modificación del texto, en consecuencia, afecta a varias texturas.
Ahora, evaluemos la carga incurrida al activar y desactivar un GameObject (GO) dentro del grupo de diseño.
Activar y desactivar un GameObject dentro de un grupo de diseño de 8 elementos:
Como podemos ver, activar o desactivar un GO también desencadena una reconstrucción del diseño.
Activar un GO dentro de un SubCanvas con un grupo de diseño de 8 elementos:
En este caso, SubCanvas también ayuda a aliviar la carga.
Ahora, verifiquemos cuál es la carga si activamos o desactivamos todo el GO con un grupo de diseño:
Como muestran los resultados, la carga alcanzó su nivel más alto hasta el momento. Al habilitar el elemento raíz, se desencadena una reconstrucción del diseño de los elementos secundarios, lo que, a su vez, genera una carga significativa tanto en el procesamiento por lotes como en la renderización.
Entonces, ¿qué podemos hacer si necesitamos habilitar o deshabilitar elementos completos de la interfaz de usuario sin crear una carga excesiva? En lugar de habilitar y deshabilitar el GO en sí, puedes simplemente deshabilitar el componente Canvas o Canvas Group. Además, configurar el canal alfa del Canvas Group en 0 puede lograr el mismo efecto y evitar problemas de rendimiento.
Esto es lo que sucede con la carga cuando deshabilitamos el componente Canvas Group. Dado que GO permanece habilitado mientras que Canvas está deshabilitado, el diseño se conserva pero simplemente no se muestra. Este enfoque no solo genera una carga de diseño baja, sino que también reduce significativamente la carga en el procesamiento por lotes y la renderización.
A continuación, examinemos el impacto de cambiar SiblingIndex dentro del grupo de diseño.
Cambiar SiblingIndex dentro de un grupo de diseño de 8 elementos:
Como se observa, la carga sigue siendo significativa, con 0,7 ms para actualizar el diseño. Esto indica claramente que las modificaciones en SiblingIndex también desencadenan una reconstrucción del diseño.
Ahora, experimentemos con un enfoque diferente. En lugar de cambiar el índice de hermanos, intercambiaremos las texturas de dos elementos dentro del grupo de diseño.
Intercambio de texturas de dos elementos en un grupo de diseño de 8 elementos:
Como podemos ver, la situación no ha mejorado, sino que ha empeorado. El reemplazo de la textura también desencadena una reconstrucción.
Ahora, vamos a crear un grupo de diseño personalizado. Construiremos 8 elementos y simplemente intercambiaremos las posiciones de dos de ellos.
Grupo de diseño personalizado con 8 elementos:
De hecho, la carga ha disminuido significativamente, y esto es lo esperado. En este ejemplo, el script simplemente intercambia las posiciones de dos elementos, lo que elimina las pesadas operaciones GetComponent y la necesidad de recalcular las posiciones de todos los elementos. Como resultado, se requiere menos actualización para el procesamiento por lotes. Si bien este enfoque parece una solución milagrosa, es importante tener en cuenta que realizar cálculos en scripts también contribuye a la carga general.
A medida que introducimos más complejidad en nuestro grupo de diseño, la carga aumentará inevitablemente, pero no necesariamente se reflejará en la sección Diseño, ya que los cálculos se realizan en scripts. Por lo tanto, es fundamental que controlemos la eficiencia del código nosotros mismos. Sin embargo, para grupos de diseño simples, las soluciones personalizadas pueden ser una excelente opción.
Reconstruir el diseño presenta un desafío importante. Para abordar este problema, debemos identificar sus causas fundamentales, que pueden variar. Estos son los principales factores que conducen a reconstrucciones de diseño:
Es importante destacar algunos aspectos que ya no plantean problemas en las versiones más nuevas de Unity pero que sí lo hacían en las anteriores: sobrescribir el mismo texto y configurar repetidamente el mismo valor con un animador.
Ahora que hemos identificado los factores que desencadenan una reconstrucción del diseño, resumamos nuestras opciones de solución:
Envuelva un GameObject (GO) que activa una reconstrucción en un SubCanvas. Este enfoque aísla los cambios, lo que evita que afecten a otros elementos en la jerarquía. Sin embargo, tenga cuidado: demasiados SubCanvas pueden aumentar significativamente la carga en el procesamiento por lotes.
Activar y desactivar el sublienzo o el grupo de lienzos en lugar del GO. Utilizar un grupo de objetos en lugar de crear nuevos GO. Este método conserva el diseño en la memoria, lo que permite una activación rápida de los elementos sin necesidad de una reconstrucción.
Utiliza animaciones de sombreado. Cambiar la textura con un sombreador no activará una reconstrucción del diseño. Sin embargo, ten en cuenta que las texturas pueden superponerse con otros elementos. Este método cumple una función similar a la del uso de sublienzos, pero requiere escribir un sombreador.
Reemplace el grupo de diseño de Unity con un grupo de diseño personalizado. Uno de los problemas clave con los grupos de diseño de Unity es que cada LayoutElement llama a GetComponent durante la reconstrucción, lo que consume muchos recursos. La creación de un grupo de diseño personalizado puede solucionar este problema, pero tiene sus propios desafíos. Los componentes personalizados pueden tener requisitos operativos específicos que debe comprender para un uso eficaz. No obstante, este enfoque puede ser más eficiente, especialmente para escenarios de grupos de diseño más simples.