1. 简介
为什么我的游戏中需要集成自适应功能?
自适应API允许您在应用程序运行时获取有关设备状态的反馈,并允许您动态调整工作负载以优化游戏的性能。它还允许您向系统告知您的工作负载,以便系统可以最佳地分配资源。
您将构建什么
在这个代码实验室中,您将打开一个原生Android示例游戏,运行它,并将其与自适应功能集成。设置并添加必要的代码后,您将能够测试之前版本的游戏和具有自适应功能的版本在感知性能上的差异。
您将学习什么
- 如何将Thermal API集成到现有游戏中,并适应散热状态以防止过热。
- 如何集成游戏模式API并对选择更改做出反应。
- 如何集成游戏状态API以告知系统游戏正在运行的状态。
- 如何集成性能提示API以告知系统您的线程模型和工作负载。
您需要什么
- Android Studio Electric Eel 或更高版本。
- 已连接到计算机的Android设备,已启用开发者选项和USB调试。您将在该设备上运行游戏。
2. 设置
设置您的开发环境
如果您以前没有在Android Studio中使用过原生项目,则可能需要安装Android NDK和CMake。如果您已经安装了它们,请继续进行项目的设置。
检查SDK、NDK和CMake是否已安装
启动Android Studio。显示“欢迎使用Android Studio”窗口时,打开“配置”下拉菜单并选择“SDK Manager”选项。
如果您已经打开了一个现有项目,则可以通过“工具”菜单打开SDK Manager。单击**工具**菜单,然后选择**SDK Manager**,SDK Manager窗口将打开。
在侧边栏中,依次选择:**外观与行为 > 系统设置 > Android SDK**。选择Android SDK窗格中的**SDK Platforms**选项卡以显示已安装工具选项的列表。确保已安装Android SDK 12.0或更高版本。
接下来,选择**SDK Tools**选项卡,并确保已安装**NDK**和**CMake**。
注意:只要版本比较新,精确版本并不重要,但我们目前使用的是NDK 25.2.9519653和CMake 3.24.0。默认安装的NDK版本会随着后续NDK版本的发布而改变。如果您需要安装特定版本的NDK,请按照Android Studio参考中有关安装NDK的说明进行操作,请参阅“**安装特定版本的NDK**”部分。
检查所有必需的工具后,单击窗口底部的**应用**按钮以安装它们。然后,您可以通过单击**确定**按钮关闭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”**以测试演示。设备上的最终结果应如下所示
关于项目
该游戏故意简化,以专注于实现自适应功能的细节。它运行一些易于配置的物理和图形工作负载,以便我们可以在运行时根据设备条件动态调整配置。
3. 集成Thermal API
在Java中监听Thermal Status更改
从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.removeThermalStatusListener和AThermal_unregisterThermalStatusListener
// CODELAB: ADPFSampleActivity.java onPause
// unregister the listener when it is no longer needed
// Remove the thermal state change listener on pause.
if (Build.VERSION.SDK_INT >= VERSION_CODES.R) {
// Use NDK Thermal API.
nativeUnregisterThermalStatusListener();
} else if (Build.VERSION.SDK_INT >= VERSION_CODES.Q) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm != null) {
pm.removeThermalStatusListener(this);
}
}
让我们通过将它们移动到ADPFManager.java来抽象这些功能,以便我们可以轻松地在其他项目中重用它们。
在游戏的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类。
在应用程序源代码的cpp文件夹($ROOT/app/src/main/cpp)中创建一对adpf_manager.h和adpf_manager.cpp文件
// CODELAB: adpf_manager.h
// Forward declarations of functions that need to be in C decl.
extern "C" {
void nativeThermalStatusChanged(JNIEnv* env, jclass cls, int32_t thermalState);
void nativeRegisterThermalStatusListener(JNIEnv* env, jclass cls);
void nativeUnregisterThermalStatusListener(JNIEnv* env, jclass cls);
}
typedef void (*thermalStateChangeListener)(int32_t, int32_t);
在ADPFManager类之外的cpp文件中定义C函数。
// CODELAB: adpf_manager.cpp
// Native callback for thermal status change listener.
// The function is called from Activity implementation in Java.
void nativeThermalStatusChanged(JNIEnv *env, jclass cls, jint thermalState) {
ALOGI("Thermal Status updated to:%d", thermalState);
ADPFManager::getInstance().SetThermalStatus(thermalState);
}
void nativeRegisterThermalStatusListener(JNIEnv *env, jclass cls) {
auto manager = ADPFManager::getInstance().GetThermalManager();
if (manager != nullptr) {
auto ret = AThermal_registerThermalStatusListener(manager, thermal_callback,
nullptr);
ALOGI("Thermal Status callback registered to:%d", ret);
}
}
void nativeUnregisterThermalStatusListener(JNIEnv *env, jclass cls) {
auto manager = ADPFManager::getInstance().GetThermalManager();
if (manager != nullptr) {
auto ret = AThermal_unregisterThermalStatusListener(
manager, thermal_callback, nullptr);
ALOGI("Thermal Status callback unregistered to:%d", ret);
}
}
初始化PowerManager和检索散热裕量的所需函数
在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;
}
确保调用初始化方法
在我们的示例中,初始化方法是从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;
}
将您的adpf_manager.cpp添加到CMakeLists.txt中的编译单元
记住将您新创建的adpf_manager.cpp添加到您的编译单元中。
我们已经完成了可复用的 ADPFManager java 和 cpp 类,所以您可以获取这些文件并在其他项目中重用它们,而无需再次重写粘合代码。
// CODELAB: CMakeLists.txt
# now build app's shared lib
add_library(game SHARED
adpf_manager.cpp # add this line
android_main.cpp
box_renderer.cpp
demo_scene.cpp
定义游戏中在热状态恶化时需要更改的参数
从这一部分开始将是游戏特定的。在我们的示例中,每当热状态升高时,我们将减少物理步骤和盒子的数量。
我们还将监控热裕量,但除了在 HUD 上显示值之外,我们不会做任何其他事情。在您的游戏中,您可以通过调整显卡执行的后处理量、降低细节级别等来响应该值。
// CODELAB: demo_scene.cpp
// String labels that represents thermal states.
const char* thermal_state_label[] = {
"THERMAL_STATUS_NONE", "THERMAL_STATUS_LIGHT",
"THERMAL_STATUS_MODERATE", "THERMAL_STATUS_SEVERE",
"THERMAL_STATUS_CRITICAL", "THERMAL_STATUS_EMERGENCY",
"THERMAL_STATUS_SHUTDOWN"};
const int32_t thermal_state_physics_steps[] = {
16, 12, 8, 4,
};
const int32_t thermal_state_array_size[] = {
8, 6, 4, 2,
};
在您的游戏中创建一个热状态更改侦听器函数
现在,我们需要为 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 开源项目的性能管理 中了解更多相关信息。
构建并运行您的项目,将显示当前的热状态和热裕量,如果热状态恶化,则物理步骤和对象数量将减少。
如果出现任何问题,您可以将您的工作与存储库中标题为 [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);
}
}
在 CMakeLists.txt 中将您的 game_mode_manager.cpp 包含到编译单元中
记住将您新创建的 game_mode_manager.cpp 添加到您的编译单元中。
我们已经完成了可复用的 GameModeManager java 和 cpp 类,所以您可以获取这些文件并在其他项目中重用它们,而无需再次重写粘合代码。
// CODELAB: CMakeLists.txt
# now build app's shared lib
add_library(game SHARED
game_mode_manager.cpp # add this line
android_main.cpp
box_renderer.cpp
demo_scene.cpp
根据用户选择的游戏模式调整您的游戏
检索游戏模式后,您必须根据用户选择的值区分您的游戏。最显著的差异(以及最两极分化的值)在于 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
游戏模式 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
的提交进行比较。
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。您可以为此组线程创建一个PerformanceHint Session,并将targetWorkDuration设置为您的目标FPS。当您在每一帧工作完成后reportActualWorkDuration时,系统可以分析此趋势并相应地调整CPU资源,以确保您每一帧都能达到目标。这将提高帧稳定性,并提高游戏的功耗效率。
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
}
}
在每一帧的开始和结束时调用它们
在每一帧中,我们需要在帧开始时记录开始时间,并在帧结束时报告实际时间。我们将在帧结束时同时调用reportActualWorkDuration和updateTargetWorkDuration。在我们的简单示例中,我们的工作负载在帧之间不会改变,我们将使用一致的目标值更新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添加更多来自自适应框架的功能。