Otimize o desempenho do seu jogo para dispositivos móveis: Dicas sobre perfilagem, memória e arquitetura de código dos principais engenheiros da Unity

THOMAS KROGH-JACOBSEN / UNITY TECHNOLOGIESSenior Technical Content Marketing Manager
Jun 23, 2021|15 Min
Otimize o desempenho do seu jogo para dispositivos móveis: Dicas sobre perfilagem, memória e arquitetura de código dos principais engenheiros da Unity
Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.
Nossa equipe de Produção do Unity Studio conhece o código-fonte como a palma da mão e apoia uma infinidade de clientes da Unity para que possam aproveitar ao máximo o motor. Em seu trabalho, eles mergulham fundo em projetos de criadores para ajudar a identificar pontos onde o desempenho pode ser otimizado para maior velocidade, estabilidade e eficiência. Sentamos com essa equipe, composta pelos engenheiros de software mais experientes da Unity, e pedimos que compartilhassem um pouco de sua experiência em otimização de jogos para dispositivos móveis.

À medida que nossos engenheiros começaram a compartilhar suas percepções sobre a otimização de jogos para dispositivos móveis, percebemos rapidamente que havia informações demais para o único post de blog que havíamos planejado. Em vez disso, decidimos transformar sua montanha de conhecimento em um e-book completo (que você pode baixar aqui), além de uma série de postagens no blog que destacam algumas dessas mais de 75 dicas práticas.

Começamos o primeiro post desta série focando em como você pode melhorar o desempenho do seu jogo com perfilagem, memória e arquitetura de código. Nas próximas semanas, seguiremos com mais dois posts: o primeiro cobrindo física de UI, seguido por outro sobre áudio e ativos, configuração de projeto e gráficos.

Quer conferir a série completa agora? Baixe o e-book completo gratuitamente.

Vamos lá!

Perfilagem

Que lugar melhor para começar do que a perfilagem e o processo de coleta e ação sobre dados de desempenho móvel? É aqui que a otimização do desempenho móvel realmente começa.

Perfilar cedo, frequentemente e no dispositivo alvo

O Profiler do Unity fornece informações essenciais de desempenho sobre sua aplicação, mas não pode ajudar se você não o usar. Perfilar seu projeto no início do desenvolvimento, não apenas quando você estiver perto de lançar. Investigue falhas ou picos assim que aparecerem. À medida que você desenvolve uma "assinatura de desempenho" para seu projeto, será capaz de identificar novos problemas mais facilmente.

Enquanto o perfilamento no Editor pode lhe dar uma ideia do desempenho relativo de diferentes sistemas em seu jogo, o perfilamento em cada dispositivo lhe dá a oportunidade de obter insights mais precisos. Perfilar uma build de desenvolvimento em dispositivos-alvo sempre que possível. Lembre-se de perfilar e otimizar tanto para os dispositivos de maior quanto de menor especificação que você planeja suportar.

Juntamente com o Profiler do Unity, você pode aproveitar ferramentas nativas do iOS e Android para testes de desempenho adicionais em seus respectivos motores:

Certos hardwares podem aproveitar ferramentas de perfilamento adicionais (por exemplo, Arm Mobile Studio, Intel VTune, e Snapdragon Profiler). Veja Perfilando Aplicações Feitas com Unity para mais informações.

Concentre-se em otimizar as áreas certas

Não adivinhe ou faça suposições sobre o que está diminuindo o desempenho do seu jogo. Use o Profiler do Unity e ferramentas específicas da plataforma para localizar a fonte precisa de um atraso.

Claro, nem toda otimização descrita aqui se aplicará à sua aplicação. Algo que funciona bem em um projeto pode não se traduzir para o seu. Identifique gargalos genuínos e concentre seus esforços no que beneficia seu trabalho.

Entenda como o profiler do Unity funciona

O Profiler do Unity pode ajudá-lo a detectar as causas de quaisquer atrasos ou congelamentos em tempo de execução e entender melhor o que está acontecendo em um quadro específico, ou ponto no tempo. Ative as trilhas de CPU e Memória por padrão. Você pode monitorar Módulos de Profiler suplementares como Renderizador, Áudio e Física, conforme necessário para seu jogo (por exemplo, jogabilidade pesada em física ou baseada em música).

Use o Profiler do Unity para testar o desempenho e a alocação de recursos para sua aplicação.
Use o Profiler do Unity para testar o desempenho e a alocação de recursos para sua aplicação.

Construa a aplicação para seu dispositivo marcando Build de Desenvolvimento e Autoconectar Profiler, ou conecte manualmente para acelerar o tempo de inicialização do aplicativo.

Configurações de compilação no editor

Escolha o alvo da plataforma para perfilagem. O botão Gravar rastreia vários segundos da reprodução da sua aplicação (300 quadros por padrão). Vá para Unity > Preferências > Análise > Profiler > Contagem de Quadros para aumentar isso até 2000 se você precisar de capturas mais longas. Embora isso signifique que o Editor do Unity precisa fazer mais trabalho de CPU e ocupar mais memória, pode ser útil dependendo do seu cenário específico.

Este é um profiler baseado em instrumentação que perfila os tempos de código explicitamente envolvidos em ProfileMarkers (como os métodos Start ou Update de MonoBehaviour, ou chamadas de API específicas). Além disso, ao usar a configuração Perfilagem Profunda, o Unity pode perfilar o início e o fim de cada chamada de função no seu código de script para informar exatamente qual parte da sua aplicação está causando uma desaceleração.

Visualização da linha do tempo no editor
Use a visualização da linha do tempo para determinar se você está limitado pela CPU ou pela GPU.

Ao perfilar seu jogo, recomendamos que você cubra tanto os picos quanto o custo de um quadro médio no seu jogo. Entender e otimizar operações caras que ocorrem em cada quadro pode ser mais útil para aplicações que estão rodando abaixo da taxa de quadros alvo. Ao procurar picos, explore primeiro operações caras (por exemplo, física, IA, animação) e coleta de lixo.

Clique na janela para analisar um quadro específico. Em seguida, use a visualização Linha do Tempo ou Hierarquia para o seguinte:

  • Linha do Tempo mostra a divisão visual do tempo para um quadro específico. Isso permite que você visualize como as atividades se relacionam entre si e em diferentes threads. Use esta opção para determinar se você está limitado pela CPU ou pela GPU.
  • Hierarquia mostra a hierarquia de ProfileMarkers, agrupados juntos. Isso permite que você classifique as amostras com base no custo de tempo em milissegundos (Tempo ms e Auto ms). Você também pode contar o número de Chamadas para uma função e a memória gerenciada do heap (GC Aloc) no quadro.
Classificando ProfileMarkers por custo de tempo
A visualização Hierarchy permite que você classifique ProfileMarkers por custo de tempo.

Leia uma visão geral completa do Unity Profiler aqui. Aqueles que são novos em profiling também podem assistir a esta Introdução ao Profiling do Unity.

Antes de otimizar qualquer coisa em seu projeto, salve o arquivo .data do Profiler. Implemente suas alterações e compare os dados salvos antes e depois da modificação. Confie neste ciclo para melhorar o desempenho: perfilar, otimizar e comparar. Então, enxágue e repita.

Use o Profile Analyzer

Esta ferramenta permite que você agregue múltiplos quadros de dados do Profiler e, em seguida, localize quadros de interesse. Quer ver o que acontece com o Profiler depois que você faz uma alteração em seu projeto? A visualização Comparar permite que você carregue e diferencie dois conjuntos de dados, para que você possa testar alterações e melhorar seus resultados. O Profile Analyzer está disponível através do Gerenciador de Pacotes do Unity.

Olhar mais profundo para o Profile Analyzer no editor
Mergulhe ainda mais em quadros e dados de marcadores com o Profile Analyzer, que complementa o Profiler existente.

Trabalhe com um orçamento de tempo específico por quadro

Cada quadro terá um orçamento de tempo baseado em seus quadros-alvo por segundo (fps). Idealmente, um aplicativo rodando a 30 fps permitirá aproximadamente 33,33 ms por quadro (1000 ms / 30 fps). Da mesma forma, um alvo de 60 fps deixa 16,66 ms por quadro.

Os dispositivos podem exceder esse orçamento por curtos períodos de tempo (por exemplo, para cenas ou sequências de carregamento), mas não por uma duração prolongada.

Contabilizar a temperatura do dispositivo

Para dispositivos móveis, no entanto, não recomendamos usar esse tempo máximo de forma consistente, pois o dispositivo pode superaquecer e o SO pode reduzir a velocidade da CPU e da GPU. Recomendamos que você use apenas cerca de 65% do tempo disponível para permitir o resfriamento entre os quadros. Um orçamento típico de quadro será de aproximadamente 22 ms por quadro a 30 fps e 11 ms por quadro a 60 fps.

A maioria dos dispositivos móveis não possui resfriamento ativo como seus equivalentes de desktop. Os níveis de calor físico podem impactar diretamente o desempenho.

Se o dispositivo estiver muito quente, o Profiler pode perceber e relatar um desempenho ruim, mesmo que isso não seja motivo de preocupação a longo prazo. Para combater o superaquecimento durante a análise, faça a análise em curtos intervalos. Isso resfria o dispositivo e simula condições do mundo real. Nossa recomendação geral é manter o dispositivo fresco por 10-15 minutos antes de analisar novamente.

Determinar se você está limitado pela GPU ou pela CPU

O Profiler pode informar se sua CPU está levando mais tempo do que o orçamento de quadro alocado, ou se o culpado é sua GPU. Ele faz isso emitindo marcadores prefixados com Gfx da seguinte forma:

  • Se você ver o marcador Gfx.WaitForCommands , isso significa que a thread de renderização está pronta, mas você pode estar esperando por um gargalo na thread principal.
  • Se você frequentemente encontrar Gfx.WaitForPresent, isso significa que a thread principal estava pronta, mas estava esperando que a GPU apresentasse o quadro.
Memória

Unity emprega gerenciamento automático de memória para seu código e scripts gerados pelo usuário. Pequenos pedaços de dados, como variáveis locais de tipo valor, são alocados na pilha. Pedaços maiores de dados e armazenamento de longo prazo são alocados no heap gerenciado.

O coletor de lixo identifica periodicamente e desaloca a memória do heap não utilizada. Enquanto isso roda automaticamente, o processo de examinar todos os objetos na pilha pode fazer o jogo travar ou rodar lentamente.

Otimizar o uso da memória significa estar consciente de quando você aloca e desaloca memória da pilha, e como você minimiza o efeito da coleta de lixo. Veja Entendendo a pilha gerenciada para mais informações.

Uma olhada no Profiler de Memória no editor
Capture, inspecione e compare instantâneas no Profiler de Memória.

Use o Profiler de Memória

Este complemento separado (disponível como um pacote Experimental ou de Pré-visualização no Gerenciador de Pacotes) pode tirar uma instantânea da sua memória da pilha gerenciada, para ajudá-lo a identificar problemas como fragmentação e vazamentos de memória.

Clique na visualização do Mapa de Árvore para rastrear uma variável até o objeto nativo que está segurando a memória. Aqui, você pode identificar problemas comuns de consumo de memória, como texturas excessivamente grandes ou ativos duplicados.

Aprenda como aproveitar o Profiler de Memória no Unity para um uso melhor da memória. Você também pode conferir nossa documentação oficial do Profiler de Memória.

Reduza o impacto da coleta de lixo (GC)

Unity usa o coletor de lixo Boehm-Demers-Weiser, que para de executar o código do seu programa e só retoma a execução normal uma vez que seu trabalho está completo.

Esteja ciente de certas alocações desnecessárias na pilha, que podem causar picos de GC:

  • Strings: Em C#, strings são tipos de referência, não tipos de valor. Reduza a criação ou manipulação desnecessária de strings. Evite analisar arquivos de dados baseados em string, como JSON e XML; armazene dados em ScriptableObjects ou formatos como MessagePack ou Protobuf em vez disso. Use a classe StringBuilder se você precisar construir strings em tempo de execução.
  • Chamadas de função do Unity: Algumas funções criam alocações de heap. Referencie caches para arrays em vez de alocá-los no meio de um loop. Além disso, aproveite certas funções que evitam gerar lixo. Por exemplo, use GameObject.CompareTag em vez de comparar manualmente uma string com GameObject.tag (pois retornar uma nova string cria lixo).
  • Boxing: Evite passar uma variável de tipo valor em vez de uma variável de tipo referência. Isso cria um objeto temporário, e o lixo potencial que vem com isso converte implicitamente o tipo valor em um tipo objeto (por exemplo, int i = 123; object o = i). Em vez disso, tente fornecer substituições concretas com o tipo valor que você deseja passar. Generics também podem ser usados para essas substituições.
  • Corrotinas: Embora yield não produza lixo, criar um novo objeto WaitForSeconds faz. Cache e reutilize o objeto WaitForSeconds em vez de criá-lo na linha yield.
  • LINQ e Expressões Regulares: Ambos geram lixo devido ao boxing nos bastidores. Evite LINQ e Expressões Regulares se o desempenho for um problema. Escreva loops for e use listas como uma alternativa para criar novos arrays.

Tempo de coleta de lixo, se possível

Se você tiver certeza de que uma pausa na coleta de lixo não afetará um ponto específico em seu jogo, você pode acionar a coleta de lixo com System.GC.Collect.

Veja Compreendendo a Gestão Automática de Memória para exemplos de como usar isso a seu favor.

Use o coletor de lixo incremental para dividir a carga de trabalho do GC

Em vez de criar uma única interrupção longa durante a execução do seu programa, a coleta de lixo incremental usa múltiplas interrupções muito mais curtas que distribuem a carga de trabalho ao longo de muitos quadros. Se a coleta de lixo estiver impactando o desempenho, tente habilitar esta opção para ver se pode reduzir o problema de picos de GC. Use o Analisador de Perfil para verificar seu benefício para sua aplicação.

Uma olhada no Coletor de Lixo Incremental
Use o Coletor de Lixo Incremental para reduzir picos de GC.
Programação e arquitetura de código

O Unity PlayerLoop contém funções para interagir com o núcleo do motor do jogo. Esta estrutura inclui vários 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 perfil, você verá o código do usuário do seu projeto sob o PlayerLoop (com componentes do Editor sob o EditorLoop).

Visão ampliada de um profiler
O Profiler mostrará seus scripts personalizados, configurações e gráficos no contexto da execução de todo o motor.
Uma visão do PlayerLoop

Conheça o PlayerLoop e o ciclo de vida de um script.

Você pode otimizar seus scripts com as seguintes dicas e truques.

Entenda o Unity PlayerLoop

Certifique-se de entender a ordem de execução do loop de quadros do Unity. Todo script do Unity executa várias funções de evento em uma ordem predeterminada. Você deve entender a diferença entre Awake, Start, Update e outras funções que criam o ciclo de vida de um script.

Consulte o Fluxograma do Ciclo de Vida do Script para a ordem específica de execução das funções de evento.

Minimize o código que roda a cada quadro

Considere se o código deve rodar a cada quadro. Mova a lógica desnecessária para fora de Update, LateUpdate e FixedUpdate. Essas funções de evento são lugares convenientes para colocar código que deve ser atualizado a cada quadro, enquanto extrai qualquer lógica que não precisa ser atualizada com essa frequência. Sempre que possível, execute a lógica apenas quando as coisas mudarem.

Se você fizer questão de usar Update, considere rodar o código a cada n quadros. Esta é uma maneira de aplicar a divisão de tempo, uma técnica comum de distribuir uma carga de trabalho pesada em múltiplos quadros. Neste exemplo, rodamos a ExampleExpensiveFunction uma vez a cada três quadros:

private int interval = 3;

void Update()
{
    if (Time.frameCount % interval == 0)
    {
        ExampleExpensiveFunction();
    }
}

Evite lógica pesada em Start/Awake

Quando sua primeira cena carrega, essas funções são chamadas para cada objeto:

  • Awake
  • OnEnable
  • Start

Evite lógica cara nessas funções até que sua aplicação renderize seu primeiro quadro. Caso contrário, você pode encontrar tempos de carregamento mais longos do que o necessário.

Consulte a ordem de execução para funções de evento para detalhes sobre o carregamento da primeira cena.

Evite eventos vazios do Unity

Mesmo MonoBehaviours vazios requerem recursos, então você deve remover métodos Update ou LateUpdate em branco.

Use diretivas de pré-processador se você estiver empregando esses métodos para testes:

#if UNITY_EDITOR
void Update()
{
}
#endif

Aqui, você pode usar livremente o Update no Editor para testes sem sobrecarga desnecessária entrando na sua compilação.

Remover declarações de log de depuração

Declarações de log (especialmente em Atualizar, AtualizaçãoTardia ou AtualizaçãoFixa) podem prejudicar o desempenho. Desative suas declarações de Log antes de fazer uma compilação.

Para fazer isso mais facilmente, considere criar um atributo Condicional junto com uma diretiva de pré-processamento. Por exemplo, crie uma classe personalizada como esta:

public static class Logging
{
    [System.Diagnostics.Conditional("ENABLE_LOG")]
    static public void Log(object message)
    {
        UnityEngine.Debug.Log(message);
    }
}
Uma visão de ENABLE_LOG
Adicionar uma diretiva de pré-processador personalizada permite que você particione seus scripts.

Gere sua mensagem de log com sua classe personalizada. Se você desativar o pré-processador ENABLE_LOG nas Configurações do Player, todas as suas declarações de Log desaparecem de uma só vez.

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 e Shader internamente. Para velocidade, todos os nomes de propriedades são convertidos em IDs de propriedade, e esses IDs são realmente usados para endereçar as propriedades.

Ao usar um método Set ou Get em um Animator, Material ou Shader, utilize o método de valor inteiro em vez dos métodos de valor string. Os métodos de string simplesmente realizam a hash 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.

Escolha a estrutura de dados certa

Sua escolha de estrutura de dados impacta a eficiência enquanto você itera milhares de vezes por quadro. Não tem certeza se deve usar uma Lista, Array ou Dicionário para sua coleção? Siga o guia MSDN sobre estruturas de dados em C# como um guia geral para escolher a estrutura correta.

Evite adicionar componentes em tempo de execução

Invocando AddComponent em tempo de execução tem um custo. Unity deve verificar componentes duplicados ou outros componentes necessários sempre que adicionar componentes em tempo de execução.

Instanciando um Prefab com os componentes desejados já configurados é geralmente mais eficiente.

Cache GameObjects e componentes

GameObject.Find, GameObject.GetComponent, e Camera.main (em versões anteriores a 2020.2) podem ser caros, então é melhor evitar chamá-los em Update métodos. Em vez disso, chame-os em Start e armazene os resultados.

Aqui está um exemplo que demonstra o uso ineficiente de uma chamada repetida de GetComponent:

void Update()
{
    Renderer myRenderer = GetComponent<Renderer>();
    ExampleFunction(myRenderer);
}

Em vez disso, invoque GetComponent apenas uma vez, pois o resultado da função é armazenado. O resultado armazenado pode ser reutilizado em Update sem mais chamadas para GetComponent.

private Renderer myRenderer;

void Start()
{
    myRenderer = GetComponent<Renderer>();
}

void Update()
{
    ExampleFunction(myRenderer);
}

Use pools de objetos

Instantiate e Destroy podem gerar lixo e picos de coleta de lixo (GC), e geralmente é um processo lento. 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.

Uma visão ampliada do ObjectPool
Neste exemplo, o ObjectPool cria 20 instâncias de PlayerLaser para reutilização.

Crie as instâncias reutilizáveis em um ponto do jogo (por exemplo, durante uma tela de menu) quando um pico de CPU é menos perceptível. Rastreie este "pool" de objetos com uma coleção. Durante o jogo, simplesmente habilite a próxima instância disponível quando necessário, desative objetos em vez de destruí-los e devolva-os ao pool.

Uma visão ampliada da hierarquia SampleScene
O pool de objetos PlayerLaser está inativo e pronto para disparar.

Isso reduz o número de alocações gerenciadas em seu projeto e pode prevenir problemas de coleta de lixo.

Aprenda a criar um sistema simples de Pooling de Objetos no Unity aqui.

Use 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 e que você só precisa configurar uma vez. Ele 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.

Fluxograma mostrando um ScriptableObject chamado Inventário contendo configurações para vários GameObjects
ScriptableObject chamado Inventário contém configurações para vários GameObjects

Usar esses 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 Introdução aos ScriptableObjects tutorial para ver como os ScriptableObjects podem ajudar seu projeto. Você também pode encontrar a documentação relevante aqui.

Baixe a lista completa de dicas de desempenho para dispositivos móveis

No próximo post do blog, vamos dar uma olhada mais de perto na otimização gráfica e de GPU. No entanto, se você quiser acessar a lista completa de dicas e truques da equipe agora, nosso e-book completo está disponível aqui.

Capa do Ebook, "Otimize o Desempenho do Seu Jogo para Dispositivos Móveis"

Baixe nosso e-book

Se você está interessado em aprender mais sobre os serviços de Suporte Integrado e deseja dar à sua equipe acesso direto a engenheiros, conselhos de especialistas e orientações sobre melhores práticas para seus projetos, então confira os planos de sucesso da Unity aqui.

Fique ligado para mais dicas de desempenho

Queremos ajudar você a tornar suas aplicações Unity o mais eficientes possível, então, se houver algum tópico de otimização que você gostaria de saber mais, por favor, nos avise nos comentários.