将适应性功能集成到原生游戏中

1. 简介

70748189a60ed450.png

为什么需要在游戏中集成适应性功能?

适应性 API 允许您在应用运行时获取设备状态反馈,并动态调整您的工作负载以优化游戏的性能。它还允许您将您的工作负载告知系统,以便系统可以最佳地分配资源。

您将构建的内容

在本 Codelab 中,您将打开一个原生 Android 示例游戏,运行它,并将其集成适应性功能。在设置并添加必要的代码后,您将能够测试原始游戏与集成适应性功能版本之间的感知性能差异。

您将学习的内容

  • 如何将 Thermal API 集成到现有游戏中,并根据热状态进行调整以防止过热。
  • 如何集成 Game Mode API 并对选择更改做出反应。
  • 如何集成 Game State API 以让系统知道游戏正在运行的状态。
  • 如何集成 Performance Hint API 以让系统知道您的线程模型和工作负载。

您需要准备的内容

2. 设置

设置您的开发环境

如果您之前没有在 Android Studio 中使用过原生项目,您可能需要安装 Android NDK 和 CMake。如果您已经安装了它们,请继续进行项目设置。

检查 SDK、NDK 和 CMake 是否已安装

启动 Android Studio。当“欢迎使用 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,请按照 Android Studio 参考文档中“安装特定版本的 NDK”部分的说明进行操作,该部分位于安装 NDK下。

d28adf9279adec4.png

检查所有必需的工具后,点击窗口底部的Apply按钮进行安装。然后,您可以点击Ok按钮关闭 Android SDK 窗口。

设置项目

示例项目是一个使用 Swappy for OpenGL 开发的简单 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 ProjectRun > Run ‘app' 来测试演示。设备上的最终结果应如下图所示

f1f33674819909f1.png

关于项目

该游戏有意设计得极简,以便专注于实现适应性功能的细节。它正在运行一些易于配置的物理和图形工作负载,以便我们可以在设备条件发生变化时在运行时动态调整配置。

3. 集成 Thermal API

a7e07127f1e5c37d.png

ce607eb5a2322d6b.png

在 Java 中监听热状态变化

自 Android 10 (API Level 29) 起,Android 设备在热状态发生变化时必须向运行中的应用报告。应用可以通过向 PowerManager 提供 OnThermalStatusChangedListener 来监听此变化。

由于 PowerManager.addThermalStatusListener 仅在 API Level 29 及更高版本上可用,因此我们需要在调用它之前进行检查

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

而在 API Level 30 及更高版本上,您可以使用 AThermal_registerThermalStatusListener 在 C++ 代码中注册回调,因此您可以定义一个原生方法并从 Java 调用它,如下所示

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

您需要在 Activity 的 onResume 生命周期函数中添加监听器。

请记住,您在 Activity 的 onResume 中添加的所有内容也需要在 Activity 的 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 来抽象化,以便我们可以轻松地在其他项目之间重用它们。

在您的游戏 Activity 类中,创建并持有一个 ADPFManager 实例,并将添加/移除热监听器与它们对应的 Activity 生命周期方法关联起来。

// 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++ 类的原生方法

在 API Level 30 及更高版本上,我们可以使用 NDK Thermal API AThermal_*,因此您可以将 Java 监听器映射到调用相同的 C++ 方法。对于 Java 方法调用 C++ 代码,您需要在 JNI_OnLoad 中注册 C++ 方法。您可以查看更多JNI Tips来了解更多信息。

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

在您的应用源文件 ($ROOT/app/src/main/cpp) 的 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);

在 cpp 文件中、ADPFManager 类外部定义 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 和获取热裕度所需的函数

在 API Level 30 及更高版本上,我们可以使用 NDK Thermal API AThermal_*,因此在初始化时,调用 AThermal_acquireManager 并保留以备将来使用。在 API Level 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 调用的。这种设置是我们的框架特有的,因此如果您要集成到您的游戏中,您需要找到合适的位置来调用 Initialize 方法

// 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 Open Source Project 的性能管理中阅读更多关于此内容的信息。

构建并运行您的项目,当前的热状态和热裕度将显示出来,如果热状态恶化,物理步长和对象数量将减少。

4bdcfe567fc603c0.png

如果出现任何问题,您可以将您的工作与标题为 [codelab] step: integrated thermal-api 的仓库提交进行比较。

4. 集成 Game Mode API

Game Mode API 允许您根据玩家的选择优化您的游戏以获得最佳性能或最长电池续航时间。它在选定的 Android 12 设备和所有 Android 13+ 设备上可用。

更新 Android Manifest

设置 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 中对其进行抽象,以便我们可以在其他项目中复用该功能。

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

调整您的 Activity 初始化 GameModeManager 并在 onResume 中检索 GameMode

将其与 Activity 生命周期挂钩。每当 Game Mode 发生变化时,您的 Activity 将重新启动,因此我们可以在 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 供游戏内检索

让我们创建一个 cpp 包装器,并将 Game Mode 值存储在 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 后,您必须根据用户选择的值来区分您的游戏。最显著的差异(也是最两极分化的值)存在于 PERFORMANCE 模式和 BATTERY 模式之间。在 PERFORMANCE 模式下,用户通常希望沉浸在游戏中并获得最佳体验,而无需担心电池续航,因此只要帧率稳定,您可以提供最佳的画面保真度。在 BATTERY 模式下,用户希望玩更长时间的游戏,并且他们能够接受较低的设置。确保帧率始终稳定,因为不稳定的帧率会给玩家带来最糟糕的体验。

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

在我们的示例中,我们在 PERFORMANCE 模式下以全分辨率和 60 FPS 渲染。由于这是一个非常简单的示例,大多数设备都能流畅地以全 FPS 运行,因此为了简单起见,我们没有进行进一步的检查。在 BATTERY 模式下,我们将渲染限制在 30 FPS 和四分之一分辨率。您需要找到自己的最佳点进行优化。请务必记住,游戏体验和省电不仅仅局限于 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 构建文件中的 sdkVersions

Game Mode API 在所有 Android 13 及更高版本的 Android 设备上都可用。部分 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 允许您将游戏的高级状态告知系统,从而使您能够判断当前内容是否可以在不干扰无法暂停的游戏进行时被中断。它还允许您指示您的游戏当前是否正在执行繁重的 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 的下次更新,我们将解释如何使用 label 和 quality 来进一步优化您的游戏!

6. 集成 Performance Hint API

Performance Hint API 允许您通过为负责特定任务的每个线程组创建 Session 来向系统发送性能提示,设置初始目标工作时长,然后在每一帧报告实际工作时长并更新下一帧的预期工作时长。

例如,如果您有一组负责敌方 AI、物理计算和渲染线程的线程。所有这些子任务都必须在每一帧中完成,其中任何一个超时都将导致您的帧延迟,达不到目标 FPS。您可以为这组线程创建一个 PerformanceHint Session,并将 targetWorkDuration 设置为您期望的目标 FPS。在完成每一帧的工作后,当您 reportActualWorkDuration 时,系统可以分析此趋势并相应地调整 CPU 资源,以确保您在每一帧中都能达到期望的目标。这使得您的游戏的帧稳定性得到改善,并且功耗更有效率。

ADPFManager InitializePerformanceHintManager

我们需要为线程创建提示会话,有 C++ API,但它仅适用于 API Level 33 及更高版本。对于 API Level 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。在我们的简单示例中,工作负载在帧之间没有变化,我们将使用一致的目标值更新 targetWorkDuration。

// 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 添加更多适应性框架的功能。