Intégrer des fonctionnalités d'adaptabilité dans votre jeu natif

1. Introduction

70748189a60ed450.png

Pourquoi dois-je intégrer des fonctionnalités d'adaptabilité dans mon jeu ?

Les API d'adaptabilité vous permettent d'obtenir des retours sur l'état de l'appareil pendant l'exécution de votre application et d'ajuster de manière dynamique votre charge de travail pour optimiser les performances de votre jeu. Vous pouvez également informer le système de votre charge de travail afin qu'il puisse allouer les ressources de manière optimale.

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez ouvrir un exemple de jeu Android natif, l'exécuter et y intégrer des fonctionnalités d'adaptabilité. Après avoir configuré et ajouté le code nécessaire, vous pourrez tester la différence de performances perçues entre le jeu précédent et la version avec les fonctions d'adaptabilité.

Points abordés

  • Comment intégrer l'API Thermal dans un jeu existant et s'adapter à l'état thermique pour éviter la surchauffe.
  • Comment intégrer l'API Game Mode et réagir aux changements de sélection.
  • Comment intégrer l'API Game State pour indiquer au système dans quel état le jeu s'exécute.
  • Comment intégrer l'API Performance Hint pour indiquer au système votre modèle de thread et votre charge de travail.

Ce dont vous aurez besoin

2. Configuration

Configurer votre environnement de développement

Si vous n'avez jamais travaillé avec des projets natifs dans Android Studio, vous devrez peut-être installer le NDK Android et CMake. Si vous les avez déjà installés, passez à la configuration du projet.

Vérifier que le SDK, le NDK et CMake sont installés

Lancez Android Studio. Lorsque la fenêtre "Welcome to Android Studio" (Bienvenue dans Android Studio) s'affiche, ouvrez le menu déroulant "Configure" (Configurer), puis sélectionnez l'option SDK Manager.

3b7b47a139bc456.png

Si vous avez déjà ouvert un projet, vous pouvez ouvrir SDK Manager via le menu "Tools" (Outils). Cliquez sur le menu Tools (Outils) et sélectionnez SDK Manager. La fenêtre SDK Manager s'ouvre.

Dans la barre latérale, sélectionnez : Appearance & Behavior > System Settings > Android SDK (Apparence et comportement > Paramètres système > SDK Android). Sélectionnez l'onglet SDK Platforms (Plates-formes de SDK) dans le volet du SDK Android afin d'afficher la liste des options des outils installés. Assurez-vous que le SDK Android 12.0 ou version ultérieure est installé.

931f6ae02822f417.png

Ensuite, sélectionnez l'onglet SDK Tools et assurez-vous que leNDK et que CMake sont installés.

Remarque : Il n'est pas impératif de posséder la version exacte, tant qu'il s'agit d'une version récente. Toutefois, nous utilisons actuellement le NDK 25.2.9519653 et CMake 3.24.0. La version du NDK installée par défaut changera au fil du temps avec les nouvelles versions du NDK. Si vous devez installer une version spécifique du NDK, suivez les instructions de la documentation de référence d'Android Studio pour installer le NDK dans la section Installer une version spécifique du NDK.

d28adf9279adec4.png

Une fois que tous les outils requis sont cochés, cliquez sur le bouton Apply (Appliquer) en bas de la fenêtre pour les installer. Vous pouvez ensuite fermer la fenêtre du SDK Android en cliquant sur le bouton OK.

Configuration du projet

Cet exemple de projet est un simple jeu de simulation physique en 3D développé avec Swappy pour OpenGL. La structure des répertoires ne change pas beaucoup par rapport au nouveau projet créé à partir du modèle, mais des modifications ont été apportées pour initialiser la physique et la boucle de rendu. Vous pouvez donc commencer par cloner le dépôt.

Cloner le dépôt

À partir de la ligne de commande, accédez au répertoire dans lequel vous souhaitez placer le répertoire racine du jeu et clonez-le depuis GitHub :

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

Assurez-vous de commencer à partir du commit initial du dépôt intitulé [codelab] start: simple game.

Configurer les dépendances

L'exemple de projet utilise la bibliothèque Dear ImGui pour son interface utilisateur. Il utilise également Bullet Physics pour les simulations physiques 3D. Ces bibliothèques sont supposées exister dans le répertoire third_party situé à la racine du projet. Nous avons vérifié les bibliothèques respectives avec --recurse-submodules, spécifié dans la commande de clone ci-dessus.

Tester le projet

Dans Android Studio, ouvrez le projet depuis la racine du répertoire. Assurez-vous qu'un appareil est connecté, puis sélectionnez Build > Make Project (Compiler > Créer un projet) et Run > Run 'app' (Exécuter > Exécuter "application") pour tester la démo. Le résultat final sur l'appareil devrait se présenter comme suit :

f1f33674819909f1.png

À propos du projet

Le jeu est volontairement minimaliste pour que nous puissions nous concentrer sur les spécificités de l'implémentation des fonctionnalités d'adaptabilité. Il exécute une charge de travail physique et graphique facilement configurable afin que nous puissions adapter la configuration de manière dynamique pendant l'exécution en fonction de l'évolution des conditions de l'appareil.

3. Intégrer l'API Thermal

a7e07127f1e5c37d.png

ce607eb5a2322d6b.png

Écouter les changements d'état thermique en Java

Depuis Android 10 (niveau d'API 29), les appareils Android doivent envoyer des rapports à l'application en cours d'exécution chaque fois que l'état thermique change. Les applications peuvent écouter ce changement en fournissant OnThermalStatusChangedListener à PowerManager.

Étant donné que PowerManager.addThermalStatusListener n'est disponible qu'à partir du niveau d'API 29, nous devons effectuer une vérification avant de l'appeler :

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

À partir du niveau d'API 30, vous pouvez enregistrer le rappel en code C++ à l'aide d'AThermal_registerThermalStatusListener. Vous pouvez ainsi définir une méthode native et l'appeler depuis Java comme suit :

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

Vous devrez ajouter les écouteurs dans la fonction "onResume" du cycle de vie de votre activité.

N'oubliez pas que tous les éléments que vous ajoutez à la fonction "onResume" de votre activité doivent également être supprimés de la fonction "onPause". Définissons notre code de nettoyage pour appeler PowerManager.removeThermalStatusListener et 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);
   }
}

Nous allons extraire ces fonctionnalités en les déplaçant vers ADPFManager.java afin de pouvoir les réutiliser facilement dans d'autres projets.

Dans la classe d'activité de votre jeu, créez et maintenez une instance d'ADPFManager, puis associez l'écouteur thermique d'ajout/suppression à la méthode de cycle de vie de l'activité correspondante.

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

Enregistrer les méthodes natives de votre classe C++ dans JNI_OnLoad

À partir du niveau d'API 30, nous pouvons utiliser l'API Thermal du NDK AThermal_* pour mapper l'écouteur Java afin d'appeler les mêmes méthodes C++. Pour que les méthodes Java puissent appeler du code C++, vous devez enregistrer les méthodes C++ dans JNI_OnLoad. Pour en savoir plus, consultez les conseils sur 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;
}

Connecter vos écouteurs natifs à votre jeu

Notre jeu C++ doit savoir quand l'état thermique correspondant a changé. Créons donc la classe adpf_manager correspondante en C++.

Dans le dossier cpp de la source de votre application ($ROOT/app/src/main/cpp), créez une paire de fichiers adpf_manager.h et 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);

Définissez les fonctions C dans le fichier cpp en dehors de la classe 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);
  }
}

Initialiser PowerManager et les fonctions requises pour récupérer la plage de température

À partir du niveau d'API 30, nous pouvons utiliser l'API Thermal du NDK AThermal_* et ainsi, lors de l'initialisation, appeler AThermal_acquireManager et la conserver pour une utilisation ultérieure. Au niveau d'API 29, nous devons trouver les références Java requises et les conserver.

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

S'assurer que la méthode d'initialisation est appelée

Dans notre exemple, la méthode d'initialisation est appelée à partir de SetApplication, elle-même appelée à partir de android_main. Cette configuration est spécifique à notre framework. Par conséquent, si vous effectuez une intégration dans votre jeu, vous devez trouver le bon endroit pour appeler la méthode 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();
}

Surveiller régulièrement la plage de température

Il est souvent préférable d'empêcher l'état thermique d'atteindre un niveau trop élevé, car il est difficile de l'abaisser sans suspendre complètement la charge de travail. Même après un arrêt complet, l'appareil met un certain temps à dissiper la chaleur et à se refroidir. Nous pouvons surveiller régulièrement notre plage de température et ajuster notre charge de travail afin de maintenir cette plage sous contrôle et d'éviter que l'état thermique ne s'aggrave.

Dans notre ADPFManager, découvrons des méthodes pour vérifier la plage de température.

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

Enfin, nous allons présenter des méthodes permettant de définir l'état thermique et son écouteur. Nous obtiendrons la valeur de l'état thermique de l'API Thermal du NDK ou du SDK Java qui appelle notre code natif.

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

Inclure votre adpf_manager.cpp à l'unité de compilation dans le fichier CMakeLists.txt

N'oubliez pas d'ajouter le fichier adpf_manager.cpp que vous venez de créer à votre unité de compilation.

Nous en avons terminé avec les classes ADPFManager Java et cpp réutilisables. Vous pouvez donc récupérer et réutiliser ces fichiers dans d'autres projets, au lieu de réécrire le glue code.

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

Définir les paramètres dans le jeu à modifier lorsque l'état thermique se dégrade

Cette partie sera spécifique au jeu. Dans notre exemple, nous allons réduire les étapes physiques et le nombre de boîtes chaque fois que l'état thermique augmente.

Nous allons également surveiller la plage de température, mais nous nous contenterons d'afficher la valeur au niveau du HUD. Dans votre jeu, vous pouvez réagir à la valeur en ajustant la quantité de post-traitement effectuée par la carte graphique, en réduisant le niveau de détail, etc.

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

Créer une fonction d'écouteur de changement d'état thermique dans votre jeu

À présent, nous devons créer un cpp thermalListener qu'ADPFManager appellera chaque fois qu'il détectera un changement du niveau thermique de l'appareil. Créez cette fonction dans votre jeu pour écouter les changements des valeurs d'état. Nous effectuons le suivi de l'élément last_state pour savoir si le niveau de chaleur augmente ou diminue.

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

Lorsque l'état thermique a changé dans votre jeu, adaptez-le en conséquence

Chaque jeu a des besoins et des priorités différents. Ce qui est très important pour un jeu peut ne pas l'être pour un autre. Vous devez donc décider de l'optimisation à adopter pour éviter que la chaleur n'augmente davantage.

Dans notre exemple, nous réduisons le nombre d'objets à l'écran ainsi que la fidélité physique. Cela allègera la charge de travail du processeur et du GPU et, espérons-le, fera baisser la température. Sachez qu'il est généralement difficile de faire baisser drastiquement la température, sauf si le joueur fait une pause et laisse l'appareil refroidir. C'est pourquoi nous surveillons étroitement la plage de température et empêchons l'appareil d'atteindre sa température limite.

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

N'oubliez pas non plus que les performances optimales générées par les processeurs et GPU sont généralement inefficaces, c'est-à-dire que ces puces produisent généralement des performances maximales avec une consommation d'énergie beaucoup plus élevée et génèrent beaucoup de chaleur. À l'inverse, les performances soutenues représentent les performances les plus optimales par unité d'énergie consommée et de chaleur générée. Pour en savoir plus, consultez la documentation Gestion des performances d'un Projet Android Open Source.

Compilez et exécutez votre projet. L'état thermique et la plage de température actuels seront affichés, et si l'état thermique se détériore, les étapes physiques et le nombre d'objets seront réduits.

4bdcfe567fc603c0.png

En cas de problème, vous pouvez comparer votre travail avec le commit du dépôt nommé [codelab] step: integrated thermal-api.

4. Intégrer l'API Game Mode

L'API Game Mode vous permet d'optimiser les performances de votre jeu ou l'autonomie de la batterie, en fonction du choix du joueur. Elle est disponible sur certains appareils Android 12 et sur tous les appareils Android 13 et versions ultérieures.

Mettre à jour le fichier manifeste Android

Définir appCategory

Pour utiliser l'API Game Mode, vous devez indiquer que votre application appartient à la catégorie "jeu" dans votre balise <application>.

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

Pour Android 13 et versions ultérieures

Nous vous recommandons de cibler les utilisateurs d'Android 13 en suivant les deux sous-étapes suivantes :

Ajouter l'élément <meta-data> "game_mode_config" et le fichier XML correspondant

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

Si vous ciblez les appareils Android 12

Ajouter chaque élément <meta-data> "mode de jeu" directement dans un fichier 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"/>

Implémenter GameModeManager.java pour extraire la fonctionnalité GameMode

L'API Game Mode n'ayant pas encore d'interface cpp, nous devons utiliser l'interface Java et fournir des interfaces JNI. Nous allons l'extraire dans GameModeManager.java pour pouvoir réutiliser la fonctionnalité dans d'autres projets.

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

Adapter votre activité pour initialiser GameModeManager et récupérer le GameMode dans onResume

Associez-le au cycle de vie de l'activité. Chaque fois que le mode de jeu est modifié, votre activité est redémarrée pour que nous puissions capturer la valeur pendant 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();
}

Implémenter la classe GameModeManager pour stocker le gameMode sélectionné par l'utilisateur afin de le retrouver dans le jeu

Créons un wrapper cpp et stockons la valeur Game Mode dans cpp pour la récupérer facilement.

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

Implémenter retrieveGameMode dans votre code natif pour transmettre la valeur GameMode à votre jeu

Il s'agit du moyen le plus simple et le plus efficace de récupérer la valeur Game Mode au démarrage et de la transmettre à votre variable cpp pour y accéder facilement. Nous pouvons compter sur la valeur mise en cache sans avoir à effectuer un appel JNI à chaque fois.

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

}

Ajouter votre game_mode_manager.cpp à l'unité de compilation dans le fichier CMakeLists.txt

N'oubliez pas d'ajouter le fichier game_mode_manager.cpp que vous venez de créer à votre unité de compilation.

Nous avons terminé avec les classes GameModeManager Java et cpc réutilisables. Vous pouvez donc récupérer et réutiliser ces fichiers dans d'autres projets, au lieu de réécrire le glue code.

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

Adapter votre jeu en fonction du GameMode sélectionné par l'utilisateur

Une fois que vous avez récupéré le mode de jeu, vous devez différencier votre jeu en fonction de la valeur sélectionnée par l'utilisateur. La différence la plus importante (et les valeurs les plus polarisantes) réside entre le mode Performances et le mode Batterie. En mode Performances, les utilisateurs veulent généralement se plonger dans le jeu et profiter d'une expérience optimale sans se préoccuper de l'autonomie de la batterie. Vous pouvez donc offrir une fidélité optimale tant que la fréquence d'images est stable. En mode Batterie, les utilisateurs souhaitent jouer à votre jeu plus longtemps et accepteront des réglages plus faibles. Assurez-vous que la fréquence d'images est toujours stable, car une fréquence d'images instable est la pire des expériences pour les joueurs.

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

Dans notre exemple, le rendu se fait en haute résolution à 60 FPS en mode Performances. Cet exemple étant assez simple, la plupart des appareils pourront fonctionner avec une fréquence d'images de 60 FPS sans problème. Pour simplifier le processus, nous n'appliquons donc pas de vérifications supplémentaires. En mode Batterie, nous limitons le rendu à 30 FPS et à une résolution d'un quart. Vous devez trouver vos propres optimisations. N'oubliez pas que l'expérience de jeu et que l'économie d'énergie ne dépendent pas seulement des FPS et de la résolution. Pour trouver des idées d'optimisation, consultez le témoignage de nos développeurs.

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

  ...
}

N'oubliez pas de vérifier vos sdkVersions dans votre fichier de compilation Gradle

L'API Game Mode est disponible sur tous les appareils Android à partir d'Android 13. Cette fonctionnalité sera également activée sur certains appareils 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
        ...
    }

En cas de problème, vous pouvez comparer votre travail avec le commit du dépôt intitulé [codelab] step: integrate game-mode-api.

496c76415d12cbed.png

5. Intégrer l'API Game State

L'API Game State vous permet d'indiquer au système l'état de votre jeu à son plus haut niveau. Vous pouvez ainsi déterminer si le contenu actuel peut être interrompu sans interférer avec le jeu, qui ne peut pas être mis en pause. Elle vous permet également d'indiquer si votre jeu effectue actuellement des E/S lourdes, comme charger des éléments à partir de disques ou d'un réseau. Connaître toutes ces données permet au système d'allouer les bonnes ressources au bon moment (par exemple, allouer la bonne quantité de bande passante de processeur et de mémoire lors d'un chargement ou donner la priorité au trafic réseau pour votre jeu multijoueur lorsque votre joueur est dans une session multijoueur cruciale).

Définir des énumérations pour faciliter le mappage avec les constantes GameState

Étant donné que l'API Game State n'a pas d'interface cpp, commençons par copier les valeurs dans 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,
};

Définir SetGameState dans votre code cpp qui appelle l'API Java via JNI

Une fonction cpp nous suffit pour transmettre toutes les informations à l'API Java.

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

Ne vous laissez pas impressionner, il s'agit simplement d'un appel 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);
  }
}

Appeler SetGameState chaque fois que l'état de votre jeu change

Il vous suffit d'appeler la fonction cpp au moment opportun, par exemple au début du chargement, à la fin du chargement ou à l'entrée et à la sortie de différents états dans votre jeu.

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

Vous pouvez transmettre UNKNOWN si vous ne connaissez pas l'état à ce moment-là. Dans le cas présent, nous déchargeons la scène et ne savons pas où l'utilisateur va se diriger, mais la scène suivante va bientôt se charger et nous pouvons appeler un autre SetGameState avec le nouvel état connu.

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

Simple, n'est-ce pas ? Une fois que vous avez intégré l'API Game State, le système connaîtra les états de votre application et commencera à optimiser les ressources pour améliorer les performances et offrir plus de fluidité à votre joueur.

La prochaine mise à jour de l'API Game State sera bientôt disponible. Nous vous expliquerons comment utiliser les libellés et la qualité pour optimiser votre jeu.

6. Intégrer l'API Performance Hint

L'API Performance Hint vous permet d'envoyer des indices sur les performances au système en créant des sessions pour chaque groupe de threads responsable d'une tâche spécifique, de définir la durée de la tâche cible initiale, puis de transmettre la durée réelle de chaque tâche à chaque frame et de mettre à jour la durée de travail attendue pour le frame suivant.

Par exemple, imaginons que vous ayez un groupe de threads responsable de l'IA ennemie, des calculs physiques et du thread de rendu. Toutes ces sous-tâches doivent être accomplies à chaque frame, et tout dépassement dans l'une d'entre elles entraînera un retard de l'image, manquant ainsi les fréquences d'image cible (FPS). Vous pouvez créer une session PerformanceHint pour ce groupe de threads et définir targetWorkDuration sur les fréquences d'image cible de votre choix. Au moment d'appeler reportActualWorkDuration après le travail effectué sur chaque frame, le système peut analyser cette tendance et ajuster les ressources du processeur en conséquence pour s'assurer que vous pouvez atteindre l'objectif souhaité à chaque frame. Votre frame offre ainsi une meilleure stabilité et la consommation d'énergie de votre jeu gagne en efficacité.

ADPFManager InitializePerformanceHintManager

Nous devons créer la session d'indices pour les threads. Il existe une API C++, mais celle-ci n'est disponible qu'à partir du niveau d'API 33. Aux niveaux d'API 31 et 32, nous devons utiliser l'API Java. Mettons en cache quelques méthodes JNI que nous pourrons utiliser ultérieurement.

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

}

L'appeler dans ADPFManager::SetApplication

N'oubliez pas d'appeler la fonction d'initialisation que nous avons définie à partir d'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);

  ...
}

Définir ADPFManager::beginPerfHintSession et ADPFManager::EndPerfHintSession

Définissez les méthodes cpp pour appeler les API via JNI, en acceptant tous les paramètres requis.

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

  }
}

Les appeler au début et à la fin de chaque frame

Pour chaque frame, nous devons enregistrer l'heure de début au début du frame et signaler l'heure réelle à la fin du frame. Nous appelons reportActualWorkDuration et updateTargetWorkDuration à la fin du frame. Dans cet exemple simple, notre charge de travail ne change pas entre les frames. Nous allons mettre à jour TargetTargetDuration avec la valeur cible cohérente.

// 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. Félicitations !

Félicitations, vous avez intégré des fonctionnalités d'adaptabilité à un jeu.

Nous ajouterons d'autres fonctionnalités du framework d'adaptabilité à Android.