Introdução à GameActivity Parte do Android Game Development Kit

Este guia descreve como configurar e integrar o GameActivity e processar eventos no seu jogo Android.

A GameActivity ajuda a levar seu jogo em C ou C++ para o Android simplificando o processo de uso de APIs essenciais. Anteriormente, NativeActivity era a classe recomendada para jogos. GameActivity a substitui como a classe recomendada para jogos e é compatível com versões anteriores à API de nível 19.

Para ver uma amostra que integra a GameActivity, consulte o repositório games-samples.

Antes de começar

Consulte as versões de GameActivity para receber uma distribuição.

Configurar o build

No Android, uma Activity serve como ponto de entrada para o jogo e também fornece a Window para desenhar. Muitos jogos estendem essa Activity com a própria classe Java ou Kotlin para driblar limitações em NativeActivity enquanto usam o código JNI para fazer a ponte com o código de jogo em C ou C++.

GameActivity oferece os seguintes recursos:

A GameActivity é distribuída como um ARchive do Android (AAR). Esse AAR contém a classe Java que você usa no AndroidManifest.xml, além do código-fonte C e C++ que conecta a parte Java da GameActivity à implementação em C/C++ do app. Se você estiver usando a GameActivity 1.2.2 ou mais recente, a biblioteca C/C++ estática também será fornecida. Sempre que aplicável, recomendamos usar a biblioteca estática em vez do código-fonte.

Inclua esses arquivos de origem ou a biblioteca estática como parte do processo de compilação usando Prefab, que expõe bibliotecas nativas e código-fonte ao seu projeto do CMake ou build do NDK (em inglês).

  1. Siga as instruções na página Jetpack Android Games para adicionar a dependência da biblioteca GameActivity ao arquivo build.gradle do jogo.

  2. Ative o prefab fazendo o seguinte com a versão do plug-in do Android para Gradle (AGP) 4.1+:

    • Adicione o seguinte ao bloco android do arquivo build.gradle do módulo:
    buildFeatures {
        prefab true
    }
    
    android.prefabVersion=2.0.0
    

    Se você usa versões anteriores do AGP, siga a documentação do prefab para as instruções de configuração correspondentes.

  3. Importe a biblioteca estática C/C++ ou o código-fonte C/++ para seu projeto da seguinte maneira:

    Biblioteca estática

    No arquivo CMakeLists.txt do projeto, importe a biblioteca estática game-activity para o módulo prefab game-activity_static:

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

    Código-fonte

    No arquivo CMakeLists.txt do projeto, importe o pacote game-activity e adicione-o ao destino: O pacote game-activity requer libandroid.so. Portanto, se ele estiver ausente, será necessário importá-lo.

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

    Além disso, inclua os seguintes arquivos no CmakeLists.txt do seu projeto: GameActivity.cpp, GameTextInput.cpp e android_native_app_glue.c.

Como o Android inicia sua atividade

O sistema Android executa o código na instância de atividade invocando métodos de callback que correspondem a estágios específicos do ciclo de vida da atividade. Para que o Android inicie sua atividade e o jogo, é necessário declarar a atividade com os atributos adequados no manifesto do Android. Para mais informações, consulte Introdução às atividades.

Manifesto do Android

Todo projeto de aplicativo precisa ter um arquivo AndroidManifest.xml na raiz do conjunto de origem do projeto. O arquivo de manifesto descreve informações essenciais sobre o app para as ferramentas de compilação do Android, para o sistema operacional Android e para o Google Play. Esse conteúdo inclui o seguinte:

Implementar GameActivity no jogo

  1. Crie ou identifique sua classe Java de atividade principal, que é aquela especificada no elemento activity dentro do arquivo AndroidManifest.xml. Mude essa classe para estender GameActivity do pacote com.google.androidgamesdk:

    import com.google.androidgamesdk.GameActivity;
    
    public class YourGameActivity extends GameActivity { ... }
    
  2. Verifique se a biblioteca nativa está carregada no início usando um bloco 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. Adicione a biblioteca nativa a AndroidManifest.xml se o nome da biblioteca não for o padrão (libmain.so):

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

Implementar android_main

  1. android_native_app_glue é uma biblioteca de códigos-fonte que seu jogo usa para gerenciar eventos de ciclo de vida da GameActivity em uma linha de execução separada para evitar bloqueios na linha de execução principal. Ao usar a biblioteca, você registra o callback para processar eventos de ciclo de vida, como eventos de entrada por toque. O arquivo GameActivity inclui a própria versão da biblioteca android_native_app_glue. Portanto, não é possível usar a versão incluída em versões do NDK. Se os jogos estiverem usando a biblioteca android_native_app_glue incluída no NDK, mude para a versão GameActivity.

    Depois de adicionar o código-fonte da biblioteca android_native_app_glue ao projeto, ele interage com GameActivity. Implemente uma função denominada android_main, que é chamada pela biblioteca e usada como ponto de entrada para o jogo. Ela recebe uma estrutura chamada android_app. Isso pode variar no seu jogo e no mecanismo. Veja um exemplo:

    #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. Processe android_app no loop de jogo principal, como a pesquisa e o processamento de eventos de ciclo do app definidos em NativeAppGlueAppCmd. Por exemplo, o snippet a seguir registra a função _hand_cmd_proxy como o gerenciador NativeAppGlueAppCmd. Em seguida, pesquisa os eventos de ciclo do app e os envia ao gerenciador registrado em android_app::onAppCmd para processamento:

    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. Para saber mais, estude a implementação do exemplo do NDK Endless Tunnel. A principal diferença é o modo de processar eventos, conforme mostrado na próxima seção.

Processar eventos

Para permitir que os eventos de entrada cheguem ao seu app, crie e registre seus filtros de eventos com android_app_set_motion_event_filter e android_app_set_key_event_filter. Por padrão, a biblioteca native_app_glue só permite eventos de movimento da entrada SOURCE_TOUCHSCREEN. Consulte o documento de referência e o código para implementação de android_native_app_glue para ver mais detalhes.

Para processar eventos de entrada, acesse uma referência ao android_input_buffer com android_app_swap_input_buffers() no loop de jogo. Eles contêm os eventos de movimento e de chave que ocorreram desde a pesquisa. O número de eventos contidos é armazenado em motionEventsCount e keyEventsCount, respectivamente.

  1. Faça a iteração e o processamento de cada evento no seu loop de jogo. Neste exemplo, o código a seguir faz a iteração de motionEvents e os processa com 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);
    }
    

    Consulte o exemplo do GitHub para ver a implementação de _cookEventForPointerIndex() e outras funções relacionadas.

  2. Quando terminar, não se esqueça de limpar a fila de eventos que você acabou de processar:

    android_app_clear_motion_events(mApp);
    

Outros recursos

Para saber mais sobre GameActivity, consulte:

Para informar bugs ou solicitar novos recursos à GameActivity, use o Issue Tracker da GameActivity (link em inglês).