Melhorar o desempenho do seu jogo

Jogadores preferem jogos com menos tempo de carregamento, frame rates consistentes e capacidade de resposta confiável.

Se você já tem experiência com desenvolvimento de jogos para computador ou console, poderá se surpreender com as diferenças entre esses dispositivos e os dispositivos móveis com relação ao tamanho da GPU e ao desempenho da memória flash. Essa divergência entre as estruturas de sistema pode dificultar a previsão do desempenho do seu app no Android.

Este guia auxiliará você a otimizar seu jogo, para que o desempenho seja o mais confiável possível em todos os tipos de dispositivos Android compatíveis. Ele explica especialmente como configurar o rastreamento do sistema de um jogo para Android. O guia também descreve como usar o relatório de resultados de um rastro do sistema para analisar aspectos específicos do desempenho do jogo.

Configurar um rastro de sistema baseado em jogo

A ferramenta Systrace, disponível como programa da linha de comando e como serviço no dispositivo, captura um perfil da CPU e da linha de execução do app durante um breve período. Você pode usar os resultados apresentados em um relatório do Systrace para entender melhor o desempenho do seu app no Android e para identificar como aprimorar a eficiência e a capacidade de resposta do jogo.

O Systrace é uma ferramenta de nível muito baixo, o que traz os seguintes benefícios:

  • Fornece informações precisas. O Systrace captura resultados diretamente do kernel. Assim, as métricas capturadas são praticamente idênticas às que seriam relatadas por diversas chamadas do sistema.
  • Consome menos recursos. O Systrace introduz uma sobrecarga muito baixa no dispositivo. Geralmente, essa sobrecarga é inferior a 1%, porque a ferramenta transfere dados para um buffer na memória.

Configurações ideais

Independentemente de como você captura o rastro do sistema, é importante que a ferramenta tenha um conjunto razoável de argumentos:

  • Categorias: o melhor conjunto de categorias para ativar um rastro do sistema baseado em jogo é: {sched, freq, idle, am, wm, gfx, view, sync, binder_driver, hal, dalvik}.
  • Tamanho do buffer: como regra geral, um buffer de 10 MB por núcleo da CPU permite um rastro de cerca de 20 segundos. Por exemplo, se um dispositivo tem duas CPUs quad-core (oito núcleos no total), o valor apropriado a ser passado para o programa systrace é 80.000 KB (80 MB).

    No entanto, se seu jogo realiza muitas trocas de contexto, aumente o buffer para 15 MB por núcleo da CPU.

  • Eventos personalizados: se você definir eventos personalizados para realizar capturas no jogo, ative a sinalização -a, que permite que o Systrace inclua esses eventos personalizados no relatório de resultados.

Se estiver utilizando o programa da linha de comando systrace, use o comando a seguir para capturar um rastro do sistema que aplique as práticas recomendadas para conjunto de categorias, tamanho do buffer e eventos personalizados:

    python systrace.py -a com.example.myapp -b 80000 -o my_systrace_report.html \
      sched freq idle am wm gfx view sync binder_driver hal dalvik
    

Se você estiver usando o app do sistema Systrace em um dispositivo, siga as etapas a seguir para capturar um rastro do sistema que aplique as práticas recomendadas para conjunto de categorias, tamanho do buffer e eventos personalizados:

  1. Ative a opção Rastrear aplicativos depuráveis.
  2. Em Tamanho do buffer, selecione 65536 (cerca de 64 MB). Dependendo de a CPU ter quatro ou oito núcleos, o dispositivo precisa ter 256 MB ou 512 MB disponíveis, respectivamente, para usar essa configuração. Além disso, cada parte de 64 MB da memória precisa estar disponível como um bloco contíguo.
  3. Selecione Categorias e ative as categorias da seguinte lista:

    • am: gerenciador de atividades
    • binder_driver: driver do binder do kernel
    • dalvik: VM Dalvik
    • freq: frequência da CPU
    • gfx: gráficos
    • hal: módulos de hardware
    • idle: CPU inativa
    • sched: programação da CPU
    • sync: sincronização
    • view: sistema de visualização
    • wm: gerenciador do Windows
  4. Ative a opção Registrar rastros.

  5. Carregue o jogo.

  6. Realize interações no jogo que correspondam à jogabilidade dos dispositivos cuja performance você quer avaliar.

  7. Logo depois de encontrar comportamentos inadequados no jogo, desative o rastreamento do sistema. Dessa forma, as estatísticas de desempenho necessárias para analisar melhor o problema terão sido capturadas.

Para economizar espaço em disco, os rastros do sistema no dispositivo salvam os arquivos em um formato de rastro compactado (*.ctrace). Para descompactar o arquivo ao gerar um relatório, use o programa da linha de comando e inclua a opção --from-file:

    python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \
      -o my_systrace_report.html
    

Aprimorar áreas de desempenho específicas

Esta seção destaca diversas questões de desempenho comuns em jogos para dispositivos móveis e descreve como identificar e aprimorar esses aspectos do jogo.

Velocidade de carregamento

Os jogadores querem entrar em ação no seu jogo o mais rápido possível. Por isso, é importante melhorar ao máximo os tempos de carregamento. As medidas a seguir geralmente diminuem os tempos de carregamento:

  • Execute o carregamento lento. Se você usa os mesmos recursos em cenas ou fases consecutivas do seu jogo, carregue-os apenas uma vez.
  • Reduza o tamanho dos recursos. Assim, é possível agrupar versões não compactadas desses recursos com o APK do jogo.
  • Use um método de compactação eficiente em disco. Um exemplo desse tipo de método é zlib.
  • Use IL2CPP em vez de mono. Essa opção só é válida se você está usando o Unity. O IL2CPP proporciona uma execução melhor para seus scripts C#.
  • Use várias linhas de execução no jogo. Para ver mais detalhes, consulte a seção consistência de frame rate.

Consistência de frame rate

Um dos elementos mais importantes da experiência de jogabilidade é atingir uma frame rate consistente. Siga as técnicas de otimização discutidas nesta seção para alcançar essa meta com mais facilidade.

Usar várias linhas de execução

Ao desenvolver para várias plataformas, é natural colocar toda a atividade do seu jogo em uma única linha de execução. Embora seja fácil implementar esse método de execução em vários mecanismos de jogo, ele está longe de ser o ideal para dispositivos Android. Consequentemente, jogos com uma única linha de execução costumam apresentar carregamento lento e falta de consistência de frame rate.

O Systrace apresentado na Figura 1 exibe o comportamento típico de um jogo executado em uma única CPU por vez:

Imagem de linhas de execução em um rastro do sistema

Figura 1. Relatório do Systrace para um jogo em uma única linha de execução.

Para melhorar o desempenho do jogo, use várias linhas de execução. Normalmente, o melhor modelo inclui duas linhas de execução:

  • Uma linha de execução de jogo, que contém os principais módulos do jogo e envia comandos de renderização.
  • Uma linha de comando de renderização, que recebe comandos de renderização e os traduz em comandos gráficos, usados pela GPU do dispositivo para exibir um cenário.

A API do Vulkan é expandida nesse modelo, devido à capacidade que ela tem de impulsionar dois buffers comuns ao mesmo tempo. Usando esse recurso, você pode distribuir diversas linhas de execução de renderização em várias CPUs, melhorando ainda mais o tempo de renderização de um cenário.

Você também pode fazer algumas alterações específicas do mecanismo para melhorar o desempenho em diversas linhas de execução do seu jogo.

  • Se você está desenvolvendo seu jogo usando o mecanismo do Unity, ative as opções Multithreaded Rendering e GPU Skinning.
  • Se você estiver usando um mecanismo de renderização personalizada, verifique se os pipelines de comando de renderização e de comando de gráficos estão alinhados corretamente. Caso contrário, você corre o risco de introduzir atrasos na exibição dos cenários do jogo.

Depois de implementar essas mudanças, o jogo ocupará pelo menos duas CPUs simultaneamente, conforme mostrado na Figura 2:

Imagem de linhas de execução em um rastro do sistema

Figura 2. Relatório do Systrace para um jogo com várias linhas de execução.

Carregamento de elementos da IU

Imagem de uma pilha de frames em um rastro do sistema
Figura 3. Relatório do Systrace para um jogo que está renderizando dezenas de elementos da IU ao mesmo tempo.

Ao criar um jogo com muitos recursos, é tentador mostrar várias opções e ações diferentes ao mesmo tempo para o jogador. No entanto, para manter uma frame rate consistente, é importante considerar o tamanho relativamente pequeno das telas de dispositivos móveis e manter a IU a mais simples possível.

O relatório do Systrace mostrado na Figura 3 é um exemplo de frame de IU que tenta renderizar elementos demais para a capacidade de um dispositivo móvel.

Tenha como meta reduzir o tempo de atualização da IU para 2 a 3 milésimos de segundo. Para conseguir atualizações nessa velocidade, implemente otimizações semelhantes às seguintes:

  • Atualize apenas os elementos que se moveram na tela.
  • Limite o número de texturas e camadas de IU. Você pode combinar chamadas gráficas que usam o mesmo material, como sombreadores e texturas.
  • Adie operações de animação de elementos para a GPU.
  • Use algoritmos de visibilidade "frustum culling" e "occlusion culling" mais agressivos.
  • Se possível, realize operações de desenho usando a API do Vulkan. A sobrecarga de chamadas de desenho é mais baixa no Vulkan.

Consumo de energia

Mesmo depois de realizar as otimizações discutidas na seção anterior, você pode achar que a frame rate do seu jogo perde desempenho dentro dos primeiros 45-50 minutos de jogo. Além disso, o dispositivo pode começar a aquecer e consumir mais bateria com o tempo.

Em muitos casos, essa alteração indesejada de temperatura e consumo de bateria está relacionada à forma como a carga de trabalho do jogo é distribuída entre as CPUs do dispositivo. Para aumentar a eficiência do jogo com relação ao consumo de bateria, aplique as práticas recomendadas mostradas nas seções a seguir.

Manter as linhas de execução com uso intenso de memória em uma só CPU

Em muitos dispositivos móveis, os caches L1 residem em CPUs específicas, e os caches L2 residem em um conjunto de CPUs que compartilham um clock. Em geral, para aumentar a ocorrência em cache L1, é melhor manter a linha de execução principal do seu jogo, juntamente com outras linhas de execução com uso intenso de memória, em uma única CPU.

Adiar trabalhos de curta duração para CPUs com menor consumo de energia

A maior parte dos mecanismos de jogo, inclusive o Unity, sabe adiar operações de linha de execução worker para outra CPU que não a da linha de execução principal do jogo. No entanto, o mecanismo não reconhece a arquitetura específica do dispositivo e não pode prever a carga de trabalho do jogo tão bem quanto você.

A maior parte dos dispositivos system on a chip tem pelo menos dois clocks compartilhados: um para as CPUs rápidas e outro para as CPUs lentas do dispositivo. Uma das consequências dessa arquitetura é que, se uma CPU precisa operar em velocidade máxima, todas as outras CPUs rápidas também operam na mesma velocidade.

O relatório de exemplo da Figura 4 mostra um jogo que usa CPUs rápidas. No entanto, esse alto nível de atividade gera rapidamente um alto consumo de bateria e aumento de temperatura.

Imagem de linhas de execução em um rastro do sistema

Figura 4. Relatório do Systrace mostrando uma atribuição não ideal de linhas de execução para as CPUs do dispositivo.

Para reduzir o consumo de bateria de maneira geral, é recomendado sugerir ao programador que trabalhos de duração mais curta (como carregamento de áudio, execução de linhas de execução worker e execução do coreógrafo) sejam adiados para o conjunto de CPUs lentas do dispositivo. Transfira a maior quantidade possível desse tipo de trabalho para as CPUs lentas, mantendo, ao mesmo tempo, uma frame rate adequada.

A maioria dos dispositivos lista as CPUs lentas antes das CPUs rápidas, mas não presuma que o SoC do dispositivo esteja usando essa ordem. Para verificar, execute comandos semelhantes àqueles mostrados neste código de descoberta de topologia da CPU (em inglês) no GitHub.

Quando você souber quais são as CPUs lentas no dispositivo, poderá declarar afinidades para suas linhas de execução de curta duração, que serão seguidas pelo programador do dispositivo. Para fazer isso, adicione o seguinte código em cada linha de execução:

    &num;include <sched.h>
    &num;include <sys/types.h>
    &num;include <unistd.h>

    pid_t my_pid; // PID of the process containing your thread.

    // Assumes that cpu0, cpu1, cpu2, and cpu3 are the "slow CPUs".
    cpu_set_t my_cpu_set;
    CPU_ZERO(&my_cpu_set);
    CPU_SET(0, &my_cpu_set);
    CPU_SET(1, &my_cpu_set);
    CPU_SET(2, &my_cpu_set);
    CPU_SET(3, &my_cpu_set);
    sched_setaffinity(my_pid, sizeof(cpu_set_t), &my_cpu_set);
    

Latência de toque para exibir

Jogos que renderizam frames com a maior velocidade possível criam um cenário vinculado à GPU, em que o buffer do frame fica sobrecarregado. A CPU precisa aguardar a GPU, o que causa um atraso perceptível entre a entrada do jogador e a entrada que acontece na tela.

Para saber se você pode melhorar o ritmo de frames do jogo, siga as etapas a seguir:

  1. Gere um relatório do Systrace que inclua as categorias gfx e input. Essas categorias abrangem medidas especialmente úteis para determinar a latência de toque para exibir.
  2. Verifique a seção SurfaceView do relatório do Systrace. Um buffer sobrecarregado faz com que os números de desenhos de buffer pendentes oscilem entre 1 e 2, conforme mostrado na Figura 5:

    Imagem da fila de buffers em um rastro do sistema

    Figura 5. Relatório do Systrace mostrando um buffer sobrecarregado que, periodicamente, fica cheio demais para aceitar comandos de desenho.

Para diminuir essa inconsistência no ritmo de frames, realize as ações descritas nas seções a seguir:

Integrar a API Android Frame Pacing ao jogo

A API Android Frame Pacing ajuda a realizar trocas de frame e definir um intervalo de troca para que seu jogo mantenha uma frame rate mais consistente.

Reduzir a resolução dos recursos do seu jogo que não são de IU

As telas dos dispositivos móveis modernos possuem muito mais pixels do que um player consegue processar. Assim, não há problema em reduzir a resolução de modo que a execução de cinco ou até 10 pixels contenha somente uma cor. Considerando a estrutura da maioria dos caches de telas, é recomendável reduzir a resolução em apenas uma dimensão.

No entanto, não reduza a resolução dos elementos de IU do jogo. É importante preservar a espessura da linha nesses elementos para manter uma área de toque grande o suficiente para todos os jogadores.

Estabilidade de renderização

Quando o SurfaceFlinger é fixado em um buffer de exibição para mostrar uma cena no jogo, a atividade de CPU aumenta temporariamente. Se esses picos na atividade da CPU ocorrerem de forma irregular, será possível notar oscilações no jogo. A imagem da Figura 6 representa a causa desse problema:

Imagem de frames que perderam uma janela do Vsync porque começaram a ser desenhados tarde demais

Figura 6. Relatório do Systrace mostrando como um frame pode perder um Vsync.

Se um frame começar a ser desenhado tarde demais, ainda que apenas por alguns milésimos de segundo, ele poderá perder a janela de exibição seguinte. Nesse caso, o frame precisa esperar pelo próximo Vsync para ser exibido (33 milésimos de segundo para um jogo a 30 FPS), o que causa um atraso perceptível para o jogador.

Para solucionar esse problema, use a API Android Frame Pacing, que sempre apresenta um novo frame em um wavefront VSync.

Estado da memória

Quando o jogo é executado por um longo período, o dispositivo pode apresentar erros de falta de memória.

Nesse caso, verifique a atividade de CPU em um relatório do Systrace e descubra com que frequência o sistema está chamando o daemon kswapd. Se houver muitas chamadas durante a execução do seu jogo, analise mais detalhadamente o gerenciamento e a limpeza da memória.

Para saber mais, consulte Gerenciar a memória em jogos.

Estado da linha de execução

Ao navegar pelos elementos típicos de um relatório do Systrace, é possível ver quanto tempo uma determinada linha de execução passou em cada estado possível selecionando a linha de execução no relatório, conforme mostrado na Figura 7:

Imagem de um relatório do Systrace

Figura 7. Relatório do Systrace mostrando como o relatório exibe um resumo do estado da linha de execução quando ela é selecionada.

Como mostrado na Figura 7, você pode descobrir que as linhas de execução do jogo não estão no estado "running" ou "runnable" com a frequência que deveriam estar. A lista a seguir mostra diversos motivos comuns que podem fazer com que uma linha de execução passe para um estado incomum periodicamente:

  • Se uma linha de execução está suspensa por um longo período, ela pode estar sofrendo contenção de bloqueio ou aguardando uma atividade da GPU.
  • Se uma linha de execução é constantemente bloqueada na E/S, significa que você está lendo muitos dados do disco de uma só vez ou que seu jogo está falhando.

Outros recursos

Para saber mais sobre como melhorar o desempenho do seu jogo, consulte as fontes a seguir:

Vídeos

  • Apresentação Systrace para jogos (vídeo em inglês) da Conferência de Desenvolvedores de Jogos Android 2018.