1. Introdução
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
- Android Studio Electric Eel ou versão mais recente.
- Um dispositivo Android conectado ao computador com as Opções do desenvolvedor e a depuração USB ativadas. Você vai executar o jogo nesse dispositivo.
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".
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.
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.
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:
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
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.
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
.
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.