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

1. 简介

70748189a60ed450.png

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

借助 Adability API,您可以在应用运行时获取设备状态反馈,并动态调整工作负载以优化游戏性能。您还可以通过此类 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。当系统显示“Welcome to Android Studio”窗口时,打开“Configure”下拉菜单,然后选择“SDK Manager”选项。

3b7b47a139bc456.png

如果您已打开现有项目,则可以通过“Tools”菜单打开 SDK 管理器。点击 Tools 菜单,然后选择 SDK Manager,即可打开 SDK 管理器窗口。

在边栏中依次选择: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 窗口。

设置项目

示例项目是使用适用于 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 ProjectRun > Run 'app' 以测试演示版。设备上的最终结果应如下所示:

f1f33674819909f1.png

项目简介

这款游戏刻意采用极简设计风格,有助您重点关注实现自适应功能的具体细节。它运行了一些易于配置的物理特性和图形工作负载,因此当设备状况发生变化时,我们可以在运行时动态调整配置。

3. 集成 Thermal API

a7e07127f1e5c37d.png

ce607eb5a2322d6b.png

监听 Java 中的热状态变化

从 Android 10(API 级别 29)开始,每当热状态发生变化时,Android 设备都必须向正在运行的应用报告。应用可以通过向 PowerManager 提供 OnThermalStatusChangedListener 来监听此类变化。

由于 PowerManager.addThermalStatusListener 仅适用于 API 级别 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 级别 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 中添加的所有内容也需要从 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 级别 30 及更高版本中,可以使用 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 类。

在应用源 ($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);

在 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 和检索热余量所需的函数

在 API 级别 30 及更高版本中,可以使用 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;
}

确保系统会调用初始化方法

在示例中,系统会先从 android_main 调用 SetApplication,再从 SetApplication 调用初始化方法。此设置专门针对我们的框架,因此,如果您要整合到您的游戏中,就需要找到调用初始化方法的正确位置。

// 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 中提取它,以便在其他项目中重复使用该功能。

// 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 生命周期。每当游戏模式发生变化时,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 封装容器,并将游戏模式值存储在 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 值传递给游戏

这是最简单、最高效的方式。在启动时检索游戏模式值并将其传递给 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 调整游戏

检索游戏模式后,您必须根据用户选择的值调整游戏。高性能模式和省电模式之间的差异最显著,值的分化也最明显。在高性能模式下,用户通常希望沉浸到游戏中,获得绝佳体验,而不必担心电池续航时间,因此只要帧速率稳定,就可以提供最佳保真度。在省电模式下,用户希望延长游戏时间,可以接受更低的设置。请确保帧速率始终稳定,因为帧速率不稳定会给玩家带来糟糕的体验。

// 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 运行,因此为简单起见,我们不会做进一步的检查。在省电模式下,我们将渲染的上限设置为 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 build 文件中的 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,您可以向系统告知游戏的顶级状态,让系统了解当前内容是否可以中断,而不会干扰无法暂停的游戏。此外,它还可以指明游戏当前是否正在执行大量 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,
};

在通过 JNI 调用 Java API 的 cpp 代码中定义 SetGameState

只需用 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,您可以通过为负责特定任务的每个线程组创建会话来向系统发送性能提示,还可以设置初始目标工作时长,然后在每个帧上报告实际工作时长,并更新下一帧的预期工作时长。

例如,假设您有一组负责敌对 AI、物理计算和渲染线程的线程。所有这些子任务都必须在每一帧中完成,如果其中有任何一项子任务过载,便会导致帧延迟,无法达到目标 FPS。您可以为这组线程创建一个 PerformanceHint 会话,并将 targetWorkDuration 设置为所需的目标 FPS。在每一帧上完成作业后,系统可通过 reportactualWorkDuration分析此趋势并相应地调整 CPU 资源,以确保可在每一帧上实现期望的目标。这样可以提高游戏的帧稳定性并提高功耗效率。

ADPFManager InitializePerformanceHintManager

我们需要为线程创建提示会话,可使用 C++ 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。在这个简单示例中,工作负载不会在各个帧之间改变,而是使用一致的目标值来更新 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 推出更多自适应框架功能,敬请期待。