Melhorar o desempenho do jogo

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

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

Este guia ajudará 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. Especificamente, será explicado como configurar o rastreamento do sistema de um jogo para Android. O guia também descreve como usar o relatório de saída 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 um programa da linha de comando e como um 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 de um relatório do Systrace para entender melhor o desempenho do seu jogo 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 saídas 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ê capturar o rastro do sistema, é importante que a ferramenta tenha um conjunto razoável de argumentos:

  • Categorias: o melhor conjunto de categorias para ativar em um rastro de sistema com base em um jogo é: {sched, freq, idle, am, wm, gfx, view, sync, binder_driver, hal, dalvik}.
  • Tamanho do buffer: como regra geral, o tamanho do 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 transmitido para o programa systrace é 80.000 KB (80 MB).

    No entanto, se o jogo realizar 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 saída.

Se você estiver utilizando o programa de linha de comando systrace, use o seguinte comando para capturar um rastro do sistema que segue as práticas recomendadas do conjunto de categorias, do tamanho do buffer e de 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, realize as etapas a seguir para capturar um rastro do sistema que segue as práticas recomendadas do conjunto de categorias, do tamanho do buffer e de eventos personalizados:

  1. Ative a opção Rastrear aplicativos depuráveis.
  2. Em Tamanho do buffer, selecione 65536 (cerca de 64 MB). Para usar essa configuração, o dispositivo precisa ter 256 ou 512 MB disponíveis, dependendo da CPU ter quatro ou oito núcleos, respectivamente. 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 de janelas
  4. Ative a opção Registrar rastros.

  5. Carregue o jogo.

  6. Realize as interações no jogo que correspondam à jogabilidade dos dispositivos em que você quer medir o desempenho.

  7. Depois de encontrar comportamentos indesejados no jogo, desative o rastreamento do sistema. Você terá capturado as estatísticas de desempenho necessárias para analisar melhor o problema.

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 várias questões de desempenho comuns de jogos para dispositivos móveis e descreve como identificar e melhorar esses aspectos do jogo.

Velocidade de carregamento

Os usuários querem começar a jogar o mais rápido possível. Por isso, é importante melhorar ao máximo os tempos de carregamento do jogo. 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 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 (link em inglês).
  • Use o IL2CPP (link em inglês) em vez do modo mono. Essa opção só é válida se você está usando o Unity. O IL2CPP apresenta um desempenho de execução melhor para 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 é um 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 não é o mais ideal para dispositivos Android. Consequentemente, jogos com uma única linha de execução costumam apresentar carregamento lento e falta de consistência do frame rate.

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

Diagrama das linhas de execução
em um rastro do sistema

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

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

  • Uma linha de execução do jogo, que contém os módulos principais 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 expande esse modelo, devido à capacidade de enviar dois buffers comuns em paralelo. 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 mudanças específicas do mecanismo para melhorar o desempenho do jogo em diversas linhas de execução.

  • 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 personalizado, verifique se os pipelines de comandos de renderização e de comandos 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 da implementação dessas mudanças, o jogo ocupará pelo menos duas CPUs simultaneamente, conforme mostrado na Figura 2:

Diagrama das 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

Diagrama 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 exibir várias opções e ações diferentes para o jogador ao mesmo tempo. No entanto, para manter um frame rate consistente, é importante considerar o tamanho relativamente pequeno das telas de dispositivos móveis e usar a IU mais simples possível.

O relatório do Systrace mostrado na Figura 3 é um exemplo de frame da 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 atualizar nessa velocidade, implemente otimizações como as 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 é menor no Vulkan.

Consumo de energia

Mesmo depois de realizar as otimizações discutidas na seção anterior, você pode perceber que o frame rate do jogo diminui nos primeiros 45 a 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 combinaçã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 de consumo da bateria do jogo, 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 ficam em CPUs específicas, e os caches L2 ficam 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 jogo, junto de 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, adia 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 operarão 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 apresenta um alto consumo de bateria e o aumento rápido da temperatura.

Diagrama das 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 geral da bateria, é recomendado sugerir ao programador que trabalhos de duração mais curta, como o carregamento de áudio, a execução de linhas de execução de worker e a execução do coreógrafo, sejam adiados para o conjunto de CPUs lentas do dispositivo. Transfira o máximo possível desse tipo de trabalho para as CPUs lentas para manter um frame rate adequado.

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 como os 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 as 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:

#include <sched.h>
#include <sys/types.h>
#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);

Estresse térmico

Quando os dispositivos ficam muito quentes, eles podem limitar a CPU e/ou a GPU, e isso pode afetar os jogos de maneiras inesperadas. Os jogos que incorporam gráficos complexos, computação intensa ou atividade de rede sustentada têm maior probabilidade de apresentar problemas.

Use a API Thermal para monitorar as mudanças de temperatura no dispositivo e tomar medidas para reduzir o uso de energia e manter a temperatura mais baixa no dispositivo. Quando o dispositivo relatar estresse térmico, desative as atividades contínuas para reduzir o consumo de energia. Por exemplo, reduza o frame rate ou a tesselação de polígonos.

Primeiro, declare o objeto PowerManager e inicialize-o no método onCreate(). Adicione um listener de status térmico ao objeto.

Kotlin

class MainActivity : AppCompatActivity() {
    lateinit var powerManager: PowerManager

    override fun onCreate(savedInstanceState: Bundle?) {
        powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
        powerManager.addThermalStatusListener(thermalListener)
    }
}

Java

public class MainActivity extends AppCompatActivity {
    PowerManager powerManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
        powerManager.addThermalStatusListener(thermalListener);
    }
}

Defina as ações que serão realizadas quando o listener detectar uma mudança de status. Se o jogo usar C/C++, adicione o código aos níveis de status térmico em onThermalStatusChanged() para chamar o código nativo do jogo usando o JNI ou use a API Thermal nativa.

Kotlin

val thermalListener = object : PowerManager.OnThermalStatusChangedListener() {
    override fun onThermalStatusChanged(status: Int) {
        when (status) {
            PowerManager.THERMAL_STATUS_NONE -> {
                // No thermal status, so no action necessary
            }

            PowerManager.THERMAL_STATUS_LIGHT -> {
                // Add code to handle light thermal increase
            }

            PowerManager.THERMAL_STATUS_MODERATE -> {
                // Add code to handle moderate thermal increase
            }

            PowerManager.THERMAL_STATUS_SEVERE -> {
                // Add code to handle severe thermal increase
            }

            PowerManager.THERMAL_STATUS_CRITICAL -> {
                // Add code to handle critical thermal increase
            }

            PowerManager.THERMAL_STATUS_EMERGENCY -> {
                // Add code to handle emergency thermal increase
            }

            PowerManager.THERMAL_STATUS_SHUTDOWN -> {
                // Add code to handle immediate shutdown
            }
        }
    }
}

Java

PowerManager.OnThermalStatusChangedListener thermalListener =
    new PowerManager.OnThermalStatusChangedListener () {

    @Override
    public void onThermalStatusChanged(int status) {

        switch (status)
        {
            case PowerManager.THERMAL_STATUS_NONE:
                // No thermal status, so no action necessary
                break;

            case PowerManager.THERMAL_STATUS_LIGHT:
                // Add code to handle light thermal increase
                break;

            case PowerManager.THERMAL_STATUS_MODERATE:
                // Add code to handle moderate thermal increase
                break;

            case PowerManager.THERMAL_STATUS_SEVERE:
                // Add code to handle severe thermal increase
                break;

            case PowerManager.THERMAL_STATUS_CRITICAL:
                // Add code to handle critical thermal increase
                break;

            case PowerManager.THERMAL_STATUS_EMERGENCY:
                // Add code to handle emergency thermal increase
                break;

            case PowerManager.THERMAL_STATUS_SHUTDOWN:
                // Add code to handle immediate shutdown
                break;
        }
    }
};

Latência entre o toque e a exibição

Jogos que renderizam frames com a maior velocidade possível criam um cenário limitado pela 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 o efeito que ela causa na tela.

Para saber se você pode melhorar o ritmo dos frames do jogo, siga estas etapas:

  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 entre o toque e a exibição.
  2. Verifique a seção SurfaceView do relatório do Systrace. Um buffer sobrecarregado faz com que os números de desenhos pendentes no buffer oscilem entre 1 e 2, conforme mostrado na Figura 5:

    Diagrama da
fila do buffer 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 o jogo mantenha um 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 o jogador consegue enxergar. Portanto, não há problema em reduzir a resolução de modo que um conjunto de 5 até 10 pixels contenha apenas 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 suficientemente grande 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 da CPU aumenta temporariamente. Se esses picos de atividade da CPU ocorrerem de forma irregular, será possível notar a renderização lenta do jogo. O diagrama na Figura 6 representa a causa desse problema:

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

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

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

Para solucionar esse problema, use a API Android Frame Pacing, que sempre exibe um novo frame em um wavefront de 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 da CPU em um relatório do Systrace e observe com que frequência o sistema está chamando o daemon kswapd. Se houver muitas chamadas durante a execução do 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, você poderá ver quanto tempo 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:

Diagrama de um
relatório do Systrace

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

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

  • Caso uma linha de execução esteja 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 é bloqueada constantemente na E/S, isso significa que você está lendo muitos dados do disco de uma vez só ou que seu jogo está com uma sobrecarga.

Outros recursos

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

Vídeos

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