Premiers pas avec GameActivity Composant d'Android Game Development Kit.

Ce guide explique comment configurer et intégrer GameActivity, et comment gérer les événements dans votre jeu Android.

GameActivity vous aide à transférer votre jeu C ou C++ sous Android en simplifiant le processus d'utilisation des API essentielles. Auparavant, NativeActivity était la classe recommandée pour les jeux. GameActivity est désormais recommandée à sa place ; cette classe est rétrocompatible jusqu'au niveau 19 de l'API.

Pour un exemple intégrant GameActivity, accédez au dépôt games-samples.

Avant de commencer

Consultez les versions de GameActivity pour obtenir une distribution.

Configurer votre build

Sous Android, une Activity sert de point d'entrée à votre jeu et fournit la Window dans laquelle vous pouvez dessiner. De nombreux jeux étendent cette Activity avec leur propre classe Java ou Kotlin afin de contourner les limitations de NativeActivity tout en utilisant le code JNI pour faire le lien avec leur code de jeu C ou C++.

GameActivity offre les fonctionnalités suivantes :

GameActivity est distribué sous forme d'archive Android (AAR). Cette AAR contient la classe Java que vous utilisez dans votre fichier AndroidManifest.xml, ainsi que le code source C et C++ qui connecte le côté Java de GameActivity à l'implémentation C/C++ de l'application. Si vous utilisez GameActivity 1.2.2 ou une version ultérieure, la bibliothèque statique C/C++ est également fournie. Le cas échéant, nous vous recommandons d'utiliser la bibliothèque statique plutôt que le code source.

Incluez ces fichiers sources ou la bibliothèque statique dans votre processus de compilation via Prefab, qui expose les bibliothèques natives et le code source au niveau de votre projet CMake ou de votre build NDK.

  1. Suivez les instructions de la page Jetpack Jeux Android pour ajouter la dépendance de la bibliothèque GameActivity au fichier build.gradle de votre jeu.

  2. Activez Prefab en procédant comme suit avec le plug-in Android (AGP) version 4.1 ou ultérieure :

    • Ajoutez le code suivant au bloc android du fichier build.gradle de votre module :
    buildFeatures {
        prefab true
    }
    
    • Sélectionnez une version de Prefab et définissez-la dans le fichier gradle.properties :
    android.prefabVersion=2.0.0
    

    Si vous utilisez des versions antérieures du plug-in AGP, suivez la documentation de Prefab pour obtenir les instructions de configuration correspondantes.

  3. Importez la bibliothèque statique C/C++ ou le code source C/++ dans votre projet comme suit.

    Bibliothèque statique

    Dans le fichier CMakeLists.txt de votre projet, importez la bibliothèque statique game-activity dans le module Prefab game-activity_static :

    find_package(game-activity REQUIRED CONFIG)
    target_link_libraries(${PROJECT_NAME} PUBLIC log android
    game-activity::game-activity_static)
    

    Code source

    Dans le fichier CMakeLists.txt de votre projet, importez le package game-activity et ajoutez-le à votre cible. Le package game-activity nécessite libandroid.so. S'il est absent, vous devez donc également l'importer.

    find_package(game-activity REQUIRED CONFIG)
    ...
    target_link_libraries(... android game-activity::game-activity)
    

    Incluez également les fichiers suivants dans le fichier CmakeLists.txt de votre projet : GameActivity.cpp, GameTextInput.cpp et android_native_app_glue.c.

Comment Android lance-t-il votre instance Activity ?

Le système Android exécute le code dans votre instance Activity en appelant des méthodes de rappel qui correspondent à des étapes spécifiques du cycle de vie de l'activité. Pour qu'Android puisse lancer votre activité et démarrer votre jeu, vous devez déclarer l'activité avec les attributs appropriés dans le fichier manifeste Android. Pour en savoir plus, consultez Présentation des activités.

Fichier manifeste Android

Chaque projet d'application doit disposer d'un fichier AndroidManifest.xml à la racine de l'ensemble de sources du projet. Le fichier manifeste fournit aux outils de compilation Android, au système d'exploitation Android et à Google Play des informations essentielles sur votre application. Par exemple :

Implémenter GameActivity dans votre jeu

  1. Créez ou identifiez la classe Java de votre activité principale (celle spécifiée dans l'élément activity de votre fichier AndroidManifest.xml). Modifiez cette classe pour étendre GameActivity à partir du package com.google.androidgamesdk :

    import com.google.androidgamesdk.GameActivity;
    
    public class YourGameActivity extends GameActivity { ... }
    
  2. Assurez-vous que votre bibliothèque native est chargée au démarrage à l'aide d'un bloc statique :

    public class EndlessTunnelActivity extends GameActivity {
      static {
        // Load the native library.
        // The name "android-game" depends on your CMake configuration, must be
        // consistent here and inside AndroidManifect.xml
        System.loadLibrary("android-game");
      }
      ...
    }
    
  3. Ajoutez votre bibliothèque native au fichier AndroidManifest.xml si le nom de votre bibliothèque n'est pas le nom par défaut (libmain.so) :

    <meta-data android:name="android.app.lib_name"
     android:value="android-game" />
    

Implémenter android_main

  1. La bibliothèque android_native_app_glue est une bibliothèque de code source que votre jeu utilise pour gérer les événements de cycle de vie GameActivity dans un thread distinct afin d'éviter le blocage de votre thread principal. Lorsque vous utilisez la bibliothèque, vous enregistrez le rappel pour gérer les événements de cycle de vie, tels que les événements de saisie tactile. L'archive GameActivity comprend sa propre version de la bibliothèque android_native_app_glue. Vous ne pouvez donc pas utiliser la version incluse dans les versions du NDK. Si vos jeux utilisent la bibliothèque android_native_app_glue incluse dans le NDK, passez à la version GameActivity.

    Une fois que vous avez ajouté le code source de la bibliothèque android_native_app_glue à votre projet, elle interagit avec GameActivity. Implémentez une fonction appelée android_main, qui est appelée par la bibliothèque et utilisée comme point d'entrée de votre jeu. Elle est transmise à une structure appelée android_app. Cette structure peut varier selon votre jeu et votre moteur. Exemple :

    #include <game-activity/native_app_glue/android_native_app_glue.h>
    
    extern "C" {
        void android_main(struct android_app* state);
    };
    
    void android_main(struct android_app* app) {
        NativeEngine *engine = new NativeEngine(app);
        engine->GameLoop();
        delete engine;
    }
    
  2. Traitez android_app dans votre boucle de jeu principale, par exemple pour interroger et gérer les événements de cycle de vie d'application définis dans NativeAppGlueAppCmd. L'extrait de code suivant enregistre la fonction _hand_cmd_proxy en tant que gestionnaire NativeAppGlueAppCmd, puis interroge les événements de cycle de vie d'application et les envoie au gestionnaire enregistré (dans android_app::onAppCmd) pour traitement :

    void NativeEngine::GameLoop() {
      mApp->userData = this;
      mApp->onAppCmd = _handle_cmd_proxy;  // register your command handler.
      mApp->textInputState = 0;
    
      while (1) {
        int events;
        struct android_poll_source* source;
    
        // If not animating, block until we get an event;
        // If animating, don't block.
        while ((ALooper_pollAll(IsAnimating() ? 0 : -1, NULL, &events,
          (void **) &source)) >= 0) {
            if (source != NULL) {
                // process events, native_app_glue internally sends the outstanding
                // application lifecycle events to mApp->onAppCmd.
                source->process(source->app, source);
            }
            if (mApp->destroyRequested) {
                return;
            }
        }
        if (IsAnimating()) {
            DoFrame();
        }
      }
    }
    
  3. Pour en savoir plus, étudiez l'implémentation de l'exemple Endless Tunnel du NDK. La principale différence réside dans la façon dont les événements sont gérés, comme indiqué dans la section suivante.

Gérer les événements

Pour que les événements d'entrée puissent accéder à votre application, créez et enregistrez vos filtres d'événements avec android_app_set_motion_event_filter et android_app_set_key_event_filter. Par défaut, la bibliothèque native_app_glue n'autorise que les événements de mouvement provenant de l'entrée SOURCE_TOUCHSCREEN. Pour en savoir plus, consultez la documentation de référence et le code d'implémentation d'android_native_app_glue.

Pour gérer les événements d'entrée, obtenez une référence à android_input_buffer avec android_app_swap_input_buffers() dans votre boucle de jeu. Ceux-ci contiennent les événements de mouvement et les événements clés qui se sont produits depuis la dernière interrogation. Le nombre d'événements contenus est respectivement stocké dans motionEventsCount et keyEventsCount.

  1. Itérez et gérez chacun des événements de votre boucle de jeu. Dans cet exemple, le code suivant itère les motionEvents et les gère via handle_event :

    android_input_buffer* inputBuffer = android_app_swap_input_buffers(app);
    if (inputBuffer && inputBuffer->motionEventsCount) {
        for (uint64_t i = 0; i < inputBuffer->motionEventsCount; ++i) {
            GameActivityMotionEvent* motionEvent = &inputBuffer->motionEvents[i];
    
            if (motionEvent->pointerCount > 0) {
                const int action = motionEvent->action;
                const int actionMasked = action & AMOTION_EVENT_ACTION_MASK;
                // Initialize pointerIndex to the max size, we only cook an
                // event at the end of the function if pointerIndex is set to a valid index range
                uint32_t pointerIndex = GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT;
                struct CookedEvent ev;
                memset(&ev, 0, sizeof(ev));
                ev.motionIsOnScreen = motionEvent->source == AINPUT_SOURCE_TOUCHSCREEN;
                if (ev.motionIsOnScreen) {
                    // use screen size as the motion range
                    ev.motionMinX = 0.0f;
                    ev.motionMaxX = SceneManager::GetInstance()->GetScreenWidth();
                    ev.motionMinY = 0.0f;
                    ev.motionMaxY = SceneManager::GetInstance()->GetScreenHeight();
                }
    
                switch (actionMasked) {
                    case AMOTION_EVENT_ACTION_DOWN:
                        pointerIndex = 0;
                        ev.type = COOKED_EVENT_TYPE_POINTER_DOWN;
                        break;
                    case AMOTION_EVENT_ACTION_POINTER_DOWN:
                        pointerIndex = ((action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
                                       >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
                        ev.type = COOKED_EVENT_TYPE_POINTER_DOWN;
                        break;
                    case AMOTION_EVENT_ACTION_UP:
                        pointerIndex = 0;
                        ev.type = COOKED_EVENT_TYPE_POINTER_UP;
                        break;
                    case AMOTION_EVENT_ACTION_POINTER_UP:
                        pointerIndex = ((action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
                                       >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
                        ev.type = COOKED_EVENT_TYPE_POINTER_UP;
                        break;
                    case AMOTION_EVENT_ACTION_MOVE: {
                        // Move includes all active pointers, so loop and process them here,
                        // we do not set pointerIndex since we are cooking the events in
                        // this loop rather than at the bottom of the function
                        ev.type = COOKED_EVENT_TYPE_POINTER_MOVE;
                        for (uint32_t i = 0; i < motionEvent->pointerCount; ++i) {
                            _cookEventForPointerIndex(motionEvent, callback, ev, i);
                        }
                        break;
                    }
                    default:
                        break;
                }
    
                // Only cook an event if we set the pointerIndex to a valid range, note that
                // move events cook above in the switch statement.
                if (pointerIndex != GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT) {
                    _cookEventForPointerIndex(motionEvent, callback,
                                              ev, pointerIndex);
                }
            }
        }
        android_app_clear_motion_events(inputBuffer);
    }
    

    Consultez l'exemple GitHub pour l'implémentation de _cookEventForPointerIndex() et d'autres fonctions associées.

  2. Lorsque vous avez terminé, n'oubliez pas de vider la file d'attente des événements que vous venez de gérer :

    android_app_clear_motion_events(mApp);
    

Ressources supplémentaires

Pour en savoir plus sur GameActivity, consultez :

Pour signaler des bugs ou demander de nouvelles fonctionnalités dans GameActivity, utilisez l'outil Issue Tracker de GameActivity.