El PlayerLoop de Unity contiene funciones para interactuar con el núcleo del motor del juego. Esta estructura incluye una serie de sistemas que se encargan de la inicialización y de 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 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 reforzar 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 comience. Utilícelos 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 debe utilizar Start para tratar con objetos que sabe que pueden hablar con otros objetos, o consultarlos cuando han sido inicializados.
Consulte el diagrama de flujo del ciclo de vida del script para conocer el orden específico de ejecución de las funciones de evento.
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 retrollamadas por fotograma que efectivamente no ejecutan ningún código excepto la comprobación de esta condición.
Siempre que Unity llama a un método de mensaje como Update o LateUpdate, realiza una llamada de interoperabilidad, es decir, una llamada desde el lado C/C++ al lado C# gestionado. Para un pequeño número de objetos, esto no es un problema. Cuando se tienen miles de objetos, esta sobrecarga empieza a ser significativa.
Suscriba los objetos activos a este Gestor de Actualizaciones cuando necesiten devoluciones de llamada, y desuscríbalos 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 colocar código que debe actualizarse cada fotograma, pero puede extraer cualquier lógica que no necesite actualizarse con esa frecuencia.
Ejecute la lógica sólo cuando las cosas cambien. 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 la actualización, 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 cuadros.
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.
Como alternativa, utilice una clase Gestora de Actualizaciones personalizada para actualizar los objetos suscritos cada n tramas.
En versiones de Unity anteriores a la 2020.2, GameObject.Find, GameObject.GetComponent y Camera.main pueden resultar caros, por lo que es mejor evitar llamarlos en los métodos de actualización.
Además, intente evitar colocar métodos caros en OnEnable y OnDisable si se llaman con frecuencia. 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.Iniciodurante la fase de inicialización. Almacene en caché las referencias necesarias y reutilícelas más tarde. Consulte nuestra sección anterior sobre el PlayerLoop de Unity para conocer la ejecución de la orden de script con más detalle.
He aquí un ejemplo que demuestra el uso ineficiente de una llamada repetida a GetComponent:
void Actualizar()
{
Renderer myRenderer = GetComponent<Renderer>();
EjemploFunción(miRenderizador);
}
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 Actualizar 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 mermar 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 la que se muestra a continuación.
Genere su mensaje de registro con su clase personalizada. Si desactiva el preprocesador ENABLE_LOG en la Configuración del reproductor > Scripting Definir símbolos, todos sus 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 formateo de cadenas puede ser potencialmente una gran ganancia de rendimiento.
Del mismo modo, los MonoBehaviour vacíos requieren recursos, por lo que debe eliminar los métodos Update o LateUpdate vacíos. Utilice las directivas del preprocesador si emplea estos métodos para las pruebas:
#if UNITY_EDITOR
void Actualizar()
{
}
#endif
Aquí puede utilizar la actualización en el editor para realizar pruebas sin que se cuelen gastos innecesarios en su compilación.
Esta entrada del 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 in the wild), 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 las propiedades de Animator, Material o Shader. Para mayor rapidez, todos los nombres de las propiedades se convierten en ID de propiedadmediante hash, y estos ID se utilizan para direccionar las propiedades.
Cuando utilice un método Set o Get en un Animator, Material o Shader, aproveche el método de valor entero en lugar de los métodos de valor cadena. Los métodos con valores de cadena realizan el hashing de la cadena y luego reenvían el ID hasheado a los métodos con valores enteros.
Utilice Animator.StringToHash para los nombres de propiedades de Animator y Shader.PropertyToID para los nombres de propiedades de Material y Shader.
Relacionado con esto está la elección de la estructura de datos, que repercute en el rendimiento al iterar 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.
Instanciar y Destruir pueden generar picos de recolección de basura (GC). Por lo general, se trata de un proceso lento, así que en lugar de instanciar y destruir GameObjects con regularidad (por ejemplo, al disparar balas de una pistola), utilice pools de objetos preasignados que puedan reutilizarse y reciclarse.
Cree las instancias reutilizables en un momento del juego, como durante una pantalla de menú o una pantalla de carga, cuando el pico de CPU sea menos perceptible. Rastree 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 cierto 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 performante, así que utilícelo en combinación con su Object Pool.
Relacionado, cuando mueva Transforms, utilice Transform.EstablecerPosiciónYGiro 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, criarlo y reposicionarlo para optimizarlo, vea más abajo.
Para más información sobre Object.Instantiate, consulte la API de scripting.
Aprenda a crear un sencillo sistema de agrupación de objetos en Unity aquí.
Almacene valores o ajustes invariables en un ScriptableObject en lugar de en un MonoBehaviour. El ScriptableObject es un activo que vive dentro del proyecto. Sólo necesita configurarse una vez y no puede adjuntarse directamente a un GameObject.
Cree campos en el ScriptableObject para almacenar sus valores o ajustes y, a continuación, haga referencia al ScriptableObject en sus MonoBehaviours. El uso de campos del ScriptableObject puede evitar la duplicación innecesaria de datos cada vez que instancie 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 sus juegos para PC y consola. Creados por nuestros expertos ingenieros de Success y Accelerate Solutions, estos consejos en profundidad le ayudarán a sacar el máximo partido de Unity y a aumentar el rendimiento de su juego.