Como integrar recursos de adaptabilidade ao jogo nativo

1. Introdução

70748189a60ed450.png

Por que preciso integrar recursos de adaptabilidade ao meu jogo?

As APIs de adaptabilidade permitem receber feedback sobre o estado do dispositivo no momento da execução do app, assim como ajustar dinamicamente a carga de trabalho para otimizar o desempenho do jogo. Elas também permitem informar ao sistema sobre a carga de trabalho para que ele possa alocar recursos de maneira otimizada.

O que você vai criar

Neste codelab, você vai criar e executar um jogo de exemplo nativo do Android e integrar recursos de adaptabilidade a ele. Depois de configurar e adicionar o código necessário, você poderá testar a diferença no desempenho percebido entre o jogo anterior e a versão com adaptabilidade.

O que você vai aprender

  • Como integrar a API Thermal e adaptar um jogo ao status térmico para evitar superaquecimento.
  • Como integrar a API Game Mode e reagir às mudanças de seleção.
  • Como integrar a API Game State para permitir que o sistema saiba em que estado o jogo está sendo executado.
  • Como integrar a API Performance Hint para informar ao sistema o modelo da linha de execução e a carga de trabalho.

O que será necessário

2. Etapas da configuração

Como configurar o ambiente de desenvolvimento

Se você ainda não trabalhou com projetos nativos no Android Studio, talvez seja necessário instalar o Android NDK e o CMake. Se eles já estiverem instalados, siga para "Como configurar o projeto".

Como verificar se o SDK, o NDK e o CMake estão instalados

Inicie o Android Studio. Quando a janela de boas-vindas for mostrada, abra o menu suspenso "Configure" e selecione a opção "SDK Manager".

3b7b47a139bc456.png

Caso você já tenha um projeto aberto, é possível acessar o SDK Manager pelo menu "Tools". Clique no menu Tools e selecione SDK Manager para que a janela dele seja aberta.

Na barra lateral, selecione esta sequência: Appearance & Behavior > System Settings > Android SDK. Selecione a guia SDK Platforms no painel do SDK do Android para abrir uma lista de opções de ferramentas instaladas. Confira se o SDK do Android 12.0 ou mais recente está instalado.

931f6ae02822f417.png

Em seguida, selecione a guia SDK Tools e confira se o NDK e o CMake estão instalados.

Observação: a versão exata não importa, desde que seja razoavelmente nova, mas as mais recentes são a 25.2.9519653 do NDK e a 3.24.0 do CMake. A versão do NDK instalada por padrão vai mudar ao longo do tempo com os lançamentos subsequentes. Se você precisar instalar uma versão específica do NDK, siga as instruções na referência do Android Studio para instalar o NDK na seção Instalar uma versão específica do NDK.

d28adf9279adec4.png

Quando todas as ferramentas necessárias estiverem marcadas, clique no botão Apply na parte de baixo da janela para instalar. Em seguida, feche a janela do SDK do Android clicando no botão Ok.

Como configurar o projeto

O projeto de exemplo é um jogo de simulação de física 3D simples desenvolvido com o Swappy para OpenGL. Não há muita diferença na estrutura de diretórios em comparação com o novo projeto criado usando o modelo, mas ainda há trabalho a ser feito para inicializar a física e o loop de renderização, então clone o repositório.

Como clonar o repositório

Na linha de comando, mude para o diretório que você quer que contenha o diretório raiz do jogo e clone-o pelo GitHub.

git clone -b codelab/start https://github.com/android/adpf-game-adaptability-codelab.git --recurse-submodules

Confira se você está começando pela confirmação inicial do repo intitulada [codelab] start: simple game.

Como configurar as dependências

O projeto de exemplo usa a biblioteca Dear ImGui para a interface do usuário. Ele também usa a Bullet Physics (links em inglês) para simulações de física em 3D. Essas bibliotecas são consideradas existentes no diretório third_party localizado na raiz do projeto. As respectivas bibliotecas foram verificadas usando --recurse-submodules especificados no comando clone acima.

Testar o projeto

No Android Studio, abra o projeto na raiz do diretório. Confira se um dispositivo está conectado. Em seguida, selecione Build > Make Project e Run > Run 'app' para testar a demonstração. O resultado final no dispositivo ficará assim:

f1f33674819909f1.png

Sobre o projeto

O jogo é intencionalmente minimalista para se concentrar nos detalhes da implementação dos recursos de adaptabilidade. Ele está executando algumas cargas de trabalho de física e gráficos facilmente configuráveis para que seja possível adaptar dinamicamente a configuração no momento da execução à medida que ocorrem mudanças na condição do dispositivo.

3. Integrar a API Thermal

a7e07127f1e5c37d.png

ce607eb5a2322d6b.png

Detectar mudanças no status térmico em Java

Desde o Android 10 (nível 29 da API), os dispositivos Android precisam informar ao aplicativo em execução qualquer mudança no status térmico. Os aplicativos podem detectar essas mudanças fornecendo OnThermalStatusChangedListener ao PowerManager.

Como o PowerManager.addThermalStatusListener só está disponível do nível 29 em diante da API, precisamos fazer uma verificação antes de chamar por ele:

// CODELAB: ADPFSampleActivity.java onResume
if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) {
   PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
      if (pm != null) {
         pm.addThermalStatusListener(this);
      }
}

Do nível 30 em diante da API, é possível registrar o callback no código em C++ usando AThermal_registerThermalStatusListener. Assim, você pode definir um método nativo e chamar por ele usando o Java, desta forma:

// CODELAB: ADPFSampleActivity.java onResume
if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
   // Use NDK Thermal API.
   nativeRegisterThermalStatusListener();
}

Você precisará adicionar os listeners na função de ciclo de vida onResume da sua atividade.

Tudo o que você adicionar ao onResume da sua atividade também precisará ser removido do onPause dela. Portanto, vamos definir nosso código de limpeza para chamar PowerManager.removeThermalStatusListener e AThermal_unregisterThermalStatusListener:

// CODELAB: ADPFSampleActivity.java onPause
// unregister the listener when it is no longer needed
// Remove the thermal state change listener on pause.
if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
   // Use NDK Thermal API.
   nativeUnregisterThermalStatusListener();
} else if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) {
   PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
   if (pm != null) {
      pm.removeThermalStatusListener(this);
   }
}

Vamos abstrair essas funcionalidades movendo-as para ADPFManager.java (link em inglês) para que possam ser facilmente reutilizadas em outros projetos.

Na classe de atividade do jogo, crie e mantenha uma instância do ADPFManager e conecte o listener térmico de adição/remoção ao método de ciclo de vida da atividade correspondente.

// CODELAB: ADPFSampleActivity.java
// Keep a copy of ADPFManager instance
private ADPFManager adpfManager;

// in onCreate, create an instance of ADPFManager
@Override
protected void onCreate(Bundle savedInstanceState) {
   // Instantiate ADPF manager.
   this.adpfManager = new ADPFManager();
   super.onCreate(savedInstanceState);
}

@Override
protected void onResume() {
   // Register ADPF thermal status listener on resume.
   this.adpfManager.registerListener(getApplicationContext());
   super.onResume();
}

@Override
protected void onPause() {
   // Remove ADPF thermal status listener on resume.
  this.adpfManager.unregisterListener(getApplicationContext());
   super.onPause();
}

Registrar os métodos nativos da classe C++ no JNI_OnLoad

Do nível 30 em diante da API, podemos usar o AThermal_* da API NDK Thermal para mapear o listener Java e chamar os mesmos métodos C++. Para chamar métodos Java no código C++, registre os métodos C++ em JNI_OnLoad. Confira outras Dicas de JNI para saber mais.

// CODELAB: android_main.cpp
// Remove the thermal state change listener on pause.
// Register classes to Java.
jint JNI_OnLoad(JavaVM *vm, void * /* reserved */) {
  JNIEnv *env;
  if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
    return JNI_ERR;
  }

  // Find your class. JNI_OnLoad is called from the correct class loader context
  // for this to work.
  jclass c = env->FindClass("com/android/example/games/ADPFManager");
  if (c == nullptr) return JNI_ERR;

  // Register your class' native methods.
  static const JNINativeMethod methods[] = {
      {"nativeThermalStatusChanged", "(I)V",
       reinterpret_cast<void *>(nativeThermalStatusChanged)},
      {"nativeRegisterThermalStatusListener", "()V",
       reinterpret_cast<void *>(nativeRegisterThermalStatusListener)},
      {"nativeUnregisterThermalStatusListener", "()V",
       reinterpret_cast<void *>(nativeUnregisterThermalStatusListener)},
  };
  int rc = env->RegisterNatives(c, methods,
                                sizeof(methods) / sizeof(JNINativeMethod));

  if (rc != JNI_OK) return rc;

  return JNI_VERSION_1_6;
}

Conectar seus listeners nativos ao jogo

Nosso jogo em C++ precisa saber reconhecer as mudanças no status térmico, então vamos criar a classe adpf_manager correspondente em C++.

Na pasta cpp da origem do app ($ROOT/app/src/main/cpp), crie um par de arquivos adpf_manager.h e adpf_manager.cpp (links em inglês):

// CODELAB: adpf_manager.h
// Forward declarations of functions that need to be in C decl.
extern "C" {
   void nativeThermalStatusChanged(JNIEnv* env, jclass cls, int32_t thermalState);
   void nativeRegisterThermalStatusListener(JNIEnv* env, jclass cls);
   void nativeUnregisterThermalStatusListener(JNIEnv* env, jclass cls);
}

typedef void (*thermalStateChangeListener)(int32_t, int32_t);

Defina as funções C no arquivo cpp fora da classe ADPFManager:

// CODELAB: adpf_manager.cpp
// Native callback for thermal status change listener.
// The function is called from Activity implementation in Java.
void nativeThermalStatusChanged(JNIEnv *env, jclass cls, jint thermalState) {
  ALOGI("Thermal Status updated to:%d", thermalState);
  ADPFManager::getInstance().SetThermalStatus(thermalState);
}

void nativeRegisterThermalStatusListener(JNIEnv *env, jclass cls) {
  auto manager = ADPFManager::getInstance().GetThermalManager();
  if (manager != nullptr) {
    auto ret = AThermal_registerThermalStatusListener(manager, thermal_callback,
                                                      nullptr);
    ALOGI("Thermal Status callback registered to:%d", ret);
  }
}

void nativeUnregisterThermalStatusListener(JNIEnv *env, jclass cls) {
  auto manager = ADPFManager::getInstance().GetThermalManager();
  if (manager != nullptr) {
    auto ret = AThermal_unregisterThermalStatusListener(
        manager, thermal_callback, nullptr);
    ALOGI("Thermal Status callback unregistered to:%d", ret);
  }
}

Inicializar o PowerManager e as funções necessárias para recuperar a margem térmica

Do nível 30 em diante da API, podemos usar o AThermal_* da API NDK Thermal. Na inicialização, chame AThermal_acquireManager e guarde para uso futuro. No nível 29 da API, precisamos encontrar e manter as referências do Java necessárias.

// CODELAB: adpf_manager.cpp
// Initialize JNI calls for the powermanager.
bool ADPFManager::InitializePowerManager() {
  if (android_get_device_api_level() >= 30) {
    // Initialize the powermanager using NDK API.
    thermal_manager_ = AThermal_acquireManager();
    return true;
  }

  JNIEnv *env = NativeEngine::GetInstance()->GetJniEnv();

  // Retrieve class information
  jclass context = env->FindClass("android/content/Context");

  // Get the value of a constant
  jfieldID fid =
      env->GetStaticFieldID(context, "POWER_SERVICE", "Ljava/lang/String;");
  jobject str_svc = env->GetStaticObjectField(context, fid);

  // Get the method 'getSystemService' and call it
  jmethodID mid_getss = env->GetMethodID(
      context, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
  jobject obj_power_service = env->CallObjectMethod(
      app_->activity->javaGameActivity, mid_getss, str_svc);

  // Add global reference to the power service object.
  obj_power_service_ = env->NewGlobalRef(obj_power_service);

  jclass cls_power_service = env->GetObjectClass(obj_power_service_);
  get_thermal_headroom_ =
      env->GetMethodID(cls_power_service, "getThermalHeadroom", "(I)F");

  // Free references
  env->DeleteLocalRef(cls_power_service);
  env->DeleteLocalRef(obj_power_service);
  env->DeleteLocalRef(str_svc);
  env->DeleteLocalRef(context);

  if (get_thermal_headroom_ == 0) {
    // The API is not supported in the platform version.
    return false;
  }

  return true;
}

Conferir se o método de inicialização será chamado

No nosso exemplo, o método de inicialização é chamado em SetApplication, e SetApplication é chamado em android_main. Essa configuração é específica para nosso framework, então se você estiver fazendo a integração no seu jogo, será necessário encontrar o lugar certo para chamar o método de inicialização:

// CODELAB: adpf_manager.cpp
// Invoke the API first to set the android_app instance.
void ADPFManager::SetApplication(android_app *app) {
  app_.reset(app);

  // Initialize PowerManager reference.
  InitializePowerManager();
}
// CODELAB: android_main.cpp
void android_main(struct android_app *app) {
  std::shared_ptr<NativeEngine> engine(new NativeEngine(app));

  // Set android_app to ADPF manager, which in turn will call InitializePowerManager
  ADPFManager::getInstance().SetApplication(app);

  ndk_helper::JNIHelper::Init(app);

  engine->GameLoop();
}

Monitorar a margem térmica regularmente

Muitas vezes, é melhor evitar que o status térmico seja elevado para um nível mais alto, já que é difícil diminuí-lo sem pausar totalmente a carga de trabalho. Mesmo depois de ser desligado, o dispositivo vai levar algum tempo para dissipar o calor e esfriar. É possível observar a margem térmica regularmente e ajustar nossa carga de trabalho para mantê-la sob controle e evitar que o status térmico aumente.

No ADPFManager, vamos expor métodos para verificar a margem térmica.

// CODELAB: adpf_manager.cpp
// Invoke the method periodically (once a frame) to monitor
// the device's thermal throttling status.
void ADPFManager::Monitor() {
  float current_clock = Clock();
  if (current_clock - last_clock_ >= kThermalHeadroomUpdateThreshold) {
    // Update thermal headroom.
    UpdateThermalStatusHeadRoom();
    last_clock_ = current_clock;
  }
}
// CODELAB: adpf_manager.cpp
// Retrieve current thermal headroom using JNI call.
float ADPFManager::UpdateThermalStatusHeadRoom() {
  if (android_get_device_api_level() >= 30) {
    // Use NDK API to retrieve thermal status headroom.
    thermal_headroom_ = AThermal_getThermalHeadroom(
        thermal_manager_, kThermalHeadroomUpdateThreshold);
    return thermal_headroom_;
  }

  if (app_ == nullptr || get_thermal_headroom_ == 0) {
    return 0.f;
  }
  JNIEnv *env = NativeEngine::GetInstance()->GetJniEnv();

  // Get thermal headroom!
  thermal_headroom_ =
      env->CallFloatMethod(obj_power_service_, get_thermal_headroom_,
                           kThermalHeadroomUpdateThreshold);
  ALOGE("Current thermal Headroom %f", thermal_headroom_);
  return thermal_headroom_;
}

Por fim, precisaremos expor métodos para definir o status térmico e o listener dele. Vamos receber o valor do status térmico pela API térmica do NDK ou pelo SDK do Java chamando nosso código nativo:

// CODELAB: adpf_manager.cpp
thermalStateChangeListener thermalListener = NULL;

void ADPFManager::SetThermalStatus(int32_t i) {
  int32_t prev_status_ = thermal_status_;
  int32_t current_status_ = i;
  thermal_status_ = i;
  if ( thermalListener != NULL ) {
    thermalListener(prev_status_, current_status_ );
  }
}

void ADPFManager::SetThermalListener(thermalStateChangeListener listener)
{
  thermalListener = listener;
}

Incluir o adpf_manager.cpp na unidade de compilação em CMakeLists.txt

É necessário adicionar o adpf_manager.cpp recém-criado à sua unidade de compilação.

Já terminamos de criar as classes java e cpp reutilizáveis do ADPFManager. Esses arquivos poderão ser usados em outros projetos sem que você precise reprogramar o código agrupador.

// CODELAB: CMakeLists.txt
# now build app's shared lib
add_library(game SHARED
        adpf_manager.cpp # add this line
        android_main.cpp
        box_renderer.cpp
        demo_scene.cpp

Definir mudanças nos parâmetros do jogo em caso de deterioração do estado térmico

Esta parte e as próximas serão específicas do jogo. No nosso exemplo, vamos reduzir as etapas de física e o número de caixas sempre que o status térmico aumentar.

Também vamos monitorar a margem térmica, mas não faremos nada além de mostrar o valor no HUD. No jogo, é possível reagir ao valor ajustando a quantidade de pós-processamento feita pela placa de vídeo, reduzindo o nível de detalhamento etc.

// CODELAB: demo_scene.cpp
// String labels that represents thermal states.
const char* thermal_state_label[] = {
    "THERMAL_STATUS_NONE",     "THERMAL_STATUS_LIGHT",
    "THERMAL_STATUS_MODERATE", "THERMAL_STATUS_SEVERE",
    "THERMAL_STATUS_CRITICAL", "THERMAL_STATUS_EMERGENCY",
    "THERMAL_STATUS_SHUTDOWN"};

const int32_t thermal_state_physics_steps[] = {
        16, 12, 8, 4,
};
const int32_t thermal_state_array_size[] = {
        8, 6, 4, 2,
};

Criar uma função para o listener de mudança de estado térmico no jogo

Agora, precisamos criar um thermalListener cpp que será chamado pelo ADPFManager sempre que o nível térmico do dispositivo mudar. Crie essa função no jogo para detectar as mudanças de valor no estado. Estamos acompanhando o last_state para saber se o nível da temperatura está aumentando ou diminuindo.

// CODELAB: demo_scene.cpp
// Dedicate a function to listen to the thermal state changed
void DemoScene::on_thermal_state_changed(int32_t last_state, int32_t current_state)
{
  if ( last_state != current_state ) {
    demo_scene_instance_->AdaptThermalLevel(current_state);
  }
}

...

// remember to pass it to the ADPFManager class we've just created, place this in DemoScene constructor: 
ADPFManager::getInstance().SetThermalListener(on_thermal_state_changed);

Adaptar o aplicativo quando o estado térmico mudar no jogo

Cada jogo terá necessidades e prioridades diferentes. O que é muito importante para um jogo pode não ser essencial para outro. Portanto, decida como otimizar seu aplicativo para evitar mais aquecimento.

No nosso exemplo, estamos reduzindo o número de objetos na tela e a fidelidade da física. Isso vai diminuir a carga de trabalho da CPU e GPU, e esperamos que também diminua um pouco a temperatura. Normalmente, é difícil diminuir a temperatura, a menos que o jogador faça uma pausa e deixe o dispositivo esfriar. É por isso que monitoramos a margem térmica e evitamos que o dispositivo atinja o estado limite:

// CODELAB: demo_scene.cpp
// Adapt your game when the thermal status has changed
void DemoScene::AdaptThermalLevel(int32_t index) {
  int32_t current_index = index;
  int32_t array_size = sizeof(thermal_state_physics_steps) / sizeof(thermal_state_physics_steps[0]);
  if ( current_index < 0 ) {
    current_index = 0;
  } else if ( current_index >= array_size ) {
    current_index = array_size - 1;
  }

  ALOGI("AdaptThermalLevel: %d", current_index);

  // in this sample, we are reducing the physics step when the device heated
  current_physics_step_ = thermal_state_physics_steps[current_index];
  // and also reduce the number of objects in the world
  // your situation may be different, you can reduce LOD to remove some calculations for example...
  int32_t new_array_size_ = thermal_state_array_size[current_index];

  if ( new_array_size_ != array_size_ ) {
      recreate_physics_obj_ = true;
  }
}

Geralmente, o desempenho máximo dos chips da CPU e GPU é ineficiente, ou seja, os chips costumam apresentar desempenho máximo com um consumo de energia muito maior e dissipar uma grande quantidade de calor. Compare isso com o desempenho sustentado, que é o melhor desempenho por unidade de energia consumida e calor dissipado. Leia mais sobre isso em Gerenciamento de desempenho do Android Open Source Project.

Crie e execute seu projeto. A margem e o estado térmico serão mostrados e, se o estado térmico deteriorar, as etapas de física e o número de objetos serão reduzidos.

4bdcfe567fc603c0.png

Caso algo dê errado, é possível comparar seu trabalho com o commit (link em inglês) do repo intitulado [codelab] step: integrated thermal-api.

4. Integrar a API Game Mode

A API Game Mode permite otimizar o jogo para ter o melhor desempenho ou a maior duração da bateria, de acordo com a seleção do jogador. Ela está disponível em alguns dispositivos com o Android 12 e em todos os dispositivos com o Android 13 ou mais recente.

Atualizar o manifesto do Android

Definir a appCategory

Para usar a API Game Mode, a categoria do aplicativo precisa ser um jogo. Vamos indicar isso na tag <application>.

// CODELAB: AndroidManifest.xml
<application
   android:appCategory="game">

Para o Android 13 e versões mais recentes

É recomendável destinar o jogo a usuários do Android 13 seguindo estas duas subetapas:

Adicionar game_mode_config <meta-data> e o arquivo XML correspondente

// CODELAB: AndroidManifest.xml
   <!-- ENABLING GAME MODES -->
   <!-- Add this <meta-data> under your <application> tag to enable Game Mode API if you're targeting API Level 33 (recommended) -->
   <meta-data android:name="android.game_mode_config"
        android:resource="@xml/game_mode_config" />
// CODELAB: app/src/main/res/xml/game_mode_config.xml
<?xml version="1.0" encoding="UTF-8"?>
<game-mode-config
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:supportsBatteryGameMode="true"
    android:supportsPerformanceGameMode="true" />

Caso o aplicativo seja destinado a dispositivos com Android 12

Adicionar cada <meta-data> do GameMode diretamente no AndroidManifest

// CODELAB: AndroidManifest.xml
   <!-- Use meta-data below instead if you are targeting API Level 31 or 32; you do not need to apply the 2 steps prior -->
   <meta-data android:name="com.android.app.gamemode.performance.enabled"
      android:value="true"/>
   <meta-data
      android:name="com.android.app.gamemode.battery.enabled"
      android:value="true"/>

Implementar GameModeManager.java para abstrair o recurso GameMode

Como a API Game Mode ainda não tem a interface cpp, é necessário usar a interface Java e fornecer as interfaces JNI. Vamos abstrair isso em GameModeManager.java para reutilizar a funcionalidade em outros projetos.

// CODELAB: GameModeManager.java
// Abstract the functionality in GameModeManager so we can easily reuse in other games
public class GameModeManager {

    private Context context;
    private int gameMode;

    public void initialize(Context context) {
        this.context = context;
        this.gameMode = GameManager.GAME_MODE_UNSUPPORTED;
        if ( context != null ) {
            if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ) {
                // Get GameManager from SystemService
                GameManager gameManager = context.getSystemService(GameManager.class);

                // Returns the selected GameMode
                gameMode = gameManager.getGameMode();
            }
        }
        // tell the native game about the selected gameMode
        this.retrieveGameMode(this.gameMode);
    }

    protected native void retrieveGameMode(int gameMode);
}

Adaptar a atividade para inicializar o GameModeManager e recuperar o GameMode em onResume

Conecte o GameMode ao ciclo de vida da atividade. Sempre que ele mudar, a atividade será reiniciada para capturar o valor durante onResume.

// CODELAB: ADPFSampleActivity.java
// we may keep and cache the object as class member variable
private GameModeManager gameModeManager;

...

@Override
protected void onCreate(Bundle savedInstanceState) {
   ...
   // Instantiate our GameModeManager
   this.gameModeManager = new GameModeManager();
   ...
   super.onCreate(savedInstanceState);
}

...

@Override
protected void onResume() {
   ...
   this.gameModeManager.initialize(getApplicationContext());
   ...
   super.onResume();
}

Implementar a classe GameModeManager para armazenar o GameMode selecionado pelo usuário para recuperação durante o jogo

Vamos criar um wrapper cpp e armazenar o valor do GameMode no cpp para facilitar a recuperação:

// CODELAB: game_mode_manager.h
class GameModeManager {
 public:
  // Singleton function.
  static GameModeManager& getInstance() {
    static GameModeManager instance;
    return instance;
  }
  // Dtor. Remove global reference (if any).
  ~GameModeManager() {}

  // Delete copy constructor since the class is used as a singleton.
  GameModeManager(GameModeManager const&) = delete;
  void operator=(GameModeManager const&) = delete;

  void SetGameMode(int game_mode) { game_mode_ = game_mode; }
  int GetGameMode() { return game_mode_; }

 private:
  // Ctor. It's private since the class is designed as a singleton.
  GameModeManager() {}

  int game_mode_ = 0;
};

Implementar retrieveGameMode no código nativo para transmitir o valor de GameMode ao jogo

Essa é a maneira mais simples e eficiente de recuperar o valor da API Game Mode após o início e transmiti-lo à variável cpp para facilitar o acesso. Podemos confiar no valor armazenado em cache sem precisar fazer chamadas de JNI todas as vezes.

// CODELAB: game_mode_manager.cpp
extern "C" {

void Java_com_android_example_games_GameModeManager_retrieveGameMode(
    JNIEnv* env, jobject obj, jint game_mode) {
  GameModeManager& gmm = GameModeManager::getInstance();
  int old_game_mode = gmm.GetGameMode();
  ALOGI("GameMode updated from %d to:%d", old_game_mode, game_mode);
  GameModeManager::getInstance().SetGameMode(game_mode);
}

}

Incluir o game_mode_manager.cpp na unidade de compilação em CMakeLists.txt

É necessário adicionar o game_mode_manager.cpp recém-criado à sua unidade de compilação.

Já terminamos de criar as classes java e cpp (links em inglês) reutilizáveis do GameModeManager. Esses arquivos poderão ser usados em outros projetos sem que você precise reprogramar o código agrupador.

// CODELAB: CMakeLists.txt
# now build app's shared lib
add_library(game SHARED
        game_mode_manager.cpp # add this line
        android_main.cpp
        box_renderer.cpp
        demo_scene.cpp

Adaptar o jogo de acordo com o GameMode selecionado pelo usuário

Depois de recuperar a API Game Mode, é necessário diferenciar o jogo de acordo com o valor selecionado pelo usuário. A diferença mais significativa (e com a maioria dos valores de polarização) está entre os modos PERFORMANCE e BATTERY. No modo PERFORMANCE, os usuários geralmente querem mergulhar no jogo e ter a melhor experiência sem se preocupar com a duração da bateria. Assim, você pode oferecer a melhor fidelidade possível, desde que o frame rate seja estável. No modo BATTERY, os usuários podem querer jogar por mais tempo, aceitando configurações menores. Confira se o frame rate permanece estável. Um frame rate instável gera uma experiência ruim para os jogadores.

// CODELAB: demo_scene.cpp
void DemoScene::RenderPanel() {
  ...
  GameModeManager& game_mode_manager = GameModeManager::getInstance();
  // Show the stat changes according to selected Game Mode
  ImGui::Text("Game Mode: %d", game_mode_manager.GetGameMode());
}
// CODELAB: native_engine.h
// Add this function to NativeEngine class, we're going to check the gameMode and set the preferred frame rate and resolution cap accordingly
void CheckGameMode();

No nosso exemplo, estamos renderizando com resolução máxima e 60 FPS no modo PERFORMANCE. Como esse é um exemplo bastante simples, a maioria dos dispositivos vai conseguir fazer a execução sem dificuldade e com um taxa alta de QPS. Portanto, não aplicamos outras verificações para simplificar o aplicativo. No modo BATTERY, limitamos a renderização a 30 QPS, com um quarto da resolução máxima. Encontre os melhores pontos para otimização no seu projeto. Lembre-se que a experiência de jogo e a economia de energia não se limitam apenas a QPS e resolução. Para ideias de como otimizar o aplicativo, confira a história de sucesso dos nossos desenvolvedores:

// CODELAB: native_engine.cpp
void NativeEngine::CheckGameMode() {
  GameModeManager &gameModeManager = GameModeManager::getInstance();
  int game_mode = gameModeManager.GetGameMode();
  if (game_mode != mGameMode) {
    // game mode changed, make necessary adjustments to the game
    // in this sample, we are capping the frame rate and the resolution
    // we're also hardcoding configs on the engine 🫣
    SceneManager *sceneManager = SceneManager::GetInstance();
    NativeEngine *nativeEngine = NativeEngine::GetInstance();
    int native_width = nativeEngine->GetNativeWidth();
    int native_height = nativeEngine->GetNativeHeight();
    int preferred_width;
    int preferred_height;
    int32_t preferredSwapInterval = SWAPPY_SWAP_30FPS;
    if (game_mode == GAME_MODE_STANDARD) {
      // GAME_MODE_STANDARD : fps: 30, res: 1/2
      preferredSwapInterval = SWAPPY_SWAP_30FPS;
      preferred_width = native_width / 2;
      preferred_height = native_height / 2;
    } else if (game_mode == GAME_MODE_PERFORMANCE) {
      // GAME_MODE_PERFORMANCE : fps: 60, res: 1/1
      preferredSwapInterval = SWAPPY_SWAP_60FPS;
      preferred_width = native_width;
      preferred_height = native_height;
    } else if (game_mode == GAME_MODE_BATTERY) {
      // GAME_MODE_BATTERY : fps: 30, res: 1/4
      preferred_height = SWAPPY_SWAP_30FPS;
      preferred_width = native_width / 4;
      preferred_height = native_height / 4;
    } else {  // game_mode == 0 : fps: 30, res: 1/2
      // GAME_MODE_UNSUPPORTED
      preferredSwapInterval = SWAPPY_SWAP_30FPS;
      preferred_width = native_width / 2;
      preferred_height = native_height / 2;
    }
    ALOGI("GameMode SetPreferredSizeAndFPS: %d, %d, %d", preferred_width,
          preferred_height, preferredSwapInterval);
    sceneManager->SetPreferredSize(preferred_width, preferred_height);
    sceneManager->SetPreferredSwapInterval(preferredSwapInterval);
    mGameMode = game_mode;
  }
}
// CODELAB: native_engine.cpp
void NativeEngine::DoFrame() {
  ...

  // here, we are checking on every frame for simplicity
  // but you can hook it to your onResume callback only
  // as gameMode changes will trigger Activity restart
  CheckGameMode();
  SwitchToPreferredDisplaySize();

  ...
}

Verificar as sdkVersions no arquivo de build do Gradle

A API Game Mode está disponível em todos os dispositivos Android do Android 13 em diante. Esse recurso também está ativado em alguns dispositivos com Android 12.

// CODELAB: app/build.gradle
android {
    compileSdk 33
    ...

    defaultConfig {
        minSdkVersion 30 
        targetSdkVersion 33 // you can use 31 if you're targeting Android 12 and follow the Note section above
        ...
    }

Caso algo dê errado, é possível comparar seu trabalho com o commit do repo intitulado [codelab] step: integrate game-mode-api.

496c76415d12cbed.png

5. Integrar a API Game State

A API Game State permite informar ao sistema o estado de nível superior do jogo, possibilitando que você saiba se o conteúdo atual pode ser interrompido sem interferir na jogabilidade, que não pode ser pausada. Além disso, ela indica se o jogo está realizando comunicações de E/S intensas, como o carregamento de recursos de discos ou rede. Saber todos esses dados permite que o sistema aloque os recursos certos no melhor momento para você. Por exemplo, ele pode alocar a quantidade correta de largura de banda da CPU e memória durante o carregamento ou pode priorizar o tráfego de rede para seu jogo multiplayer quando o jogador está em uma sessão importante.

Definir tipos enumerados para facilitar o mapeamento para constantes de GameState

Como a API Game State não tem uma interface cpp, vamos copiar os valores.

// CODELAB: game_mode_manager.h
enum GAME_STATE_DEFINITION {
    GAME_STATE_UNKNOWN = 0,
    GAME_STATE_NONE = 1,
    GAME_STATE_GAMEPLAY_INTERRUPTIBLE = 2,
    GAME_STATE_GAMEPLAY_UNINTERRUPTIBLE = 3,
    GAME_STATE_CONTENT = 4,
};

Definir SetGameState no código cpp para chamar a API do Java pelo JNI

Uma função cpp é tudo o que precisamos para transmitir todas as informações para a API do Java.

// CODELAB: game_mode_manager.h
void SetGameState(bool is_loading, GAME_STATE_DEFINITION game_state);

Não se preocupe, vamos usar apenas uma chamada de JNI…

// CODELAB: game_mode_manager.cpp
void GameModeManager::SetGameState(bool is_loading,
                                   GAME_STATE_DEFINITION game_state) {
  if (android_get_device_api_level() >= 33) {
    ALOGI("GameModeManager::SetGameState: %d => %d", is_loading, game_state);

    JNIEnv* env = NativeEngine::GetInstance()->GetJniEnv();

    jclass cls_gamestate = env->FindClass("android/app/GameState");

    jmethodID ctor_gamestate =
        env->GetMethodID(cls_gamestate, "<init>", "(ZI)V");
    jobject obj_gamestate = env->NewObject(
        cls_gamestate, ctor_gamestate, (jboolean)is_loading, (jint)game_state);

    env->CallVoidMethod(obj_gamemanager_, gamemgr_setgamestate_, obj_gamestate);

    env->DeleteLocalRef(obj_gamestate);
    env->DeleteLocalRef(cls_gamestate);
  }
}

Chamar SetGameState sempre que o estado do jogo mudar

Basta chamar nossa função cpp no momento adequado, como ao iniciar o carregamento, parar o carregamento ou entrar e sair de diferentes estados no jogo:

// CODELAB: welcome_scene.cpp
void WelcomeScene::OnInstall() {
  // 1. Game State: Start Loading
  GameModeManager::getInstance().SetGameState(true, GAME_STATE_NONE);
}
// CODELAB: welcome_scene.cpp
void WelcomeScene::OnStartGraphics() {
  // 2. Game State: Finish Loading, showing the attract screen which is interruptible
  GameModeManager::getInstance().SetGameState(
      false, GAME_STATE_GAMEPLAY_INTERRUPTIBLE);
}
// CODELAB: welcome_scene.cpp
void WelcomeScene::OnKillGraphics() {
  // 3. Game State: exiting, cleaning up and preparing to load the next scene
  GameModeManager::getInstance().SetGameState(true, GAME_STATE_NONE);
}

Você poderá transmitir UNKNOWN se não souber o estado nesse momento. No nosso caso, estamos descarregando a cena e não sabemos para onde o usuário vai seguir, mas a próxima cena será carregada em breve. Então, poderemos chamar outro SetGameState com o novo estado conhecido.

// CODELAB: welcome_scene.cpp
void WelcomeScene::OnUninstall() {
  // 4. Game State: Finished unloading this scene, it will be immediately followed by loading the next scene
  GameModeManager::getInstance().SetGameState(false, GAME_STATE_UNKNOWN);
}

Fácil, não é? Depois de integrar a API Game State, o sistema vai saber dos estados do seu aplicativo e começará a otimizar os recursos para conseguir o melhor desempenho e a melhor eficiência para o jogador.

Fique de olho na próxima atualização da API Game State, em que vamos explicar como você pode usar rótulos e a qualidade para otimizar ainda mais seu jogo.

6. Integrar a API Performance Hint

A API Performance Hint permite enviar dicas de desempenho ao sistema criando sessões para cada grupo de linha de execução responsável por uma tarefa específica, assim como definir a duração inicial desejada para o trabalho e, em cada frame, informar a duração real e atualizar a esperada para o próximo frame.

Por exemplo, se você tiver um grupo de linhas de execução responsáveis pela IA inimiga, pelo cálculo físico e pela linha de execução de renderização. Todas essas subtarefas precisam ser concluídas a cada frame. Se houver um excesso de dados em uma delas, seu frame será atrasado, perdendo a meta de QPS. Você pode criar uma sessão PerformanceHint para esse grupo de linhas de execução e definir targetWorkDuration como o QPS desejado. Conforme você usa reportActualWorkDuration após o trabalho realizado em cada frame, o sistema pode analisar essa tendência e ajustar os recursos da CPU adequadamente. Isso garante que você possa atingir a meta desejada em cada frame. O resultado é uma melhor estabilidade de frames e um consumo de energia mais eficiente do seu jogo.

InitializePerformanceHintManager no ADPFManager

É necessário criar a sessão de dicas para as linhas de execução. Temos a API C++, mas ela está disponível apenas do nível 33 da API em diante. Para os níveis 31 e 32 da API, temos que usar a API Java. Vamos armazenar em cache alguns métodos JNI para usar depois.

// CODELAB: adpf_manager.cpp
// Initialize JNI calls for the PowerHintManager.
bool ADPFManager::InitializePerformanceHintManager() {
  #if __ANDROID_API__ >= 33
    if ( hint_manager_ == nullptr ) {
        hint_manager_ = APerformanceHint_getManager();
    }
    if ( hint_session_ == nullptr && hint_manager_ != nullptr ) {
        int32_t tid = gettid();
        thread_ids_.push_back(tid);
        int32_t tids[1];
        tids[0] = tid;
        hint_session_ = APerformanceHint_createSession(hint_manager_, tids, 1, last_target_);
    }
    ALOGI("ADPFManager::InitializePerformanceHintManager __ANDROID_API__ 33");
    return true;
#else  
  ALOGI("ADPFManager::InitializePerformanceHintManager __ANDROID_API__ < 33");
  JNIEnv *env = NativeEngine::GetInstance()->GetJniEnv();

  // Retrieve class information
  jclass context = env->FindClass("android/content/Context");

  // Get the value of a constant
  jfieldID fid = env->GetStaticFieldID(context, "PERFORMANCE_HINT_SERVICE",
                                       "Ljava/lang/String;");
  jobject str_svc = env->GetStaticObjectField(context, fid);

  // Get the method 'getSystemService' and call it
  jmethodID mid_getss = env->GetMethodID(
      context, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
  jobject obj_perfhint_service = env->CallObjectMethod(
      app_->activity->javaGameActivity, mid_getss, str_svc);

  // Add global reference to the power service object.
  obj_perfhint_service_ = env->NewGlobalRef(obj_perfhint_service);

  // Retrieve methods IDs for the APIs.
  jclass cls_perfhint_service = env->GetObjectClass(obj_perfhint_service_);
  create_hint_session_ =
      env->GetMethodID(cls_perfhint_service, "createHintSession",
                       "([IJ)Landroid/os/PerformanceHintManager$Session;");
  jmethodID mid_preferedupdaterate = env->GetMethodID(
      cls_perfhint_service, "getPreferredUpdateRateNanos", "()J");

  // Create int array which contain current tid.
  jintArray array = env->NewIntArray(1);
  int32_t tid = gettid();
  env->SetIntArrayRegion(array, 0, 1, &tid);
  const jlong DEFAULT_TARGET_NS = 16666666;

  // Create Hint session for the thread.
  jobject obj_hintsession = env->CallObjectMethod(
      obj_perfhint_service_, create_hint_session_, array, DEFAULT_TARGET_NS);
  if (obj_hintsession == nullptr) {
    ALOGI("Failed to create a perf hint session.");
  } else {
    obj_perfhint_session_ = env->NewGlobalRef(obj_hintsession);
    preferred_update_rate_ =
        env->CallLongMethod(obj_perfhint_service_, mid_preferedupdaterate);

    // Retrieve mid of Session APIs.
    jclass cls_perfhint_session = env->GetObjectClass(obj_perfhint_session_);
    report_actual_work_duration_ = env->GetMethodID(
        cls_perfhint_session, "reportActualWorkDuration", "(J)V");
    update_target_work_duration_ = env->GetMethodID(
        cls_perfhint_session, "updateTargetWorkDuration", "(J)V");
    set_threads_ = env->GetMethodID(
        cls_perfhint_session, "setThreads", "([I)V");
  }

  // Free local references
  env->DeleteLocalRef(obj_hintsession);
  env->DeleteLocalRef(array);
  env->DeleteLocalRef(cls_perfhint_service);
  env->DeleteLocalRef(obj_perfhint_service);
  env->DeleteLocalRef(str_svc);
  env->DeleteLocalRef(context);

  if (report_actual_work_duration_ == 0 || update_target_work_duration_ == 0) {
    // The API is not supported in the platform version.
    return false;
  }

  return true;
#endif // __ANDROID_API__ >= 33

}

Chamar em ADPFManager::SetApplication

Chame a função de inicialização que definimos no android_main:

// CODELAB: adpf_manager.cpp
// Invoke the API first to set the android_app instance.
void ADPFManager::SetApplication(android_app *app) {
  ...

  // Initialize PowerHintManager reference.
  InitializePerformanceHintManager();
}
// CODELAB: android_main.cpp
void android_main(struct android_app *app) {
  ...

  // Set android_app to ADPF manager & call InitializePerformanceHintManager
  ADPFManager::getInstance().SetApplication(app);

  ...
}

Definir ADPFManager::BeginPerfHintSession e ADPFManager::EndPerfHintSession

Defina os métodos de cpp para invocar as APIs com o JNI, aceitando todos os parâmetros necessários.

// CODELAB: adpf_manager.h
// Indicates the start and end of the performance intensive task.
// The methods call performance hint API to tell the performance
// hint to the system.
void BeginPerfHintSession();
void EndPerfHintSession(jlong target_duration_ns);
// CODELAB: adpf_manager.cpp
// Indicates the start and end of the performance intensive task.
// The methods call performance hint API to tell the performance hint to the system.
void ADPFManager::BeginPerfHintSession() { 
  perf_start_ = std::chrono::high_resolution_clock::now(); 
}

void ADPFManager::EndPerfHintSession(jlong target_duration_ns) {
#if __ANDROID_API__ >= 33
    auto perf_end = std::chrono::high_resolution_clock::now();
    auto dur = std::chrono::duration_cast<std::chrono::nanoseconds>(perf_end - perf_start_).count();
    int64_t actual_duration_ns = static_cast<int64_t>(dur);
    APerformanceHint_reportActualWorkDuration(hint_session_, actual_duration_ns);
    APerformanceHint_updateTargetWorkDuration(hint_session_, target_duration_ns);
#else
  if (obj_perfhint_session_) {
    auto perf_end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(perf_end - perf_start_).count();
    int64_t duration_ns = static_cast<int64_t>(duration);
    JNIEnv *env = NativeEngine::GetInstance()->GetJniEnv();

    // Report and update the target work duration using JNI calls.
    env->CallVoidMethod(obj_perfhint_session_, report_actual_work_duration_,
                        duration_ns);
    env->CallVoidMethod(obj_perfhint_session_, update_target_work_duration_,
                        target_duration_ns);
  }
#endif // __ANDROID_API__ >= 33

  }
}

Fazer chamadas no início e no final de cada frame

Em cada frame, será necessário registrar o horário de início no começo do frame e informar a duração real no final dele. Para isso, vamos chamar reportActualWorkDuration e updateTargetWorkDuration. No nosso exemplo simples, a carga de trabalho não muda entre os frames, mas vamos atualizar TargetWorkDuration com o valor desejado consistente:

// CODELAB: demo_scene.cpp
void DemoScene::DoFrame() {
  // Tell ADPF manager beginning of the perf intensive task.
  ADPFManager::getInstance().BeginPerfHintSession();

  ...
  
  // Tell ADPF manager end of the perf intensive tasks.
  ADPFManager::getInstance().EndPerfHintSession(jlong target_duration_ns);
}

7. Parabéns

Parabéns! Você integrou os recursos de adaptabilidade a um jogo.

No futuro, vamos adicionar mais recursos do framework de adaptabilidade ao Android.