Introducción a GameActivity Parte de Android Game Development Kit.

GameActivity te ayuda a llevar tu juego de C o C++ a Android simplificando el proceso de uso de APIs críticas.

Para ver una muestra de que está integrada en GameActivity, consulta el repositorio de muestras de juegos.

Cómo configurar tu compilación

En Android, una Activity sirve como punto de entrada para tu juego y también proporciona la Window en la que dibujar. Muchos juegos extienden esta Activity con su propia clase de Java o Kotlin a fin de superar las limitaciones de NativeActivity y usan código JNI de modo que se conecte al código C o C++ de su juego.

GameActivity ofrece las siguientes funciones:

GameActivity se distribuye como un Android ARchive (AAR). Este AAR contiene la clase Java que usarás en tu AndroidManifest.xml, así como el código fuente C y C++, que implementa las funciones nativas de GameActivity. Incluye estos archivos fuente como parte del proceso de compilación mediante Prefab, que expone bibliotecas nativas y código fuente a tu proyecto de CMake o compilación del NDK.

  1. Sigue las instrucciones en la página de juegos de Android para Jetpack a fin de agregar la dependencia de la biblioteca GameActivity al archivo build.gradle de tu juego.

  2. Para habilitar prefab, haz lo siguiente con la versión 4.1 o posteriores del complemento para Android (AGP):

    • Agrega lo siguiente al bloque android del archivo build.gradle de tu módulo:
    buildFeatures {
        prefab true
    }
    
    android.prefabVersion=2.0.0
    

    Si usas versiones anteriores del AGP, sigue la documentación de Prefab para obtener las instrucciones de configuración correspondientes.

  3. En el archivo CMakeLists.txt de tu proyecto, importa el paquete game-activity y agrégalo a tu destino (la actividad del juego necesita libandroid.so, agrégalo también si no está allí):

    find_package(game-activity REQUIRED CONFIG)
    ...
    target_link_libraries(... android game-activity::game-activity)
    
  4. En una de las campañas de archivos .cpp existentes de tu juego o en un nuevo archivo .cpp agrega lo siguiente para incluir el GameActivity, GameTextInput y la implementación glue nativa complementaria:

    #include <game-activity/GameActivity.cpp>
    #include <game-text-input/gametextinput.cpp>
    extern "C" {
      #include <game-activity/native_app_glue/android_native_app_glue.c>
    }
    

Cómo Android inicia tu actividad

El sistema Android ejecuta el código en tu instancia de Activity invocando métodos de devolución de llamada que corresponden a etapas específicas del ciclo de vida de la actividad. A fin de que Android inicie la actividad y tu juego, debes declarar la actividad con los atributos adecuados en el Manifiesto de Android. Si deseas obtener más información, consulta Introducción a las actividades.

Manifiesto de Android

Cada proyecto de app debe tener un archivo AndroidManifest.xml en la raíz del conjunto de orígenes del proyecto. El archivo de manifiesto describe información esencial sobre tu app para las herramientas de compilación de Android, el sistema operativo Android y Google Play. Esto incluye lo siguiente:

Cómo implementar GameActivity en tu juego

  1. Crea o identifica la clase de Java de tu actividad principal (la especificada en el elemento activity dentro de tu archivo AndroidManifest.xml). Cambia esta clase para extender GameActivity desde el paquete com.google.androidgamesdk:

    import com.google.androidgamesdk.GameActivity;
    
    public class YourGameActivity extends GameActivity { ... }
    
  2. Asegúrate de que tu biblioteca nativa se cargue al comienzo con un bloque estático:

    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. Agrega tu biblioteca nativa a AndroidManifest.xml si el nombre de tu biblioteca no es el predeterminado (libmain.so):

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

Cómo implementar android_main

  1. La biblioteca de android_native_app_glue es una biblioteca de código fuente que tu juego usa con el fin de administrar eventos de ciclo de vida de GameActivity en un subproceso independiente a efectos de evitar bloqueos en tu subproceso principal. Cuando usas la biblioteca, registras la devolución de llamada para controlar los eventos de ciclo de vida, como los eventos de entrada táctil. El archivo GameActivity incluye su propia versión de la biblioteca android_native_app_glue, por lo que no puedes usar la versión incluida en las versiones del NDK. Si tus juegos usan la biblioteca android_native_app_glue que se incluye en el NDK, cambia a la versión GameActivity.

    Después de agregar el código fuente de la biblioteca android_native_app_glue a tu proyecto, este interactuará con GameActivity. Implementa una función llamada android_main, que la biblioteca llama y que se usa como punto de entrada para tu juego. Se le pasa una estructura llamada android_app. Esto puede diferir según el juego y el motor. Por ejemplo:

    #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. Procesa android_app en el bucle principal del juego, por ejemplo, consulta y administra eventos de ciclo de la app definidos en NativeAppGlueAppCmd. Por ejemplo, en el siguiente fragmento, se registra la función _hand_cmd_proxy como el controlador NativeAppGlueAppCmd; luego, sondea los eventos de ciclo de la app y los envía al controlador registrado (en android_app::onAppCmd) para su procesamiento:

    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. Si deseas obtener más información, estudia la implementación del ejemplo del NDK de Endless Tunnel. La diferencia principal estará dada por la forma de controlar los eventos, como se muestra en la siguiente sección.

Cómo controlar eventos

Para permitir que los eventos de entrada lleguen a tu aplicación, crea y registra los filtros de evento con android_app_set_motion_event_filter y android_app_set_key_event_filter. De forma predeterminada, la biblioteca native_app_glue solo permite eventos de movimiento desde la entrada SOURCE_TOUCHSCREEN. Asegúrate de consultar el documento de referencia y el código de implementación android_native_app_glue para obtener los detalles.

Para controlar eventos de entrada, obtén una referencia al android_input_buffer con android_app_swap_input_buffers() en el bucle de juego. Estos contienen eventos de movimiento y eventos clave que ocurrieron desde la última vez que se sondeó. La cantidad de eventos contenidos se almacena en motionEventsCount y keyEventsCount respectivamente.

  1. Itera y controla cada evento en tu bucle de juego. En este ejemplo, el siguiente código itera motionEvents y los administra mediante 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);
    }
    

    Consulta la muestra de GitHub para ver la implementación de _cookEventForPointerIndex() y otras funciones relacionadas.

  2. Cuando termines, recuerda borrar la cola de eventos que acabas de controlar:

    android_app_clear_motion_events(mApp);
    

Referencias

Para obtener más información sobre GameActivity, consulta los siguientes vínculos:

Para informar errores o solicitar nuevas funciones a GameActivity, usa la herramienta de seguimiento de errores de GameActivity.