Mengintegrasikan fitur Adaptasi ke dalam Game Native Anda

1. Pengantar

70748189a60ed450.png

Mengapa saya perlu mengintegrasikan fitur Adaptasi ke dalam game saya?

Adaptability API memungkinkan Anda mendapatkan masukan informasi status perangkat selama runtime aplikasi dan menyesuaikan beban kerja secara dinamis untuk mengoptimalkan performa game. API ini juga memungkinkan Anda memberi tahu sistem tentang beban kerja Anda agar sistem dapat mengalokasikan resource secara optimal.

Yang akan Anda bangun

Dalam codelab ini, Anda akan membuka game contoh Android native, menjalankannya, dan mengintegrasikan fitur Adaptasi dengan game tersebut. Setelah menyiapkan dan menambahkan kode yang diperlukan, Anda dapat menguji perbedaan performa yang dirasakan antara game sebelumnya dan versi dengan fitur Adaptasi.

Yang akan Anda pelajari

  • Cara mengintegrasikan Thermal API ke dalam game yang sudah ada & beradaptasi dengan status termal untuk mencegah panas berlebih.
  • Cara mengintegrasikan Game Mode API & merespons perubahan pilihan.
  • Cara mengintegrasikan Game State API untuk memberi tahu sistem tentang status pengoperasian game.
  • Cara mengintegrasikan Performance Hint API untuk memberi tahu sistem tentang model threading dan beban kerja Anda.

Yang akan Anda butuhkan

2. Mempersiapkan

Menyiapkan lingkungan pengembangan Anda

Jika belum pernah menggunakan project native di Android Studio, Anda mungkin perlu menginstal Android NDK dan CMake. Jika Anda sudah menginstalnya, lanjutkan ke Menyiapkan project.

Memastikan SDK, NDK, dan CMake telah diinstal

Luncurkan Android Studio. Saat jendela Welcome to Android Studio ditampilkan, buka menu dropdown Konfigurasi lalu pilih opsi SDK Manager.

3b7b47a139bc456.png

Jika sudah membuka project, Anda dapat membuka SDK Manager melalui menu Alat. Klik menu Tools lalu pilih SDK Manager. Jendela SDK Manager akan terbuka.

Pada sidebar, pilih secara berurutan: Appearance & Behavior > System Settings > Android SDK. Pilih tab SDK Platforms di panel Android SDK untuk menampilkan daftar opsi alat yang terinstal. Pastikan Android SDK 12.0 atau yang lebih baru telah diinstal.

931f6ae02822f417.png

Selanjutnya, pilih tab SDK Tools, lalu pastikan NDK dan CMake telah diinstal.

Catatan: Versi tepatnya tidak terlalu penting selama versi tersebut cukup baru, tetapi saat ini kita menggunakan NDK 25.2.9519653 dan CMake 3.24.0. Versi NDK yang terinstal akan berubah secara default seiring waktu dengan rilis NDK berikutnya. Jika Anda perlu menginstal versi NDK tertentu, ikuti petunjuk dalam referensi Android Studio untuk menginstal NDK di bagian "Menginstal versi NDK tertentu".

d28adf9279adec4.png

Setelah semua alat yang diperlukan dicentang, klik tombol Apply di bagian bawah jendela untuk menginstalnya. Anda dapat menutup jendela Android SDK dengan mengklik tombol Ok.

Menyiapkan project

Project contoh adalah game simulasi fisika 3D sederhana yang dikembangkan dengan Swappy for OpenGL. Tidak ada banyak perubahan dalam struktur direktori dibandingkan dengan project baru yang dibuat dari template, tetapi ada beberapa tugas yang harus dilakukan untuk menginisialisasi fisika dan loop rendering, jadi lanjutkan dan clone repo sebagai gantinya.

Meng-clone repo

Dari command line, ubah ke direktori yang ingin Anda sertakan direktori game root-nya, lalu clone dari GitHub:

git clone -b codelab/start https://github.com/android/adpf-game-adaptability-codelab.git --recurse-submodules

Pastikan Anda memulai dari commit awal repo yang bernama [codelab] start: simple game.

Menyiapkan dependensi

Project contoh menggunakan library Dear ImGui untuk antarmuka penggunanya. Aplikasi ini juga menggunakan Bullet Physics untuk simulasi fisika 3D. Library ini diasumsikan ada di direktori third_party yang berada di root project. Kami telah memeriksa library masing-masing melalui --recurse-submodules yang ditentukan dalam perintah clone di atas.

Menguji project

Di Android Studio, buka project dari root direktori. Pastikan perangkat terhubung, lalu pilih Build > Make Project dan Run > Run 'app' untuk menguji demo. Hasil akhir di perangkat akan terlihat seperti ini

f1f33674819909f1.png

Tentang project

Game sengaja dibuat minimalis agar berfokus pada detail penerapan fitur Adaptasi. Game ini menjalankan beberapa beban kerja fisika dan grafis yang mudah dikonfigurasi sehingga kita dapat menyesuaikan konfigurasi secara dinamis selama runtime saat kondisi perangkat berubah.

3. Mengintegrasikan Thermal API

a7e07127f1e5c37d.png

ce607eb5a2322d6b.png

Memproses Status Termal yang diubah di Java

Mulai Android 10 (Level API 29), perangkat Android harus melaporkan ke aplikasi yang sedang berjalan setiap kali status termal berubah. Aplikasi dapat memproses perubahan ini dengan memberikan OnThermalStatusChangedListener ke PowerManager.

Karena PowerManager.addThermalStatusListener hanya tersedia di Level API 29 dan seterusnya, kita perlu memeriksa sebelum memanggilnya:

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

Di Level API 30 dan seterusnya, Anda juga dapat mendaftarkan callback dalam kode C++ menggunakan AThermal_registerThermalStatusListener sehingga Anda dapat menentukan metode native dan memanggilnya dari Java seperti ini:

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

Anda perlu menambahkan pemroses dalam fungsi siklus proses onResume Aktivitas Anda.

Ingatlah bahwa semua yang Anda tambahkan ke onResume Aktivitas juga harus dihapus dalam onPause Aktivitas Anda. Jadi, mari kita tentukan kode pembersihan untuk memanggil PowerManager.removeThermalStatusListener dan 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);
   }
}

Mari kita pisahkan fungsi ini dengan memindahkannya ke ADPFManager.java sehingga kita dapat menggunakannya kembali di seluruh project lain dengan mudah.

Di class aktivitas game, buat dan tahan instance ADPFManager lalu hubungkan pemroses termal tambahkan/hapus dengan metode siklus proses aktivitas yang sesuai.

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

Mendaftarkan metode native class C++ Anda di JNI_OnLoad

Di Level API 30 dan seterusnya, kita dapat menggunakan NDK Thermal API AThermal_* sehingga Anda dapat memetakan pemroses Java untuk memanggil metode C++ yang sama. Agar metode Java dapat memanggil kode C++, Anda harus mendaftarkan metode C++ dalam JNI_OnLoad. Anda dapat melihat Tip JNI lainnya untuk mempelajarinya lebih lanjut.

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

Menghubungkan pemroses native ke game Anda

Game C++ kita perlu mengetahui kapan status termal yang sesuai telah berubah. Jadi, mari kita buat class adpf_manager yang sesuai di C++.

Di folder cpp sumber aplikasi Anda ($ROOT/app/src/main/cpp), buat pasangan file adpf_manager.h dan 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);

Tentukan fungsi C dalam file cpp di luar class ADPFManager.

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

Melakukan inisialisasi PowerManager dan fungsi yang diperlukan untuk mengambil headroom termal

Di Level API 30 dan seterusnya, kita dapat menggunakan NDK Thermal API AThermal_* sehingga pada inisialisasi, panggil AThermal_acquireManager dan simpan untuk digunakan pada masa mendatang. Di Level API 29, kita perlu menemukan referensi Java yang diperlukan dan mempertahankannya.

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

Memastikan metode inisialisasi akan dipanggil

Dalam contoh kami, metode inisialisasi dipanggil dari SetApplication, dan SetApplication dipanggil dari android_main. Penyiapan ini khusus untuk framework kami. Jadi, jika Anda mengintegrasikan ke dalam game, Anda perlu menemukan tempat yang tepat untuk memanggil metode Inisialisasi

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

Memantau headroom termal secara berkala

Lebih baik mencegah status termal dinaikkan ke level yang lebih tinggi karena akan sulit untuk menurunkannya tanpa menjeda beban kerja sepenuhnya. Bahkan setelah dimatikan sepenuhnya, perangkat akan membutuhkan waktu untuk menghilangkan panas dan melakukan pendinginan. Kita dapat melihat headroom termal secara berkala dan menyesuaikan beban kerja agar headroom tetap terjaga dan mencegah status termal dinaikkan.

Di ADPFManager, mari kita ekspos metode untuk memeriksa headroom termal.

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

Terakhir, kita perlu mengekspos metode untuk menetapkan status termal dan pemrosesnya. Kita akan mendapatkan nilai status termal dari API termal NDK atau Java SDK yang memanggil kode native kita.

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

Sertakan adpf_manager.cpp ke unit kompilasi di CMakeLists.txt

Jangan lupa menambahkan adpf_manager.cpp yang baru dibuat ke dalam unit kompilasi Anda.

Sekarang kita telah selesai dengan class java dan cpp ADPFManager yang dapat digunakan kembali sehingga Anda dapat mengambil semua file ini dan menggunakannya kembali di project lain, bukan menulis ulang kode glue lagi.

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

Menentukan parameter dalam game agar berubah saat status termal menurun

Bagian ini dan seterusnya adalah khusus game. Dalam contoh ini, kita akan mengurangi langkah-langkah fisika dan jumlah kotak setiap kali status termal dinaikkan.

Kita juga akan memantau headroom termal, tetapi tidak akan melakukan apa pun selain menampilkan nilai pada HUD. Di game, Anda dapat bereaksi terhadap nilai dengan menyesuaikan jumlah pasca-pemrosesan yang dilakukan oleh kartu grafis untuk mengurangi tingkat detail, dll.

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

Membuat fungsi pemroses status termal yang berubah dalam game Anda

Sekarang, kita perlu membuat thermalListener cpp untuk ADPFManager yang akan dipanggil setiap kali mendeteksi bahwa level termal perangkat telah berubah. Buat fungsi ini dalam game Anda untuk memproses nilai status yang berubah. Kita akan melacak last_state sehingga kita dapat mengetahui apakah level termal naik atau turun.

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

Saat status termal berubah di game Anda, sesuaikan

Setiap game akan memiliki kebutuhan dan prioritas yang berbeda. Hal yang sangat penting untuk salah satu game mungkin tidak penting bagi game lainnya. Jadi, Anda harus memutuskan cara mengoptimalkannya agar tidak terjadi pemanasan lebih jauh.

Dalam contoh kami, kami mengurangi jumlah objek di layar dan mengurangi fidelitas fisika. Tindakan ini akan meringankan beban kerja CPU & GPU, dan diharapkan akan sedikit menurunkan termal. Perlu diketahui bahwa biasanya akan sulit menurunkan termal, kecuali pemutar berhenti sejenak dan membuat perangkat menjadi dingin. Inilah alasan kami memantau headroom termal dengan cermat dan mencegah perangkat mencapai status throttle termal.

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

Ingat juga bahwa performa puncak yang diberikan oleh chip CPU & GPU biasanya tidak efisien. Artinya, chip biasanya memberikan performa maksimum dengan konsumsi energi yang jauh lebih tinggi dan menghilangkan banyak panas. Berbeda dengan performa berkelanjutan, yang merupakan performa paling optimal per unit energi yang dikonsumsi dan panas yang dibuang. Anda dapat membaca hal ini lebih lanjut di Pengelolaan Performa Project Open Source Android.

Bangun dan jalankan project Anda. Status Termal dan Headroom Termal saat ini akan ditampilkan, dan jika status termal menurun, langkah-langkah fisika dan jumlah objek akan dikurangi.

4bdcfe567fc603c0.png

Jika terjadi error, Anda dapat membandingkan pekerjaan Anda dengan commit repo yang bernama [codelab] step: integrated thermal-api.

4. Mengintegrasikan Game Mode API

Game Mode API memungkinkan Anda mengoptimalkan game untuk mendapatkan performa terbaik atau masa pakai baterai yang paling lama, sesuai pilihan pemain. Fitur ini tersedia di perangkat Android 12 tertentu, serta semua perangkat Android 13 dan yang lebih baru.

Mengupdate Manifes Android

Menetapkan appCategory

Untuk menggunakan Game Mode API, aplikasi Anda harus berkategori game. Mari kita tunjukkan hal tersebut di tag <application> Anda

// CODELAB: AndroidManifest.xml
<application
   android:appCategory="game">

Untuk Android 13 dan yang lebih baru

Sebaiknya Anda menargetkan pengguna Android 13 dengan mengikuti 2 langkah berikutnya:

Menambahkan game_mode_config <meta-data> dan file xml yang sesuai

// 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" />

Jika Anda menargetkan perangkat Android 12

Menambahkan setiap gamemode <meta-data> langsung di AndroidManifest

// 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"/>

Menerapkan GameModeManager.java untuk memisahkan fitur GameMode

Karena Game Mode API belum memiliki antarmuka cpp, kita perlu menggunakan antarmuka Java dan menyediakan antarmuka JNI. Mari kita pisahkan di GameModeManager.java agar kita dapat menggunakan kembali fungsi tersebut di project lain.

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

Menyesuaikan Aktivitas Anda untuk melakukan inisialisasi GameModeManager dan mengambil GameMode di onResume

Kaitkan ke siklus proses Aktivitas. Setiap kali Mode Game diubah, Aktivitas Anda akan dimulai ulang sehingga kita dapat mengambil nilai selama 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();
}

Mengimplementasikan class GameModeManager guna menyimpan gameMode yang dipilih pengguna untuk pengambilan dalam game

Mari kita buat wrapper cpp dan menyimpan nilai Mode Game dalam cpp agar mudah diambil.

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

Mengimplementasikan retrieveGameMode dalam kode native untuk meneruskan nilai GameMode ke game Anda

Ini adalah cara yang paling sederhana dan efisien. Saat memulai, ambil nilai Mode Game dan teruskan ke variabel cpp Anda untuk akses mudah. Kita dapat mengandalkan nilai yang di-cache tanpa perlu melakukan panggilan JNI setiap saat.

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

}

Menyertakan game_mode_manager.cpp ke unit kompilasi di CMakeLists.txt

Jangan lupa menambahkan game_mode_manager.cpp yang baru dibuat ke unit kompilasi Anda.

Sekarang kita telah selesai dengan class java dan cpp GameModeManager yang dapat digunakan kembali sehingga Anda dapat mengambil semua file ini dan menggunakannya kembali di project lain, bukan menulis ulang kode glue lagi.

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

Menyesuaikan game Anda dengan GameMode yang dipilih pengguna

Setelah mengambil Mode Game, Anda harus membedakan game sesuai dengan nilai yang dipilih pengguna. Perbedaan yang paling signifikan (dan nilai yang paling membedakan) terletak antara mode PERFORMA dan mode BATERAI. Dalam mode PERFORMA, pengguna biasanya ingin menikmati game dan mendapatkan pengalaman terbaik tanpa perlu khawatir dengan masa pakai baterai. Dengan begitu, Anda dapat menemukan fidelitas terbaik selama kecepatan frame stabil. Dalam mode BATERAI, pengguna ingin bermain game lebih lama dan mereka bisa menerima setelan yang lebih rendah. Pastikan kecepatan frame selalu stabil karena kecepatan frame yang tidak stabil memberikan pengalaman terburuk bagi pemain.

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

Dalam contoh kita, kami merender pada resolusi penuh dan 60 FPS dalam mode PERFORMA. Karena ini adalah contoh yang cukup sederhana, sebagian besar perangkat akan dapat berjalan pada FPS penuh dengan lancar sehingga kami tidak menerapkan pemeriksaan lebih lanjut, agar semua langkah tetap sederhana. Dalam mode BATERAI, kami membatasi rendering pada 30 FPS dan resolusi seperempat. Anda harus menemukan titik yang tepat untuk dioptimalkan. Selalu ingat bahwa pengalaman bermain game dan penghematan daya tidak terbatas pada FPS dan resolusi saja. Untuk mendapatkan inspirasi terkait cara mengoptimalkan, lihat kisah sukses developer kami.

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

  ...
}

Jangan lupa memeriksa sdkVersions Anda di file build gradle

Game Mode API tersedia di semua perangkat Android mulai Android 13. Perangkat Android 12 tertentu juga akan mengaktifkan fitur ini.

// 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
        ...
    }

Jika terjadi error, Anda dapat membandingkan pekerjaan Anda dengan commit repo yang bernama [codelab] step: integrate game-mode-api.

496c76415d12cbed.png

5. Mengintegrasikan Game State API

Game State API memungkinkan Anda memberi tahu sistem tentang status game teratas sehingga Anda dapat mengetahui apakah konten saat ini dapat terganggu, tanpa mengganggu gameplay yang tidak dapat dijeda. FItur ini juga memungkinkan Anda menunjukkan apakah game Anda saat ini melakukan I/O yang berat seperti memuat aset dari disk atau jaringan. Dengan mengetahui semua data ini, sistem dapat mengalokasikan resource yang tepat pada waktu yang tepat bagi Anda (misalnya: mengalokasikan jumlah bandwidth CPU & memori yang tepat saat Anda memuat atau memprioritaskan traffic jaringan untuk game multiplayer Anda, ketika pemain berada di sesi multiplayer yang penting ).

Menentukan enum untuk memudahkan pemetaan ke konstanta GameState

Karena Game State API tidak memiliki antarmuka cpp, mari kita mulai dengan menyalin nilai ke 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,
};

Menentukan SetGameState dalam kode cpp yang akan memanggil Java API melalui JNI

Yang kita perlukan hanyalah fungsi cpp untuk meneruskan semua informasi ke Java API.

// CODELAB: game_mode_manager.h
void SetGameState(bool is_loading, GAME_STATE_DEFINITION game_state);

Jangan merasa kewalahan, ini hanyalah satu panggilan 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);
  }
}

Memanggil SetGameState setiap kali status gameplay Anda berubah

Cukup panggil fungsi cpp kami pada waktu yang tepat seperti mulai memuat, berhenti memuat, atau masuk dan keluar dari berbagai status dalam game Anda.

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

Anda dapat meneruskan UNKNOWN jika Anda tidak mengetahui status pada saat itu. Dalam kasus ini, kita menghapus muatan scene dan tidak tahu ke mana tujuan berikutnya pengguna, tetapi scene berikutnya akan segera dimuat, dan kita dapat memanggil SetGameState lain dengan status baru yang diketahui.

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

Mudah, kan? Setelah Anda mengintegrasikan Game State API, sistem akan mengetahui status dalam aplikasi Anda dan mulai mengoptimalkan resource untuk mencapai performa & efisiensi yang lebih baik bagi pemain.

Nantikan pembaruan berikutnya untuk Game State API karena kami akan menjelaskan cara menggunakan label dan kualitas untuk lebih mengoptimalkan game Anda.

6. Mengintegrasikan Performance Hint API

Performance Hint API memungkinkan Anda mengirimkan petunjuk performa ke sistem dengan membuat Sesi untuk setiap grup thread yang bertanggung jawab atas tugas tertentu, menetapkan durasi kerja target awal, lalu pada setiap frame melaporkan durasi kerja sebenarnya dan memperbarui perkiraan durasi kerja untuk frame berikutnya.

Misalnya, jika Anda memiliki sekelompok thread yang bertanggung jawab atas AI musuh, penghitungan fisika, dan thread render. Semua sub-tugas ini harus diselesaikan setiap frame, dan kelebihan beban di salah satunya akan menyebabkan frame tertunda, sehingga kehilangan FPS target. Anda dapat membuat PerformanceHint Session untuk grup thread ini dan menetapkan targetWorkDuration ke FPS target yang diinginkan. Saat Anda reportAktualWorkDuration setelah pekerjaan dilakukan pada setiap frame, sistem dapat menganalisis tren ini dan menyesuaikan resource CPU untuk memastikan Anda dapat mencapai target yang diinginkan pada setiap frame. Hal ini menghasilkan stabilitas frame yang lebih baik dan konsumsi daya yang lebih efisien untuk game Anda.

InitializePerformanceHintManager ADPFManager

Kita perlu membuat sesi petunjuk untuk thread. Ada API C++, tetapi hanya tersedia untuk Level API 33 dan seterusnya. Untuk Level API 31 & 32, kita harus menggunakan API Java. Mari kita simpan di cache beberapa metode JNI yang dapat kita gunakan nanti.

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

}

Memanggil di ADPFManager::SetApplication

Jangan lupa memanggil fungsi inisialisasi yang telah kita tentukan dari 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);

  ...
}

Menentukan ADPFManager::BeginPerfHintSession & ADPFManager::EndPerfHintSession

Tentukan metode cpp untuk benar-benar memanggil API melalui JNI, menerima semua parameter yang diperlukan.

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

  }
}

Memanggil di awal dan akhir setiap frame

Pada setiap frame, kita harus mencatat waktu mulai di awal frame, dan melaporkan waktu sebenarnya di akhir frame. Kita akan memanggil reportActualWorkDuration dan updateTargetWorkDuration di akhir frame. Dalam contoh kita yang sederhana, beban kerja tidak berubah di antara frame. Kita akan mengupdateTargetWorkDuration dengan nilai target yang konsisten.

// 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. Selamat

Selamat, Anda telah berhasil mengintegrasikan fitur Adaptasi ke game.

Nantikan kabar terbarunya karena kami akan menambahkan lebih banyak fitur dari framework Adaptasi ke Android.