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á el código de usuario de su proyecto bajo el PlayerLoop - con los componentes del Editor bajo el EditorLoop.
Es importante entender el orden de ejecución del FrameLoop de Unity. Cada script de Unity ejecuta varias funciones de eventos en un orden predeterminado. Aprenda la diferencia entre Awake, Start, Update y otras funciones que crean el ciclo de vida de un script para fortalecer el rendimiento.
Algunos ejemplos incluyen el uso de FixedUpdate en lugar de Update cuando se trata de un Rigidbody o el uso de Awake en lugar de Start para inicializar variables o el estado del juego antes de que el juego comience. Utilízalos para minimizar el código que se ejecuta en cada fotograma. Awake se llama sólo una vez durante la vida de la instancia de script y siempre antes de las funciones Start. Esto significa que debes utilizar Start para tratar con objetos que sabes que pueden hablar con otros objetos, o consultarlos cuando se han inicializado.
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.
Si su proyecto tiene requisitos de rendimiento exigentes (por ejemplo, un juego de mundo abierto), considere la posibilidad de crear un Gestor de Actualizaciones personalizado utilizando Update, LateUpdate o FixedUpdate.
Un patrón de uso común para Update o LateUpdate es ejecutar la lógica sólo cuando se cumple alguna condición. Esto puede dar lugar a una serie de llamadas de retorno por fotograma que no ejecutan ningún código excepto la comprobación de esta condición.
Cada vez que Unity llama a un método de mensaje como Update o LateUpdate, hace una llamada interop - es decir, una llamada desde el lado C/C++ al lado C# administrado. Para un pequeño número de objetos, esto no es un problema. Cuando tienes miles de objetos, esta sobrecarga empieza a ser significativa.
Suscriba objetos activos a este Gestor de Actualizaciones cuando necesiten llamadas de retorno, y cancele la suscripción cuando no las necesiten. Este patrón puede reducir muchas de las llamadas interoperativas a sus objetos Monobehaviour.
Consulte las técnicas de optimización específicas de los motores de juego para ver ejemplos de aplicación.
Considere si el código debe ejecutarse en cada fotograma. Puede mover la lógica innecesaria fuera de Update, LateUpdate y FixedUpdate. Estas funciones de eventos de Unity son lugares convenientes para poner código que debe actualizarse cada frame, pero usted puede extraer cualquier lógica que no necesite actualizarse con esa frecuencia.
Sólo ejecuta la lógica cuando las cosas cambian. Recuerde aprovechar técnicas como el patrón observador en forma de eventos para activar una firma de función específica.
Si necesita utilizar Actualizar, podría 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.
El truco está en intercalar esto con otro trabajo que se ejecuta en los otros marcos. En este ejemplo, podría "programar" otras funciones caras cuando Time.frameCount % interval == 1 o Time.frameCount % interval == 2.
Alternativamente, utilice una clase personalizada de Gestor de Actualizaciones para actualizar los objetos suscritos cada n tramas.
En versiones de Unity anteriores a 2020.2, GameObject.Find, GameObject.GetComponent, y Camera.main pueden ser costosos, así que es mejor evitar llamarlos en métodos Update.
Además, intente evitar colocar métodos caros en OnEnable y OnDisable si se llaman a menudo. Llamar con frecuencia a estos métodos puede contribuir a los picos de CPU.
Siempre que sea posible, ejecute funciones caras, como MonoComportamiento.Awake y MonoComportamiento.Startdurante la fase de inicialización. Almacena en caché las referencias necesarias y reutilízalas más tarde. Revise nuestra sección anterior sobre el PlayerLoop de Unity para la ejecución de la orden del script en más detalle.
He aquí un ejemplo que demuestra el uso ineficiente de una llamada repetida a GetComponent:
void Actualizar()
{
Renderer myRenderer = GetComponent<Renderer>();
ExampleFunction(myRenderer);
}
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.
Más información sobre el orden de ejecución de las funciones de eventos.
Las sentencias de registro (especialmente en Update, LateUpdate o FixedUpdate) pueden reducir el rendimiento, así que desactive sus sentencias de registro antes de realizar una compilación. Para hacer esto rápidamente, considere hacer un atributo condicional junto con una directiva de preprocesamiento.
Por ejemplo, es posible que desee crear una clase personalizada como se muestra a continuación.
Genere su mensaje de registro con su clase personalizada. Si desactivas el preprocesador ENABLE_LOG en la Configuración del jugador > Scripting Definir símbolos, todos tus logstatements desaparecerán de un plumazo.
El manejo de cadenas y texto es una fuente común de problemas de rendimiento en los proyectos Unity. Por eso, eliminar las sentencias de registro y su costoso formato de cadena puede suponer una gran mejora del rendimiento.
Del mismo modo, los MonoBehaviours vacíos requieren recursos, por lo que debería eliminar los métodos Update o LateUpdate vacíos. Utilice directivas de preprocesador si emplea estos métodos para las pruebas:
#if UNITY_EDITOR
void Actualizar()
{
}
#endif
Aquí, usted puede utilizar la actualización en el editor para las pruebas sin sobrecarga innecesaria deslizarse en su construcción.
Esta entrada de blog sobre 10,000 llamadas de Actualización explica cómo Unity ejecuta el Monobehaviour.Update.
Utilice las opciones de Rastreo de pila en la Configuración del reproductor para controlar qué tipo de mensajes de registro aparecen. Si su aplicación está registrando errores o mensajes de advertencia en su compilación de lanzamiento (por ejemplo, para generar informes de fallos en la naturaleza), desactive Stack Traces para mejorar el rendimiento.
Más información sobre el registro de Stack Trace.
Unity no utiliza nombres de cadena para direccionar internamente propiedades de Animator, Material, o Shader. Para mayor rapidez, todos los nombres de las propiedades se convierten en identificadores de propiedad, 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 con valores de cadena realizan el hash de la cadena y luego envían el ID hash a los métodos con valores enteros.
Utilice Animator.StringToHash para los nombres de propiedades de Animator y Shader.PropertyToID para nombres de propiedades de Material y Shader.
La elección de la estructura de datos también influye en el rendimiento, ya que se itera miles de veces por fotograma. Siga la guía MSDN sobre estructuras de datos en C# como guía general para elegir la estructura adecuada.
Instantiate y Destroy pueden generar picos de recolección de basura (GC). Esto es generalmente un proceso lento, así que en lugar de instanciar y destruir GameObjects regularmente (por ejemplo, disparando balas de una pistola), utilice pools de objetos preasignados que pueden ser reutilizados y reciclados.
Crea las instancias reutilizables en un momento del juego, como durante una pantalla de menú o de carga, cuando 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, y desactivar los objetos en lugar de destruirlos, antes de devolverlos a la reserva. Esto reduce el número de asignaciones gestionadas en su proyecto y puede evitar problemas de GC.
Del mismo modo, evite añadir componentes en tiempo de ejecución; invocar AddComponent tiene algún coste. Unity debe comprobar si hay duplicados u otros componentes necesarios siempre que añada componentes en tiempo de ejecución. Instanciar un Prefab con los componentes deseados ya configurados es más eficaz, así que utilícelo en combinación con su Object Pool.
En relación con esto, al mover Transforms, utilice Transform.SetPositionAndRotation para actualizar tanto la posición como la rotación a la vez. Esto evita la sobrecarga de modificar un Transform dos veces.
Si necesita instanciar un GameObject en tiempo de ejecución, padre y reposicionarlo para su optimización, vea más abajo.
Para obtener más información sobre Object.Instantiate, consulte la API de secuencias de comandos.
Aprenda a crear un sistema simple de Object Pooling en Unity aquí.
Almacenar valores o ajustes invariables en un ScriptableObject en lugar de un MonoBehaviour. El ScriptableObject es un activo que vive dentro del proyecto. Sólo necesita ser configurado una vez, y no puede ser unido directamente a un GameObject.
Cree campos en el ScriptableObject para almacenar sus valores o configuraciones y, a continuación, haga referencia al ScriptableObject en sus MonoBehaviours. Utilizar campos del 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 y encuentre la documentación pertinente aquí.
Una de nuestras guías más completas reúne más de 80 consejos prácticos sobre cómo optimizar tus juegos para PC y consola. Creados por nuestros expertos ingenieros de Success y Accelerate Solutions, estos consejos en profundidad te ayudarán a sacar el máximo partido de Unity y a aumentar el rendimiento de tu juego.