
Programação avançada e arquitetura de código
Entenda o Unity PlayerLoop
O Unity PlayerLoop contém funções para interagir com o núcleo do motor do jogo. Esta estrutura inclui uma série de sistemas que lidam com inicialização e atualizações por quadro. Todos os seus scripts dependerão deste PlayerLoop para criar a jogabilidade. Ao fazer o perfil, você verá o código do usuário do seu projeto sob o PlayerLoop – com componentes Editor sob o EditorLoop.
É importante entender a ordem de execução do FrameLoop do Unity. Cada script do Unity executa várias funções de evento em uma ordem predeterminada. Aprenda a diferença entre Awake, Start, Update e outras funções que criam o ciclo de vida de um script para fortalecer o desempenho.
Alguns exemplos incluem usar FixedUpdate em vez de Update ao lidar com um Rigidbody ou usar Awake em vez de Start para inicializar variáveis ou o estado do jogo antes que o jogo comece. Use isso para minimizar o código que é executado em cada quadro. Awake é chamado apenas uma vez durante a vida útil da instância do script e sempre antes das funções Start. Isso significa que você deve usar Start para lidar com objetos que você sabe que podem se comunicar com outros objetos ou consultá-los assim que forem inicializados.
Veja o fluxograma do ciclo de vida do script para a ordem específica de execução das funções de evento.

Construa um Gerenciador de Atualizações personalizado
Se o seu projeto tiver requisitos de desempenho exigentes (por exemplo, um jogo de mundo aberto), considere criar um Gerenciador de Atualizações personalizado usando Update, LateUpdate ou FixedUpdate.
Um padrão de uso comum para Update ou LateUpdate é executar a lógica apenas quando alguma condição é atendida. Isso pode levar a um número de callbacks por quadro que efetivamente não executam código, exceto para verificar essa condição.
Sempre que o Unity chama um método de mensagem como Update ou LateUpdate, ele faz uma chamada de interop – ou seja, uma chamada do lado C/C++ para o lado gerenciado C#. Para um pequeno número de objetos, isso não é um problema. Quando você tem milhares de objetos, essa sobrecarga começa a se tornar significativa.
Inscreva objetos ativos neste Gerenciador de Atualizações quando eles precisarem de callbacks e desinscreva quando não precisarem. Esse padrão pode reduzir muitas das chamadas de interop para seus Monobehaviour objetos.
Consulte as técnicas de otimização específicas do motor de jogo para exemplos de implementação.
Minimize o código que é executado a cada quadro
Considere se o código deve ser executado a cada quadro. Você pode mover lógica desnecessária para fora de Update, LateUpdate e FixedUpdate. Essas funções de evento do Unity são lugares convenientes para colocar código que deve ser atualizado a cada quadro, mas você pode extrair qualquer lógica que não precisa ser atualizada com essa frequência.
Execute a lógica apenas quando as coisas mudam. Lembre-se de aproveitar técnicas como o padrão observador na forma de eventos para acionar uma assinatura de função específica.
Se você precisar usar Update, pode executar o código a cada n quadros. Esta é uma maneira de aplicar Time Slicing, uma técnica comum de distribuir uma carga de trabalho pesada em vários quadros.
Neste exemplo, executamos a ExampleExpensiveFunction uma vez a cada três quadros.
O truque é intercalar isso com outros trabalhos que são executados nos outros quadros. Neste exemplo, você poderia "agendar" outras funções caras quando Time.frameCount % interval == 1 ou Time.frameCount % interval == 2.
Alternativamente, use uma classe de Gerenciador de Atualização personalizada para atualizar os objetos inscritos a cada n quadros.
Armazene em cache os resultados de funções caras
Em versões do Unity anterior a 2020.2, GameObject.Find, GameObject.GetComponent e Camera.main podem ser caros, então é melhor evitar chamá-los em métodos Update.
Além disso, tente evitar colocar métodos caros em OnEnable e OnDisable se forem chamados com frequência. Chamar esses métodos com frequência pode contribuir para picos de CPU.
Sempre que possível, execute funções caras, como MonoBehaviour.Awake e MonoBehaviour.Start durante a fase de inicialização. Armazene em cache as referências necessárias e reutilize-as mais tarde. Confira nossa seção anterior sobre o Unity PlayerLoop para a execução da ordem dos scripts em mais detalhes.
Aqui está um exemplo que demonstra o uso ineficiente de uma chamada repetida de GetComponent:
void Update()
{
Renderer myRenderer = GetComponent();
ExampleFunction(myRenderer);
}
Em vez disso, invoque GetComponent apenas uma vez, pois o resultado da função é armazenado em cache. O resultado em cache pode ser reutilizado no Update sem chamadas adicionais para GetComponent.
Leia mais sobre a Ordem de execução para funções de evento.
Evite eventos vazios do Unity e declarações de log de depuração
Declarações de log (especialmente em Update, LateUpdate ou FixedUpdate) podem prejudicar o desempenho, então desative suas declarações de log antes de fazer uma build. Para fazer isso rapidamente, considere criar um Atributo Condicional junto com uma diretiva de pré-processamento.
Por exemplo, você pode querer criar uma classe personalizada como mostrado abaixo.
Gere sua mensagem de log com sua classe personalizada. Se você desativar o ENABLE_LOG pré-processador nas Configurações do Player > Símbolos de Definição de Script, todas as suas declarações de log desaparecem de uma só vez.
Manipular strings e texto é uma fonte comum de problemas de desempenho em projetos Unity. É por isso que remover declarações de log e sua formatação de string cara pode potencialmente ser uma grande vitória de desempenho.
Da mesma forma, MonoBehaviours vazios requerem recursos, então você deve remover métodos em branco Update ou LateUpdate. Use diretivas de pré-processador se você estiver empregando esses métodos para testes:
#if UNITY_EDITOR
void Update()
{
}
#endif
Aqui, você pode usar o Update no Editor para testes sem sobrecarga desnecessária entrando na sua build.
Este post no blog sobre 10.000 chamadas de Update explica como o Unity executa o Monobehaviour.Update.
Desative o registro de Stack Trace
Use as opções de Stack Trace nas Configurações do Player para controlar que tipo de mensagens de log aparecem. Se sua aplicação estiver registrando erros ou mensagens de aviso na sua build de lançamento (por exemplo, para gerar relatórios de falhas no campo), desative os Stack Traces para melhorar o desempenho.
Saiba mais sobre registro de Stack Trace.
Use valores de hash em vez de parâmetros de string
Unity não usa nomes de string para endereçar propriedades de Animator, Material ou Shader internamente. Para velocidade, todos os nomes de propriedades são hashados em ID de Propriedades, e esses IDs são usados para endereçar as propriedades.
Ao usar um Conjunto ou Obter método em um Animator, Material ou Shader, aproveite o método de valor inteiro em vez dos métodos de valor string. Os métodos de valor string realizam hashing de string e, em seguida, encaminham o ID hash para os métodos de valor inteiro.
Use Animator.StringToHash para nomes de propriedades do Animator e Shader.PropertyToID para nomes de propriedades de Material e Shader.
Relacionado está a escolha da estrutura de dados, que impacta o desempenho à medida que você itera milhares de vezes por quadro. Siga o guia MSDN para estruturas de dados em C# como um guia geral para escolher a estrutura certa.

Agrupe seus objetos
Instanciar e Destruir podem gerar picos de coleta de lixo (GC). Este é geralmente um processo lento, então, em vez de instanciar e destruir GameObjects regularmente (por exemplo, disparar balas de uma arma), use pools de objetos pré-alocados que podem ser reutilizados e reciclados.
Crie as instâncias reutilizáveis em um ponto do jogo, como durante uma tela de menu ou uma tela de carregamento, quando um pico de CPU é menos perceptível. Acompanhe este "pool" de objetos com uma coleção. Durante o jogo, simplesmente habilite a próxima instância disponível quando necessário e desative objetos em vez de destruí-los, antes de devolvê-los ao pool. Isso reduz o número de alocações gerenciadas em seu projeto e pode prevenir problemas de GC.
Da mesma forma, evite adicionar componentes em tempo de execução; Invocar AddComponent vem com algum custo. Unity deve verificar duplicatas ou outros componentes necessários sempre que adicionar componentes em tempo de execução. Instanciar um Prefab com os componentes desejados já configurados é mais eficiente, então use isso em combinação com seu Pool de Objetos.
Relacionado, ao mover Transformações, use Transform.SetPositionAndRotation para atualizar tanto a posição quanto a rotação de uma vez. Isso evita a sobrecarga de modificar um Transform duas vezes.
Se você precisar instanciar um GameObject em tempo de execução, parentar e reposicionar para otimização, veja abaixo.
Para mais sobre Object.Instantiate, veja a API de Scripting.
Aprenda como criar um sistema simples de Pooling de Objetos no Unity aqui.

Aproveite o poder dos ScriptableObjects
Armazene valores ou configurações imutáveis em um ScriptableObject em vez de um MonoBehaviour. O ScriptableObject é um ativo que vive dentro do projeto. Ele só precisa ser configurado uma vez e não pode ser anexado diretamente a um GameObject.
Crie campos no ScriptableObject para armazenar seus valores ou configurações, e então faça referência ao ScriptableObject em seus MonoBehaviours. Usar campos do ScriptableObject pode evitar a duplicação desnecessária de dados toda vez que você instanciar um objeto com esse MonoBehaviour.
Assista a este tutorial de Introdução aos ScriptableObjects e encontre a documentação relevante aqui.

Um de nossos guias mais abrangentes já reúne mais de 80 dicas práticas sobre como otimizar seus jogos para PC e console. Criadas por nossos engenheiros especialistas em Sucesso e Soluções Aceleradas, essas dicas detalhadas ajudarão você a aproveitar ao máximo o Unity e melhorar o desempenho do seu jogo.