Unity otimize seu jogo usando o Profiler

O desempenho é um aspecto fundamental de qualquer jogo e não é surpresa, não importa o quão bom seja o jogo, se rodar mal na máquina do usuário, não será tão agradável.

Como nem todo mundo tem um PC ou dispositivo de última geração (se você estiver segmentando para dispositivos móveis), é importante manter o desempenho em mente durante todo o processo de desenvolvimento.

Existem vários motivos pelos quais o jogo pode ser executado lentamente:

  • Renderização (muitas malhas com muitos polígonos, shaders complexos ou efeitos de imagem)
  • Áudio (principalmente causado por configurações incorretas de importação de áudio)
  • Código não otimizado (scripts que contêm funções que exigem desempenho nos lugares errados)

Neste tutorial, mostrarei como otimizar seu código com a ajuda do Unity Profiler.

Perfil

Historicamente, depurar o desempenho em Unity era uma tarefa tediosa, mas, desde então, um novo recurso foi adicionado, chamado Profiler.

Profiler é uma ferramenta em Unity que permite identificar rapidamente os gargalos em seu jogo monitorando o consumo de memória, o que simplifica muito o processo de otimização.

Janela do Unity Profiler

Baixo desempenho

Desempenho ruim pode acontecer a qualquer momento: digamos que você esteja trabalhando na instância inimiga e quando você a coloca na cena, ela funciona bem sem problemas, mas conforme você gera mais inimigos, você pode notar fps (quadros por segundo) começam a cair.

Confira o exemplo abaixo:

Na cena, tenho um cubo com um script anexado a ele, que move o cubo de um lado para o outro e exibe o nome do objeto:

SC_ShowName.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);
    }
}

Olhando para as estatísticas, podemos ver que o jogo roda a mais de 800 fps, então quase não tem impacto no desempenho.

Mas vamos ver o que acontecerá quando duplicarmos o Cubo 100 vezes:

Fps caiu mais de 700 pontos!

NOTA: Todos os testes foram feitos com o Vsync desativado

Geralmente, é uma boa ideia começar a otimizar quando o jogo começa a exibir gagueira, congelamento ou fps cai abaixo de 120.

Como usar o Profiler?

Para começar a usar o Profiler, você precisará de:

  • Comece seu jogo pressionando Jogar
  • Abra o Profiler em Window -> Analysis -> Profiler (ou pressione Ctrl + 7)

  • Aparecerá uma nova janela semelhante a esta:

Janela do Unity 3D Profiler

  • Pode parecer intimidante no começo (especialmente com todos aqueles gráficos, etc.), mas não é a parte que veremos.
  • Clique na guia Timeline e mude para Hierarchy:

  • Você notará 3 seções (EditorLoop, PlayerLoop e Profiler.CollectEditorStats):

  • Expanda o PlayerLoop para ver todas as partes onde o poder de computação está sendo gasto (NOTA: Se os valores do PlayerLoop não estiverem atualizando, clique no botão "Clear" na parte superior do Profiler janela).

Para obter os melhores resultados, direcione seu personagem do jogo para a situação (ou local) onde o jogo fica mais lento e aguarde alguns segundos.

  • Depois de esperar um pouco, pare o jogo e observe a lista PlayerLoop

Você precisa olhar para o valor GC Alloc, que significa Garbage Collection Allocation. Este é um tipo de memória que foi alocada pelo componente, mas não é mais necessária e está aguardando para ser liberada pela Coleta de Lixo. Idealmente, o código não deve gerar lixo (ou ser o mais próximo possível de 0).

Tempo ms também é um valor importante, mostra quanto tempo o código levou para ser executado em milissegundos, portanto, idealmente, você deve tentar reduzir isso valor também (armazenando valores em cache, evitando chamar funções que exigem desempenho a cada atualização, etc.).

Para localizar as partes problemáticas mais rapidamente, clique na coluna GC Alloc para classificar os valores de cima para baixo)

  • No gráfico de uso da CPU, clique em qualquer lugar para pular para esse quadro. Especificamente, precisamos observar os picos, onde o fps foi o mais baixo:

Gráfico de uso de CPU do Unity

Aqui está o que o Profiler revelou:

GUI.Repaint está alocando 45,4 KB, o que é bastante, expandindo revelou mais informações:

  • Isso mostra que a maioria das alocações vem dos métodos GUIUtility.BeginGUI() e OnGUI() no script SC_ShowName, sabendo que podemos começar a otimizar.

GUIUtility.BeginGUI() representa um método OnGUI() vazio (Sim, mesmo o método OnGUI() vazio aloca bastante memória) .

Use o Google (ou outro mecanismo de busca) para encontrar os nomes que você não reconhece.

Aqui está a parte OnGUI() que precisa ser otimizada:

    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);
    }

Otimização

Vamos começar a otimizar.

Cada script SC_ShowName chama seu próprio método OnGUI(), o que não é bom considerando que temos 100 instâncias. Então o que pode ser feito sobre isso? A resposta é: Ter um único script com o método OnGUI() que chame o método GUI para cada Cubo.

  • Primeiro, substituí o padrão OnGUI() no script SC_ShowName por public void GUIMethod() que será chamado de outro 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);
    }
  • Então eu criei um novo script e o chamei de método SC_GUIM:

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();
    }

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

O método SC_GUIM será anexado a um objeto aleatório na cena e chamará todos os métodos GUI.

  • Passamos de 100 métodos OnGUI() individuais para apenas um, vamos apertar o play e ver o resultado:

  • GUIUtility.BeginGUI() agora está alocando apenas 368B em vez de 36,7KB, uma grande redução!

No entanto, o método OnGUI() ainda está alocando memória, mas como sabemos que está chamando apenas GUIMethod() do script SC_ShowName, vamos direto para a depuração desse método.

Mas o Profiler mostra apenas informações globais, como vemos exatamente o que está acontecendo dentro do método?

Para depurar dentro do método, Unity tem uma API útil chamada Profiler.BeginSample

Profiler.BeginSample permite capturar uma seção específica do script, mostrando quanto tempo levou para ser concluído e quanta memória foi alocada.

  • Antes de usar a classe Profiler no código, precisamos importar o namespace UnityEngine.Profiling no início do script:
using UnityEngine.Profiling;
  • A amostra do Profiler é capturada adicionando Profiler.BeginSample("SOME_NAME"); no início da captura e adicionando Profiler.EndSample(); no final da captura, assim:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

Como não sei qual parte do GUIMethod() está causando alocações de memória, incluí cada linha em Profiler.BeginSample e Profiler.EndSample (Mas se seu método tiver muitas linhas, você definitivamente não precisa fechar cada linha, apenas dividi-la em partes iguais e depois trabalhar a partir daí).

Aqui está um método final com Amostras do Profiler implementadas:

    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();
    }
  • Agora pressiono Play e vejo o que aparece no Profiler:
  • Por conveniência, procurei por "sc_show_" no Profiler, pois todas as amostras começam com esse nome.

  • Interessante... Muita memória está sendo alocada em sc_show_names parte 3, que corresponde a esta parte do código:
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

Depois de pesquisar no Google, descobri que obter o nome do objeto aloca bastante memória. A solução é atribuir o nome de um Objeto a uma variável string em void Start(), assim ele será chamado apenas uma vez.

Aqui está o código otimizado:

SC_ShowName.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();
    }
}
  • Vamos ver o que o Profiler está mostrando:

Todas as amostras estão alocando 0B, então não há mais memória sendo alocada.