1. 簡介
為什麼需要在遊戲中整合適應性功能?
適應性 API 可讓您在應用程式執行階段期間取得裝置的狀態回饋,並以動態方式調整工作負載,將遊戲效能最佳化。這些 API 也可以向系統告知工作負載,讓系統以最佳方式分配資源。
建構項目
在本程式碼研究室中,您將開啟並執行原生 Android 範例遊戲,然後在遊戲中整合適應性功能。設定及新增必要的程式碼後,您就能針對先前的遊戲與具適應性功能的版本,測試玩家感受到的效能有何不同。
課程內容
- 如何在現有遊戲中整合 Thermal API,透過調整熱力狀態來防止過熱。
- 如何整合 Game Mode API,因應所選模式的變更。
- 如何整合 Game State API,讓系統瞭解遊戲的執行狀態。
- 如何整合 Performance Hint API,讓系統瞭解執行緒模型和工作負載。
軟硬體需求
- Android Studio:Electric Eel 以上版本。
- 可連接至電腦的 Android 裝置,且已啟用開發人員選項和 USB 偵錯功能。您將在這部裝置上執行遊戲。
2. 開始設定
設定開發環境
如果您之前未在 Android Studio 中使用原生專案,可能需要安裝 Android NDK 和 CMake。如已安裝這些項目,請跳至「設定專案」一節。
檢查是否已安裝 SDK、NDK 和 CMake
請啟動 Android Studio。在畫面顯示「Welcome to Android Studio」視窗時,開啟「Configure」下拉式選單,然後選取「SDK Manager」選項。
如果已開啟現有專案,可以改從「Tools」選單開啟 SDK Manager。請按一下「Tools」選單,然後選取「SDK Manager」,SDK Manager 視窗會隨即開啟。
在側欄中,依序選取「Appearance & Behavior」>「System Settings」>「Android SDK」。在 Android SDK 窗格中選取「SDK Platforms」分頁標籤,畫面就會列出已安裝的工具選項。請確認已安裝 Android SDK 12.0 以上版本。
接著選取「SDK Tools」分頁標籤,確認已安裝 NDK 和 CMake。
注意:只要版本不舊皆可,不一定要使用特定版本。我們目前使用的是 NDK 25.2.9519653 和 CMake 3.24.0。隨著較新的 NDK 版本持續推出,預設安裝的 NDK 版本也會變更。如需安裝特定版本的 NDK,請參閱關於安裝 NDK 的 Android Studio 參考資料,按照「安裝特定版本的 NDK」一節指示操作。
勾選所有必要工具後,點選視窗底部的「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'」,測試此試用版。裝置上的最終結果應如下所示:
關於專案
我們刻意將遊戲設計得十分精簡,讓您專注在實作適應性功能的具體細節。這款遊戲會執行一些可輕鬆設定的物理和圖形工作負載,方便我們在執行階段期間,隨著裝置狀況的變化動態調整設定。
3. 整合 Thermal API
在 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.removeThermalStatusListener 和 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);
}
}
請將這些功能移至 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.h 和 adpf_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 java 和 cpp 類別,您就能擷取這些檔案,並在其他專案中重複使用,不需再次編寫膠水程式碼。
// 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 開放原始碼計畫的「效能管理」一文。
請建構並執行專案,畫面上會顯示目前的熱力狀態和熱力上升空間。如果熱力狀態下降,物理步驟和物體數量都會減少。
如果發生錯誤,您可以比較自己的成果與 [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 java 和 cpp 類別,您就能擷取這些檔案,並在其他專案中重複使用,不需再次編寫膠水程式碼。
// 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
存放區中的修訂版本。
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
}
}
在每個影格開始和結束時都呼叫這些方法
針對每個影格,我們需要在影格開始時記錄開始時間,並在影格結束時回報實際時間。我們會在影格結束時呼叫 reportactualWorkDuration 和 updateTargetWorkDuration。在這個簡單的範例中,工作負載不會在影格之間變更,而 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 適應性架構推出更多新功能,敬請持續關注。