Unity optimiza tu juego con Profiler

El rendimiento es un aspecto clave de cualquier juego y no sorprende que, sin importar qué tan bueno sea el juego, si funciona mal en la máquina del usuario, no se sentirá tan agradable.

Dado que no todo el mundo tiene una PC o un dispositivo de gama alta (si su objetivo es un dispositivo móvil), es importante tener en cuenta el rendimiento durante todo el proceso de desarrollo.

Hay varias razones por las que el juego podría funcionar lentamente:

  • Representación (demasiadas mallas de alto contenido de polígonos, sombreadores complejos o efectos de imagen)
  • Audio (principalmente causado por configuración de importación de audio incorrecta)
  • Código no optimizado (secuencias de comandos que contienen funciones exigentes de rendimiento en los lugares equivocados)

En este tutorial, mostraré cómo optimizar su código con la ayuda de Unity Profiler.

perfilador

Históricamente, la depuración del rendimiento en Unity era una tarea tediosa, pero desde entonces, se ha agregado una nueva característica llamada Perfilador.

Profiler es una herramienta en Unity que le permite identificar rápidamente los cuellos de botella en su juego al monitorear el consumo de memoria, lo que simplifica enormemente el proceso de optimización.

Ventana del generador de perfiles de Unity

Mal desempeño

El mal rendimiento puede ocurrir en cualquier momento: supongamos que está trabajando en la instancia enemiga y cuando la coloca en la escena, funciona bien sin ningún problema, pero a medida que genera más enemigos, puede notar fps (fotogramas por segundo ) comienzan a caer.

Revisa el ejemplo a continuación:

En la Escena, tengo un Cubo con un script adjunto, que mueve el Cubo de lado a lado y muestra el nombre del objeto:

SC_MostrarNombre.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
}

Mirando las estadísticas, podemos ver que el juego funciona a más de 800 fps, por lo que apenas tiene impacto en el rendimiento.

Pero veamos qué pasará cuando dupliquemos el Cubo 100 veces:

¡Los fps se redujeron en más de 700 puntos!

NOTA: Todas las pruebas se realizaron con Vsync deshabilitado

En general, es una buena idea comenzar a optimizar cuando el juego comienza a tartamudear, congelarse o los fps caen por debajo de 120.

¿Cómo usar el perfilador?

Para comenzar a usar Profiler, necesitará:

  • Comienza tu juego presionando Play
  • Abra Profiler yendo a Ventana -> Análisis -> Profiler (o presione Ctrl + 7)

  • Aparecerá una nueva ventana que se parece a esto:

Ventana Perfilador 3D de Unity

  • Puede parecer intimidante al principio (especialmente con todos esos gráficos, etc.), pero no es la parte que veremos.
  • Haga clic en la pestaña Línea de tiempo y cámbiela a Jerarquía:

  • Notarás 3 secciones (EditorLoop, PlayerLoop y Profiler.CollectEditorStats):

  • Expanda PlayerLoop para ver todas las partes en las que se gasta la potencia de cálculo (NOTA: si los valores de PlayerLoop no se actualizan, haga clic en el botón "Clear" en la parte superior de la ventana Profiler).

Para obtener los mejores resultados, dirige a tu personaje del juego a la situación (o lugar) donde el juego se retrasa más y espera un par de segundos.

  • Después de esperar un poco, detén el juego y observa la lista de PlayerLoop

Debe mirar el valor GC Alloc, que significa Asignación de recolección de basura. Este es un tipo de memoria que ha sido asignada por el componente pero ya no es necesaria y está esperando a ser liberada por la recolección de elementos no utilizados. Idealmente, el código no debería generar basura (o estar lo más cerca posible de 0).

El tiempo ms también es un valor importante, muestra cuánto tiempo tardó en ejecutarse el código en milisegundos, por lo que, idealmente, también debería intentar reducir este valor (al almacenar valores en caché, evitando llamar a funciones que exigen rendimiento cada Actualización, etc.).

Para ubicar las partes problemáticas más rápido, haga clic en la columna GC Alloc para ordenar los valores de mayor a menor)

  • En el gráfico de uso de CPU, haga clic en cualquier lugar para saltar a ese cuadro. Específicamente, debemos observar los picos, donde los fps fueron los más bajos:

Gráfico de uso de CPU de Unity

Esto es lo que reveló el Profiler:

GUI.Repaint está asignando 45,4 KB, que es bastante, al expandirlo reveló más información:

  • Muestra que la mayoría de las asignaciones provienen del método GUIUtility.BeginGUI() y OnGUI() en el script SC_ShowName, sabiendo que podemos comenzar a optimizar.

GUIUtility.BeginGUI() representa un método OnGUI() vacío (Sí, incluso el método OnGUI() vacío asigna bastante memoria).

Utilice Google (u otro motor de búsqueda) para encontrar los nombres que no reconoce.

Aquí está la parte OnGUI() que necesita ser optimizada:

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }

Mejoramiento

Comencemos a optimizar.

Cada secuencia de comandos SC_ShowName llama a su propio método OnGUI(), lo cual no es bueno teniendo en cuenta que tenemos 100 instancias. Entonces, ¿qué se puede hacer al respecto? La respuesta es: tener un solo script con el método OnGUI() que llama al método GUI para cada Cubo.

  • Primero, reemplacé el OnGUI() predeterminado en el script SC_ShowName con GUMethod() vacío público que se llamará desde otro script:
    public void GUIMethod()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
  • Luego creé un nuevo script y lo llamé SC_GUIMethod:

SC_GUIMethod.cs

using UnityEngine;

public class SC_GUIMethod : MonoBehaviour
{
    SC_ShowName[] instances; //All instances where GUI method will be called

    void Start()
    {
        //Find all instances
        instances = FindObjectsOfType<SC_ShowName>();
    }

    void OnGUI()
    {
        for(int i = 0; i < instances.Length; i++)
        {
            instances[i].GUIMethod();
        }
    }
}

SC_GUIMethod se adjuntará a un objeto aleatorio en la escena y llamará a todos los métodos de GUI.

  • Pasamos de tener 100 métodos OnGUI() individuales a tener solo uno, presionemos play y veamos el resultado:

  • GUIUtility.BeginGUI() ahora solo asigna 368B en lugar de 36.7KB, ¡una gran reducción!

Sin embargo, el método OnGUI() sigue asignando memoria, pero como sabemos que solo está llamando a GUIMethod() desde el script SC_ShowName, vamos directamente a depurar ese método.

Pero el generador de perfiles solo muestra información global, ¿cómo vemos qué sucede exactamente dentro del método?

Para depurar dentro del método, Unity tiene una API útil llamada Profiler.BeginSample

Profiler.BeginSample le permite capturar una sección específica del script, mostrando cuánto tiempo tardó en completarse y cuánta memoria se asignó.

  • Antes de usar la clase Profiler en el código, debemos importar el espacio de nombres UnityEngine.Profiling al comienzo del script:
using UnityEngine.Profiling;
  • La muestra del perfilador se captura agregando Profiler.BeginSample("SOME_NAME"); al comienzo de la captura y agregando Profiler.EndSample() ; al final de la captura, así:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

Como no sé qué parte de GUIMethod() está causando asignaciones de memoria, adjunté cada línea en Profiler.BeginSample y Profiler.EndSample (pero si su método tiene muchas líneas, definitivamente no necesita adjuntar cada línea, simplemente divídala en partes iguales y luego trabaje desde allí).

Aquí hay un método final con Profiler Samples implementado:

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
        Profiler.EndSample();
    }
  • Ahora presiono Reproducir y veo lo que muestra en el Perfilador:
  • Para mayor comodidad, busqué "sc_show_" en Profiler, ya que todas las muestras comienzan con ese nombre.

  • Interesante... Se está asignando mucha memoria en sc_show_names parte 3, que corresponde a esta parte del código:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

Después de buscar en Google, descubrí que obtener el nombre de Object asigna bastante memoria. La solución es asignar el nombre de un objeto a una variable de cadena en void Start(), de esa manera solo se llamará una vez.

Aquí está el código optimizado:

SC_MostrarNombre.cs

using UnityEngine;
using UnityEngine.Profiling;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    string objectName = "";

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
        objectName = gameObject.name; //Store Object name to a variable
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
        Profiler.EndSample();
    }
}
  • Veamos lo que muestra el generador de perfiles:

Todas las muestras asignan 0B, por lo que no se asigna más memoria.

Artículos sugeridos
Consejos de optimización para Unity
Mejorar el rendimiento de un juego móvil en Unity
Cómo utilizar Update en Unity
Configuración de importación de clips de audio de Unity para obtener el mejor rendimiento
El generador de vallas publicitarias para la unidad
Cómo convertirse en un mejor programador en Unity
Cómo hacer un juego inspirado en FNAF en Unity