O que você está procurando?
Games

Otimize o desempenho de seus jogos para celular: Dicas sobre criação de perfil, memória e arquitetura de código dos principais engenheiros da Unity

THOMAS KROGH-JACOBSEN / UNITY TECHNOLOGIESProduct Marketing Core Tech
Jun 23, 2021|15 Min
Otimize o desempenho de seus jogos para celular: Dicas sobre criação de perfil, 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 da Accelerate Solutions conhece o código-fonte por dentro e por fora e oferece suporte a uma infinidade de clientes da Unity para que eles possam aproveitar ao máximo a engine. Em seu trabalho, eles se aprofundam em projetos de criadores para ajudar a identificar pontos em que o desempenho pode ser otimizado para aumentar a velocidade, a estabilidade e a eficiência. Nós nos reunimos com essa equipe, composta pelos engenheiros de software mais experientes da Unity, e pedimos que compartilhassem alguns de seus conhecimentos sobre otimização de jogos para dispositivos móveis.

Quando nossos engenheiros começaram a compartilhar seus insights sobre a otimização de jogos para dispositivos móveis, percebemos rapidamente que havia informações excelentes demais para a única publicação de blog que havíamos planejado. Em vez disso, decidimos transformar a montanha de conhecimento deles em um e-book completo (que pode ser baixado aqui), bem como em uma série de publicações no blog que destacam algumas dessas mais de 75 dicas práticas.

Começamos a primeira postagem desta série mostrando como você pode melhorar o desempenho do seu jogo com a criação de perfil, a memória e a arquitetura de código. Nas próximas semanas, faremos mais duas postagens: a primeira abordando a física da interface do usuário, seguida por outra sobre áudio e ativos, configuração do projeto e gráficos.

Quer conferir a série completa agora? Faça o download do e-book completo gratuitamente.

Vamos nos aprofundar!

Criação de perfil

Qual é o melhor lugar para começar do que a criação de perfis e o processo de coleta e ação sobre os dados de desempenho móvel? É aqui que a otimização do desempenho móvel realmente começa.

Crie perfis cedo, com frequência e no dispositivo de destino

O Unity Profiler fornece informações essenciais de desempenho sobre seu aplicativo, mas não poderá ajudá-lo se você não o usar. Trace o perfil do seu projeto no início do desenvolvimento, não apenas quando estiver próximo do envio. Investigue falhas ou picos assim que eles aparecerem. Ao desenvolver uma "assinatura de desempenho" para o seu projeto, você poderá identificar novos problemas com mais facilidade.

Embora a criação de perfis no Editor possa lhe dar uma ideia do desempenho relativo de diferentes sistemas em seu jogo, a criação de perfis em cada dispositivo lhe dá a oportunidade de obter insights mais precisos. Crie o perfil de uma compilação de desenvolvimento nos dispositivos de destino sempre que possível. Lembre-se de criar um perfil e otimizar para os dispositivos de especificações mais altas e mais baixas aos quais você planeja dar suporte.

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

Determinado hardware pode se beneficiar de ferramentas adicionais de criação de perfil (por exemplo, Arm Mobile Studio, Intel VTune e Snapdragon Profiler). Consulte Criação de perfil de aplicativos feitos com Unity para obter mais informações.

Concentre-se em otimizar as áreas certas

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

Obviamente, nem todas as otimizações descritas aqui se aplicarão ao seu aplicativo. Algo que funciona bem em um projeto pode não se aplicar ao seu. Identifique os verdadeiros gargalos e concentre seus esforços naquilo que beneficia seu trabalho.

Entenda como o profiler da Unity funciona

O Unity Profiler pode ajudá-lo a detectar as causas de atrasos ou congelamentos no tempo de execução e a entender melhor o que está acontecendo em um quadro ou ponto específico no tempo. Ative os rastros de CPU e memória por padrão. Você pode monitorar módulos suplementares do Profiler, como Renderer, Audio e Physics, conforme necessário para o seu jogo (por exemplo, jogabilidade com muita física ou música).

Use o Unity Profiler para testar o desempenho e a alocação de recursos de seu aplicativo.
Use o Unity Profiler para testar o desempenho e a alocação de recursos de seu aplicativo.

Crie o aplicativo para seu dispositivo marcando Development Build e Autoconnect Profiler ou conecte-se manualmente para acelerar o tempo de inicialização do aplicativo.

Configurações de criação no editor

Escolha o alvo da plataforma para o perfil. O botão Recorder rastreia vários segundos da reprodução do seu aplicativo (300 quadros por padrão). Vá para Unity > Preferências > Análise > Profiler > Contagem de quadros para aumentar esse número até 2000 se precisar de capturas mais longas. Embora isso signifique que o Unity Editor tenha que trabalhar mais com a CPU e ocupar mais memória, pode ser útil dependendo de seu cenário específico.

Esse é um profiler baseado em instrumentação que traça o perfil dos tempos de código explicitamente envolvidos em ProfileMarkers (como os métodos Start ou Update do MonoBehaviour ou chamadas específicas de API). Além disso, ao usar a configuração Deep Profilingsetting, a Unity pode traçar o perfil do início e do fim de cada chamada de função no código do seu script para informar exatamente qual parte do seu aplicativo está causando a lentidão.

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

Ao traçar o perfil de seu jogo, recomendamos que você cubra os picos e o custo de um quadro médio em seu jogo. Compreender e otimizar as operações caras que ocorrem em cada quadro pode ser mais útil para aplicativos executados abaixo da taxa de quadros desejada. Ao procurar picos, explore primeiro as operações caras (por exemplo, física, IA, animação) e a coleta de lixo.

Clique na janela para analisar um quadro específico. Em seguida, use a visualização Timeline ou Hierarchy para o seguinte:

  • Timeline mostra o detalhamento visual do tempo para um quadro específico. Isso permite que você visualize como as atividades se relacionam umas com as outras e entre diferentes tópicos. Use essa opção para determinar se você está vinculado à CPU ou à GPU.
  • Hierarchy (Hierarquia ) mostra a hierarquia de ProfileMarkers, agrupados. Isso permite que você classifique as amostras com base no custo do tempo em milissegundos(Time ms e Self ms). Você também pode contar o número de chamadas para uma função e a memória heap gerenciada(GC Alloc) no quadro.
Classificação de ProfileMarkers por custo de tempo
A visualização Hierarchy permite que você classifique os ProfileMarkers por custo de tempo.

Leia uma visão geral completa do Unity Profiler aqui. Os iniciantes em criação de perfis também podem assistir a esta Introdução à criação de perfis no Unity.

Antes de otimizar qualquer coisa em seu projeto, salve o arquivo de dados do Profiler. Implemente suas alterações e compare os dados .salvos antes e depois da modificação. Conte com esse ciclo para aprimorar o desempenho: perfil, otimização e comparação. Em seguida, enxágue e repita.

Use o Profile Analyzer

Essa ferramenta permite que você agregue vários quadros de dados do Profiler e, em seguida, localize quadros de interesse. Deseja ver o que acontece com o Profiler depois que você faz uma alteração em seu projeto? A visualização Compare permite carregar e diferenciar dois conjuntos de dados, para que você possa testar as alterações e melhorar o resultado delas. O Profile Analyzer está disponível no Package Manager da Unity.

Análise mais detalhada do Profile Analyzer no editor
Mergulhe ainda mais fundo nos dados de quadros e marcadores com o Profile Analyzer, que complementa o Profiler existente.

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

Cada quadro terá um orçamento de tempo com base em sua meta de quadros por segundo (FPS). O ideal é que um aplicativo executado a 30 fps permita aproximadamente 33,33 ms por quadro (1000 ms / 30 fps). Da mesma forma, uma meta 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 um período prolongado.

Leve em conta 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 sistema operacional pode acelerar termicamente a CPU e a GPU. Recomendamos que você use apenas cerca de 65% do tempo disponível para permitir o tempo de recarga entre os quadros. Um orçamento de quadro típico 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 tem resfriamento ativo como seus equivalentes de desktop. Os níveis de calor físico podem afetar diretamente o desempenho.

Se o dispositivo estiver aquecido, o Profiler poderá perceber e relatar um desempenho ruim, mesmo que isso não seja motivo de preocupação a longo prazo. Para combater o superaquecimento da criação de perfil, crie perfis em Bursts curtos. Isso resfria o dispositivo e simula as condições do mundo real. Nossa recomendação geral é manter o dispositivo resfriado por 10 a 15 minutos antes de criar um novo perfil.

Determinar se você está vinculado à GPU ou à CPU

O Profiler pode informar se a CPU está demorando mais do que o orçamento de quadros alocado ou se a culpada é a GPU. Ele faz isso emitindo marcadores prefixados com Gfx da seguinte forma:

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

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

O coletor de lixo identifica e desaloca periodicamente a memória heap não utilizada. Embora isso seja executado automaticamente, o processo de examinar todos os objetos no heap pode fazer com que o jogo gagueje ou seja executado lentamente.

Otimizar o uso da memória significa ter consciência de quando alocar e desalocar a memória heap e como minimizar o efeito da coleta de lixo. Consulte Entendendo o heap gerenciado para obter mais informações.

Uma olhada no editor do Memory Profiler
Capture, inspecione e compare instantâneos no Memory Profiler.

Use o Memory Profiler

Esse complemento separado (disponível como um pacote Experimental ou Preview no Package Manager) pode tirar um instantâneo da memória heap gerenciada, para ajudá-lo a identificar problemas como fragmentação e vazamentos de memória.

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

Saiba como aproveitar o Memory Profiler no Unity para melhorar o uso da memória. Você também pode consultar nossa documentação oficial do Memory Profiler.

Reduzir o impacto da coleta de lixo (GC)

A Unity usa o coletor de lixo Boehm-Demers-Weiser, que interrompe a execução do código do programa e só retoma a execução normal quando o trabalho é concluído.

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

  • Cordas: No C#, as cadeias de caracteres são tipos de referência, não tipos de valor. Reduzir a criação ou manipulação desnecessária de strings. Evite analisar arquivos de dados baseados em strings, como JSON e XML; em vez disso, armazene os dados em ScriptableObjects ou em formatos como MessagePack ou Protobuf. Use a classe StringBuilder se precisar criar cadeias de caracteres em tempo de execução.
  • Chamadas de função Unity: Algumas funções criam alocações de heap. Armazene em cache referências a matrizes em vez de alocá-las no meio de um loop. Além disso, tire proveito de determinadas funções que evitam a geração de lixo. Por exemplo, use GameObject.CompareTag em vez de comparar manualmente uma cadeia de caracteres com GameObject.tag (pois o retorno de uma nova cadeia de caracteres cria lixo).
  • Boxe: Evite passar uma variável tipada como valor no lugar de uma variável tipada como referência. Isso cria um objeto temporário, e o lixo potencial que vem com ele converte implicitamente o tipo de valor em um objeto de tipo (por exemplo, int i = 123; object o = i). Em vez disso, tente fornecer substituições concretas com o tipo de valor que você deseja passar. Os genéricos também podem ser usados para essas substituições.
  • Rotinas: Embora o yield não produza lixo, a criação de um novo objeto WaitForSeconds produz. Armazene em cache e reutilize o objeto WaitForSeconds em vez de criá-lo na linha de yield.
  • LINQ e expressões regulares: Ambos geram lixo nos bastidores do boxe. Evite o LINQ e as expressões regulares se o desempenho for um problema. Escreva loops for e use listas como alternativa à criação de novas matrizes.

Coleta de lixo em tempo hábil, se possível

Se tiver certeza de que o congelamento da coleta de lixo não afetará um ponto específico do jogo, você poderá acionar a coleta de lixo com System.GC.Collect.

Consulte Entendendo o gerenciamento automático de memória para obter 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 e longa interrupção durante a execução do programa, a coleta de lixo incremental usa várias interrupções muito mais curtas que distribuem a carga de trabalho em vários quadros. Se a coleta de lixo estiver afetando o desempenho, tente ativar essa opção para ver se ela pode reduzir o problema dos picos de GC. Use o Profile Analyzer para verificar os benefícios para seu aplicativo.

Uma olhada no Incremental Garbage Collector (coletor de lixo incremental)
Use o Incremental Garbage Collector para reduzir os picos de GC.
Programação e arquitetura de código

O Unity PlayerLoop contém funções para interagir com o núcleo do mecanismo de jogo. Essa estrutura inclui vários sistemas que lidam com a inicialização e as atualizações por quadro. Todos os seus scripts dependerão desse PlayerLoop para criar a jogabilidade.

Ao criar um perfil, você verá o código de usuário do seu projeto no PlayerLoop (com os componentes do Editor no EditorLoop).

Visão ampliada de um perfilador
O Profiler mostrará seus scripts, configurações e gráficos personalizados no contexto de toda a execução do mecanismo.
Uma visualização do PlayerLoop

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

Você pode otimizar seus scripts com as dicas e os truques a seguir.

Entenda o PlayerLoop do Unity

Certifique-se de que você entendeu a ordem de execução do loop de quadros da Unity. Cada script 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 saber a ordem específica de execução das funções de evento.

Minimizar o código que é executado a cada quadro

Considere se o código deve ser executado a cada quadro. Remova a lógica desnecessária de Update, LateUpdate e FixedUpdate. Essas funções de evento são locais convenientes para colocar o código que deve ser atualizado a cada quadro e, ao mesmo tempo, extrair qualquer lógica que não precise ser atualizada com essa frequência. Sempre que possível, execute a lógica somente quando as coisas mudarem.

Se você precisar usar o Update, considere a possibilidade de executar o código a cada n quadros. Essa é uma maneira de aplicar o fatiamento de tempo, uma técnica comum de distribuição de uma carga de trabalho pesada em vários quadros. Neste exemplo, executamos a ExampleExpensiveFunction uma vez a cada três quadros:

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Evite lógica pesada em Start/Awake

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

  • Despertar
  • OnEnable
  • Início

Evite lógica cara nessas funções até que o aplicativo renderize o primeiro quadro. Caso contrário, você poderá enfrentar tempos de carregamento mais longos do que o necessário.

Consulte a ordem de execução das funções de evento para obter detalhes sobre o primeiro carregamento de cena.

Evite eventos Unity vazios

Mesmo MonoBehaviour vazios requerem recursos, portanto, você deve remover os métodos Update ou LateUpdate em branco.

Use as diretivas do pré-processador se estiver empregando esses métodos para testes:

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Aqui, você pode usar livremente o Update in-Editor para testes, sem que haja sobrecarga desnecessária em sua compilação.

Remover instruções do registro de depuração

As instruções de registro (especialmente em Update, LateUpdate ou FixedUpdate) podem prejudicar o desempenho. Desative suas instruções de registro antes de fazer uma compilação.

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

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Uma visão de ENABLE_LOG
A adição de uma diretiva de pré-processador personalizada permite particionar seus scripts.

Gere sua mensagem de registro com sua classe personalizada. Se você desativar o pré-processador ENABLE_LOG nas configurações do jogador, todas as suas declarações de registro desaparecerão de uma só vez.

Use valores de hash em vez de parâmetros de cadeia de caracteres

A Unity não usa nomes de string para endereçar internamente as propriedades Animator, Material e Shader. Para acelerar, todos os nomes de propriedades são transformados em hash em IDs de propriedades, e esses IDs são de fato usados para endereçar as propriedades.

Ao usar um método Set ou Get em um Animator, Material ou Shader, use o método de valor inteiro em vez dos métodos de valor de cadeia de caracteres. Os métodos de cadeia de caracteres simplesmente executam o hash de cadeia de caracteres e, em seguida, encaminham o ID com hash para os métodos de valor inteiro.

Use Animator.StringToHash para nomes de propriedades do Animator e Shader.PropertyToID para nomes de propriedades do Material e do Shader.

Escolha a estrutura de dados correta

Sua escolha de estrutura de dados afeta a eficiência à medida que você itera milhares de vezes por quadro. Não tem certeza se deve usar uma lista, uma matriz ou um dicionário para sua coleção? Siga o guia MSDN para estruturas de dados em C# como um guia geral para escolher a estrutura correta.

Evite adicionar componentes em tempo de execução

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

A instanciação de um Prefab com os componentes desejados já configurados é geralmente mais eficiente.

Cache de GameObjects e componentes

GameObjects.Find, GameObject.GetComponent e Camera.main (em versões anteriores à 2020.2) podem ser caros, portanto, é melhor evitar chamá-los nos métodos Update. Em vez disso, chame-os no Start e armazene os resultados em cache.

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

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Em vez disso, invoque GetComponent apenas uma vez, pois o resultado da função é armazenado em cache. O resultado armazenado em cache pode ser reutilizado no Update sem nenhuma outra chamada para GetComponent.

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.

Usar pools de objetos

Instanciar e destruir pode gerar picos de lixo e de coleta de lixo (GC), além de ser um processo geralmente lento. Em vez de instanciar e destruir GameObjects regularmente (por exemplo, disparar balas de uma arma), use pools de objetos pré-alocados que possam ser reutilizados e reciclados.

Uma visão ampliada do ObjectPool
Neste exemplo, o ObjectPool cria 20 instâncias do 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 for menos perceptível. Rastreie esse "pool" de objetos com uma coleção. Durante o jogo, basta ativar a próxima instância disponível quando necessário, desativar objetos em vez de destruí-los e devolvê-los ao pool.

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

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

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

Usar ScriptableObjects

Armazene valores ou configurações imutáveis em um ScriptableObject em vez de um MonoBehaviour. O ScriptableObject é um ativo que reside dentro do projeto e que você só precisa configurar uma vez. Ele não pode ser anexado diretamente a um GameObjects.

Crie campos no ScriptableObject para armazenar seus valores ou configurações e, em seguida, faça referência ao ScriptableObject em seus MonoBehaviours.

Fluxograma mostrando um ScriptableObject chamado Inventory que contém configurações para vários GameObjects
O ScriptableObject chamado Inventory mantém as configurações de vários GameObjects

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

Faça o download da lista completa de dicas de desempenho móvel

Na próxima postagem do blog, daremos uma olhada mais de perto nos gráficos e na otimização da GPU. No entanto, se 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 de seus jogos para celular"

Baixe nosso e-book

Se estiver interessado em saber mais sobre os serviços de suporte integrado e quiser dar à sua equipe acesso direto a engenheiros, consultoria especializada e orientação de práticas recomendadas para seus projetos, confira os planos de sucesso da Unity aqui.

Fique atento a mais dicas de desempenho

Queremos ajudá-lo a tornar seus aplicativos Unity tão eficientes quanto possível, portanto, se houver algum tópico de otimização sobre o qual você gostaria de saber mais, mantenha-nos informados nos comentários.