在原生遊戲中整合適應性功能

1. 簡介

70748189a60ed450.png

為什麼需要在遊戲中整合適應性功能?

適應性 API 可讓您在應用程式執行階段期間取得裝置的狀態回饋,並以動態方式調整工作負載,將遊戲效能最佳化。這些 API 也可以向系統告知工作負載,讓系統以最佳方式分配資源。

建構項目

在本程式碼研究室中,您將開啟並執行原生 Android 範例遊戲,然後在遊戲中整合適應性功能。設定及新增必要的程式碼後,您就能針對先前的遊戲與具適應性功能的版本,測試玩家感受到的效能有何不同。

課程內容

  • 如何在現有遊戲中整合 Thermal API,透過調整熱力狀態來防止過熱。
  • 如何整合 Game Mode API,因應所選模式的變更。
  • 如何整合 Game State API,讓系統瞭解遊戲的執行狀態。
  • 如何整合 Performance Hint API,讓系統瞭解執行緒模型和工作負載。

軟硬體需求

2. 開始設定

設定開發環境

如果您之前未在 Android Studio 中使用原生專案,可能需要安裝 Android NDK 和 CMake。如已安裝這些項目,請跳至「設定專案」一節。

檢查是否已安裝 SDK、NDK 和 CMake

請啟動 Android Studio。在畫面顯示「Welcome to Android Studio」視窗時,開啟「Configure」下拉式選單,然後選取「SDK Manager」選項。

3b7b47a139bc456.png

如果已開啟現有專案,可以改從「Tools」選單開啟 SDK Manager。請按一下「Tools」選單,然後選取「SDK Manager」,SDK Manager 視窗會隨即開啟。

在側欄中,依序選取「Appearance & Behavior」>「System Settings」>「Android SDK」。在 Android SDK 窗格中選取「SDK Platforms」分頁標籤,畫面就會列出已安裝的工具選項。請確認已安裝 Android SDK 12.0 以上版本。

931f6ae02822f417.png

接著選取「SDK Tools」分頁標籤,確認已安裝 NDKCMake

注意:只要版本不舊皆可,不一定要使用特定版本。我們目前使用的是 NDK 25.2.9519653 和 CMake 3.24.0。隨著較新的 NDK 版本持續推出,預設安裝的 NDK 版本也會變更。如需安裝特定版本的 NDK,請參閱關於安裝 NDK 的 Android Studio 參考資料,按照「安裝特定版本的 NDK」一節指示操作。

d28adf9279adec4.png

勾選所有必要工具後,點選視窗底部的「Apply」按鈕即可安裝。接著您可以點選「OK」按鈕,關閉 Android SDK 視窗。

設定專案

本範例專案是使用 OpenGL 版 Swappy 開發的簡易 3D 物理模擬遊戲。與範本建立的新專案相較,本專案的目錄結構並無多少變更,只是在初始化物理和算繪迴圈方面有些調整,因此您可以繼續操作,並改為複製存放區。

複製存放區

請在指令列中,變更為要納入根遊戲目錄的目錄,並從 GitHub 複製該目錄:

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

開始操作時,請確認您是使用 [codelab] start: simple game 存放區的初始修訂版本。

設定依附元件

範例專案會將 Dear ImGui 程式庫用於使用者介面,並將 Bullet Physics 程式庫用於執行 3D 物理模擬。我們假設這些程式庫位於專案根目錄的 third_party 目錄中。在上述複製指令中,我們已透過指定 --recurse-submodules 查看個別程式庫。

測試專案

請在 Android Studio 中,從根目錄開啟專案。請確認裝置已連接,然後依序選取「Build」>「Make Project」>「Run」>「Run 'app'」,測試此試用版。裝置上的最終結果應如下所示:

f1f33674819909f1.png

關於專案

我們刻意將遊戲設計得十分精簡,讓您專注在實作適應性功能的具體細節。這款遊戲會執行一些可輕鬆設定的物理和圖形工作負載,方便我們在執行階段期間,隨著裝置狀況的變化動態調整設定。

3. 整合 Thermal API

a7e07127f1e5c37d.png

ce607eb5a2322d6b.png

在 Java 中監聽熱力狀態變更

從 Android 10 (API 級別 29) 開始,每當熱力狀態有所變更,Android 裝置都必須向執行中的應用程式回報。透過向 PowerManager 提供 OnThermalStatusChangedListener,應用程式即可監聽這項變更。

由於 PowerManager.addThermalStatusListener 只適用於 29 以上的 API 級別,因此呼叫前需先進行檢查:

// 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);
      }
}

在 30 以上的 API 級別,您可以使用 AThermal_registerThermalStatusListener 在 C++ 程式碼中註冊回呼,這樣就能定義原生方法,然後在 Java 中呼叫該方法,如下所示:

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

您需要在活動的 onResume 生命週期函式中新增事件監聽器。

請記住,您在活動的 onResume 中新增的所有內容,之後都需要從活動的 onPause 中移除。因此,請定義清除程式碼,呼叫 PowerManager.removeThermalStatusListenerAThermal_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);
   }
}

請將這些功能移至 ADPFManager.java 來提取這些功能,方便在其他專案中輕鬆重複使用。

請在遊戲的活動類別中,建立並保留 ADPFManager 的例項,然後將新增/移除熱力事件監聽器連結至相對應的活動生命週期方法。

// 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();
}

在 JNI_OnLoad 中註冊 C++ 類別的原生方法

在 30 以上的 API 級別,我們可以使用 NDK Thermal API AThermal_*,因此您可以將 Java 事件監聽器對應至相同的 C++ 方法。如要將 Java 方法呼叫至 C++ 程式碼中,您需要在 JNI_OnLoad 中註冊 C++ 方法。詳情請參閱「JNI 提示」。

// 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;
}

連結原生事件監聽器與遊戲

C++ 遊戲需要知道相對應的熱力狀態何時已變更,所以我們要在 C++ 中建立相對應的 adpf_manager 類別。

請在應用程式來源的 cpp 資料夾中 ($ROOT/app/src/main/cpp),建立一組 adpf_manager.hadpf_manager.cpp 檔案。

// 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);

在 ADPFManager 類別外的 cpp 檔案中定義 C 函式。

// 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);
  }
}

初始化 PowerManager 和擷取熱力上升空間所需的函式

在 30 以上的 API 級別,我們可以使用 NDK Thermal API AThermal_*,因此請針對初始化作業呼叫並保留 AThermal_acquireManager 供日後使用。在 API 級別 29,我們需要找到並保留必要的 Java 參照。

// 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;
}

確認系統會呼叫初始化方法

在本範例中,系統會從 SetApplication 呼叫初始化方法,而 SetApplication 是從 android_main 呼叫。這項設定只適用於此架構,因此如果要在您的遊戲中整合這項設定,請務必找出呼叫初始化方法的正確位置。

// 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();
}

定期監控熱力上升空間

一般而言,建議防止熱力狀態上升到更高的等級,因為如果未完全暫停工作負載,會難以降低熱力狀態。即使在完全關閉裝置後,裝置仍需要一段時間才能散熱降溫。我們可以定期監測熱力上升空間並調整工作負載,藉此控制上升空間,防止熱力狀態升高。

接下來,我們要在 ADPFManager 中公開檢查熱力上升空間的方法。

// 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_;
}

最後,我們還需要公開方法,才能設定熱力狀態和事件監聽器。我們會從 NDK Thermal API 或呼叫至原生程式碼的 Java SDK,取得熱力狀態的值。

// 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;
}

將 adpf_manager.cpp 納入 CMakeLists.txt 中的編譯單位

請記得在編譯單位中加入新建立的 adpf_manager.cpp。

我們現在已處理完可重複使用的 ADPFManager javacpp 類別,您就能擷取這些檔案,並在其他專案中重複使用,不需再次編寫膠水程式碼。

// 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

定義要在熱力狀態下降時變更的遊戲內參數

本節之後的內容只與遊戲相關。在本範例中,每當熱力狀態提高,我們都要減少物理步驟和方塊數量。

我們也要監測熱力上升空間,但除了在 HUD 顯示值之外,並不會採取其他行動。在您的遊戲中,如要針對該值採取因應措施,您可以調整圖形資訊卡執行的後置處理量、降低精細程度等。

// 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,
};

在遊戲中建立監聽熱力狀態變更的函式

現在,我們需要建立 cpp thermalListener,每當 ADPFManager 偵測到裝置熱等級發生變化時,就會呼叫此函式。在遊戲中建立這個函式,即可監聽狀態變更值。我們會持續追蹤 last_state,以便瞭解熱等級是上升或下降。

// 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);

根據熱力狀態變更調整遊戲

每款遊戲的需求和優先順序都不盡相同,對某款遊戲非常重要的項目在其他遊戲中可能並非必要,因此您需要自行決定如何最佳化,防止遊戲的熱等級進一步上升。

在本範例中,我們將減少畫面中物體的數量,並降低物理擬真度。這麼做可減輕 CPU 和 GPU 的工作負載,有望稍微降低熱等級。請注意,除非玩家休息並讓裝置降溫,否則熱等級通常很難大幅下降,因此我們要密切監測熱力上升空間,防止裝置達到過熱保護狀態。

// 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;
  }
}

另請注意,CPU 和 GPU 晶片提供最高效能時通常效率不彰,因為晶片在提供最高效能時,耗電量通常會大幅提高,並排放大量的熱。相較之下,提供穩定持續的效能時,每單位耗電量和熱排放量可獲得最佳表現。詳情請參閱 Android 開放原始碼計畫的「效能管理」一文。

請建構並執行專案,畫面上會顯示目前的熱力狀態和熱力上升空間。如果熱力狀態下降,物理步驟和物體數量都會減少。

4bdcfe567fc603c0.png

如果發生錯誤,您可以比較自己的成果與 [codelab] step: integrated thermal-api 存放區中的修訂版本

4. 整合 Game Mode API

Game Mode API 可讓您根據玩家選擇的模式將遊戲最佳化,達到最佳效能或最長電池續航力。這項功能適用於特定 Android 12 裝置,以及搭載 Android 13 以上版本的所有裝置。

更新 Android 資訊清單

設定 appCategory

如要使用 Game Mode API,應用程式類別必須設為遊戲。請在 <application> 標記中指明。

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

針對 Android 13 以上版本

建議您按照下列 2 個子步驟,指定 Android 13 使用者:

新增 game_mode_config <meta-data> 和對應的 XML 檔案

// 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" />

指定 Android 12 裝置時

直接在 AndroidManifest 中新增各個 gamemode <meta-data>

// 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"/>

實作 GameModeManager.java 來提取 GameMode 功能

由於 Game Mode API 還沒有 cpp 介面,我們需要使用 Java 介面並提供 JNI 介面。讓我們在 GameModeManager.java 中提取此 API,方便在其他專案中重複使用其功能。

// 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);
}

調整活動以初始化 GameModeManager,並在 onResume 中擷取 GameMode

請連結活動生命週期。每當 Game Mode 有所變更,活動就會重新啟動,我們就能在 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();
}

實作 GameModeManager 類別來儲存使用者所選的 gameMode,方便在遊戲內擷取

為方便擷取 Game Mode 值,我們要建立 cpp 包裝函式,將該值儲存在 cpp 中。

// 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;
};

在原生程式碼中實作 retrieveGameMode,將 GameMode 值傳遞至遊戲

這是最簡單也最有效率的方法:在啟動時擷取 Game Mode 值,然後傳遞至 cpp 變數,即可輕鬆存取。我們可以使用快取的值,不需要每次都發出 JNI 呼叫。

// 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);
}

}

將 game_mode_manager.cpp 納入 CMakeLists.txt 中的編譯單位

請記得在編譯單位中加入新建立的 game_mode_manager.cpp。

我們現在已處理完可重複使用的 GameModeManager javacpp 類別,您就能擷取這些檔案,並在其他專案中重複使用,不需再次編寫膠水程式碼。

// 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

根據使用者所選 GameMode 調整遊戲

擷取 Game Mode 後,您必須根據使用者所選的值調整遊戲。差異最顯著且值最分化的模式為效能模式和省電模式。在效能模式中,使用者通常希望沉浸在遊戲中享受最佳體驗,不會擔心電池續航力,因此您可以提供最佳擬真度,只要畫面更新率保持穩定即可。在省電模式中,使用者會希望延長玩遊戲的時間,並接受較低階的設定。請確認畫面更新率保持穩定,因為不穩定的畫面更新率會造成極差的玩家體驗。

// 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();

在本範例中,效能模式下的算繪作業會採用 60 FPS 的完整解析度。由於這是相當簡單的範例,大多數裝置都能以完整 FPS 順暢執行,因此我們不會進一步套用檢查機制,而會保持簡單的做法。若是省電模式下的算繪作業,FPS 最高為 30,而解析度最高為四分之一。您需要自行找出理想的最佳化做法。切記,許多因素都會影響遊戲體驗和省電效果,並非只限於 FPS 和解析度!如要進一步瞭解如何最佳化,歡迎參考開發人員的成功案例

// 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();

  ...
}

記得檢查 Gradle 建構檔案中的 sdkVersion

從 Android 13 開始,所有 Android 裝置都可使用 Game Mode API。特定 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
        ...
    }

如果發生錯誤,您可以比較自己的成果與 [codelab] step: integrate game-mode-api 存放區中的修訂版本。

496c76415d12cbed.png

5. 整合 Game State API

Game State API 可讓系統瞭解遊戲的頂層狀態,讓您判斷是否能中斷目前的內容,而不會對無法暫停的遊戲過程造成干擾。此 API 也可指出遊戲是否正在執行大量 I/O 作業,例如從磁碟或網路載入資產。系統瞭解所有這些資料後,就能在適當時機分配合適的資源,例如在載入作業期間分配適當的 CPU 用量和記憶體頻寬,或是在玩家處於重要的多人遊戲工作階段時,為該多人遊戲優先提供網路流量。

定義列舉,方便對應至 GameState 常數

由於 Game State API 沒有 cpp 介面,我們要先將值複製到 cpp。

// 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,
};

在 cpp 程式碼中定義 SetGameState,即可透過 JNI 呼叫 Java API

我們只需要設定 cpp 函式,就能將所有資訊傳遞至 Java API。

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

別慌張,只是一個 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);
  }
}

每當遊戲狀態變更就呼叫 SetGameState

只要在適當時機呼叫 cpp 函式即可,例如在開始載入、停止載入,或是進入/離開不同遊戲狀態的時候。

// 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);
}

如果不知道當時的狀態,可以傳遞 UNKNOWN。在本例中,我們要卸載場景,且不知道使用者接下來的操作,但下一個場景很快就會載入,我們就能使用新的已知狀態呼叫另一個 SetGameState。

// 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);
}

很簡單吧!整合 Game State API 後,系統就會瞭解應用程式中的狀態,並開始針對資源進行最佳化,為玩家提供更卓越的效能和效率。

敬請密切留意 Game State API 的後續更新,我們將說明如何利用標籤和品質進一步改善遊戲!

6. 整合 Performance Hint API

Performance Hint API 可讓您為每個負責特定工作的執行緒群組建立 Session,向系統傳送效能提示;也可以設定初始目標工作時間長度,然後針對每個影格回報實際工作時間長度,並為下一個影格更新預期的工作時間長度。

舉例來說,假設有一組執行緒負責敵方 AI、物理計算和轉譯執行緒。每個影格都必須完成所有這些子工作,如果其中一個子工作執行超時,就會導致影格延遲,因而無法達到目標每秒影格數。您可以為這組執行緒建立 PerformanceHint Session,並將 targetWorkDuration 設定為所需目標每秒影格數。完成每個影格的工作後執行 reportactualWorkDuration,系統就能分析這個趨勢,據以調整 CPU 資源,確保每個影格都能達到預期目標,進而提升遊戲的影格穩定性和電源消耗效率。

ADPFManager InitializePerformanceHintManager

我們需要為執行緒建立提示工作階段,雖然可以使用 C++ API,但這個 API 僅適用於 API 級別 33 以上版本。如果是 API 級別 31 和 32,就需要使用 Java API,因此要快取一些 JNI 方法供稍後使用。

// 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

}

在 ADPFManager::SetApplication 中呼叫

請記得呼叫從 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);

  ...
}

定義 ADPFManager::BeginPerfHintSession 和 ADPFManager::EndPerfHintSession

請定義 cpp 方法,透過 JNI 實際叫用 API,藉此接受所有必要參數。

// 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

  }
}

在每個影格開始和結束時都呼叫這些方法

針對每個影格,我們需要在影格開始時記錄開始時間,並在影格結束時回報實際時間。我們會在影格結束時呼叫 reportactualWorkDurationupdateTargetWorkDuration。在這個簡單的範例中,工作負載不會在影格之間變更,而 updateTargetWorkDuration 會使用一致的目標值。

// 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. 恭喜

恭喜!您已成功在遊戲中整合適應性功能。

我們將在日後為 Android 適應性架構推出更多新功能,敬請持續關注。