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

1. 简介

70748189a60ed450.png

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

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

您将构建什么

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

您将学到什么

  • 如何将热管理 API 集成到现有游戏中,并适应热状态以防止过热。
  • 如何集成游戏模式 API 并对选择更改做出反应。
  • 如何集成游戏状态 API 以让系统知道游戏正在运行的哪个状态。
  • 如何集成性能提示 API 以让系统了解您的线程模型和工作负载。

您需要什么

2. 设置

设置您的开发环境

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

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

启动 Android Studio。显示“欢迎使用 Android Studio”窗口时,打开“配置”下拉菜单并选择“SDK 管理器”选项。

3b7b47a139bc456.png

如果您已打开现有项目,则可以通过“工具”菜单打开 SDK 管理器。点击工具菜单并选择SDK 管理器,SDK 管理器窗口将打开。

在侧边栏中,依次选择:外观 & 行为 > 系统设置 > Android SDK。在 Android SDK 面板中选择SDK 平台选项卡以显示已安装的工具选项列表。确保已安装 Android SDK 12.0 或更高版本。

931f6ae02822f417.png

接下来,选择SDK 工具选项卡并确保已安装NDKCMake

注意:只要版本比较新,确切的版本并不重要,但我们目前使用的是 NDK 25.2.9519653 和 CMake 3.24.0。默认安装的 NDK 版本会随着后续 NDK 版本的发布而发生变化。如果您需要安装特定版本的 NDK,请按照 Android Studio 参考中有关安装 NDK的说明进行操作,该说明位于“安装特定版本的 NDK”部分下。

d28adf9279adec4.png

检查完所有必需的工具后,点击窗口底部的应用按钮进行安装。然后,您可以通过点击确定按钮关闭 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 中,从目录的根目录打开项目。确保已连接设备,然后选择构建 > 构建项目运行 > 运行“app”以测试演示。设备上的最终结果应如下所示

f1f33674819909f1.png

关于项目

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

3. 集成热管理 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 中的所有内容也需要在 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 的实例,并将 add/remove 热监听器与其对应的 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 热管理 API AThermal_*,因此您可以将 Java 监听器映射到调用相同的 C++ 方法。为了让 Java 方法调用 C++ 代码,您需要在 JNI_OnLoad 中注册 C++ 方法。您可以查看更多JNI 提示以了解更多信息。

// CODELAB: android_main.cpp
// Remove the thermal state change listener on pause.
// Register classes to Java.
jint JNI_OnLoad(JavaVM *vm, void * /* reserved */) {
  JNIEnv *env;
  if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
    return JNI_ERR;
  }

  // Find your class. JNI_OnLoad is called from the correct class loader context
  // for this to work.
  jclass c = env->FindClass("com/android/example/games/ADPFManager");
  if (c == nullptr) return JNI_ERR;

  // Register your class' native methods.
  static const JNINativeMethod methods[] = {
      {"nativeThermalStatusChanged", "(I)V",
       reinterpret_cast<void *>(nativeThermalStatusChanged)},
      {"nativeRegisterThermalStatusListener", "()V",
       reinterpret_cast<void *>(nativeRegisterThermalStatusListener)},
      {"nativeUnregisterThermalStatusListener", "()V",
       reinterpret_cast<void *>(nativeUnregisterThermalStatusListener)},
  };
  int rc = env->RegisterNatives(c, methods,
                                sizeof(methods) / sizeof(JNINativeMethod));

  if (rc != JNI_OK) return rc;

  return JNI_VERSION_1_6;
}

将您的原生监听器连接到您的游戏

我们的 C++ 游戏需要知道何时相应的热状态发生了变化,因此让我们在 C++ 中创建相应的 adpf_manager 类。

在应用程序源代码的 cpp 文件夹中($ROOT/app/src/main/cpp)创建一个adpf_manager.hadpf_manager.cpp文件对

// CODELAB: adpf_manager.h
// Forward declarations of functions that need to be in C decl.
extern "C" {
   void nativeThermalStatusChanged(JNIEnv* env, jclass cls, int32_t thermalState);
   void nativeRegisterThermalStatusListener(JNIEnv* env, jclass cls);
   void nativeUnregisterThermalStatusListener(JNIEnv* env, jclass cls);
}

typedef void (*thermalStateChangeListener)(int32_t, int32_t);

在 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 级别 30 及更高版本上,我们可以使用 NDK 热管理 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 调用的。此设置特定于我们的框架,因此如果您将其集成到您的游戏中,则需要找到调用 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 的热管理 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;
}

在 CMakeLists.txt 中将您的 adpf_manager.cpp 包含到编译单元中

请记住将您新创建的 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,
};

在您的游戏中创建一个热状态更改监听器函数

现在,我们需要为 ADPFManager 创建一个 cpp thermalListener,以便在它检测到设备热级别已更改时调用。在您的游戏中创建此函数以监听状态更改的值。我们正在跟踪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. 集成游戏模式 API

游戏模式 API允许您根据玩家的选择优化游戏以获得最佳性能或最长的电池续航时间。它适用于选定的 Android 12 设备和所有 Android 13 及更高版本设备。

更新 Android 清单

设置 appCategory

要使用游戏模式 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 以抽象化游戏模式功能

由于游戏模式 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 中的游戏模式

将其连接到 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 类以存储用户为游戏内检索选择的游戏模式

让我们创建一个 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

根据用户选择的游戏模式调整您的游戏

检索游戏模式后,您必须根据用户选择的值区分您的游戏。最显著的区别(以及最两极分化的值)在于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();

在我们的示例中,我们以全分辨率和 60 FPS 渲染 PERFORMANCE 模式。由于这是一个非常简单的示例,大多数设备都能够以全 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

游戏模式 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. 集成游戏状态 API

游戏状态 API 允许您告诉系统游戏的顶级状态,使您能够判断当前内容是否可以中断而不会干扰无法暂停的游戏玩法。它还允许您指示您的游戏当前是否正在执行繁重的 I/O 操作,例如从磁盘或网络加载资源。了解所有这些数据使系统能够在正确的时间为您分配正确的资源(例如:当您正在加载时分配正确的 CPU 和内存带宽,或者当您的玩家处于关键的多人游戏会话时优先处理多人游戏的网络流量)。

定义枚举以更轻松地映射到 GameState 常量

由于游戏状态 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);
}

很简单吧?集成游戏状态 API 后,系统将了解应用程序中的状态,并开始优化资源以提高玩家的性能和效率。

敬请期待游戏状态 API 的下一次更新,我们将解释如何使用标签和质量进一步优化您的游戏!

6. 集成性能提示 API

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

例如,如果您有一组线程负责敌人 AI、物理计算和渲染线程。所有这些子任务都必须在每一帧完成,其中一个子任务的超载会导致您的帧延迟,错过了目标 FPS。您可以为此组线程创建一个性能提示会话,并将targetWorkDuration设置为所需的 target FPS。当您在每一帧工作完成后reportActualWorkDuration时,系统可以分析此趋势并相应地调整 CPU 资源,以确保您可以在每一帧达到所需的 target。这将提高帧稳定性和游戏的功耗效率。

ADPFManager 初始化 PerformanceHintManager

我们将需要为线程创建提示会话,有一个 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 添加适应性框架中的更多功能。