Начните работу с GameActivity , входящей в комплект разработки игр для Android .

В этом руководстве описывается, как настроить и интегрировать GameActivity и обрабатывать события в игре для Android.

GameActivity помогает перенести игру на C или C++ на Android, упрощая процесс использования важных API. Раньше NativeActivity был рекомендуемым классом для игр. GameActivity заменяет его в качестве рекомендуемого класса для игр и обратно совместим с уровнем API 19.

Пример, интегрирующий GameActivity, можно найти в репозитории games-samples .

Прежде чем начать

См. выпуски GameActivity , чтобы получить дистрибутив.

Настройте свою сборку

В Android Activity служит точкой входа в вашу игру, а также предоставляет Window для рисования. Многие игры расширяют это Activity с помощью собственного класса Java или Kotlin, чтобы обойти ограничения NativeActivity и использовать код JNI для соединения с игровым кодом C или C++.

GameActivity предлагает следующие возможности:

  • Наследует от AppCompatActivity , что позволяет использовать компоненты архитектуры Android Jetpack .

  • Преобразуется в SurfaceView , который позволяет взаимодействовать с любым другим элементом пользовательского интерфейса Android.

  • Обрабатывает события активности Java. Это позволяет любому элементу пользовательского интерфейса Android (например, EditText , WebView или Ad ) интегрироваться в вашу игру через интерфейс C.

  • Предлагает API C, аналогичный NativeActivity , и библиотеку android_native_app_glue .

GameActivity распространяется в виде Android Archive (AAR) . Этот AAR содержит класс Java, который вы используете в своем AndroidManifest.xml , а также исходный код C и C++, который соединяет Java-часть GameActivity с реализацией приложения C/C++. Если вы используете GameActivity 1.2.2 или более позднюю версию, также предоставляется статическая библиотека C/C++. Если это возможно, мы рекомендуем вам использовать статическую библиотеку вместо исходного кода.

Включите эти исходные файлы или статическую библиотеку в процесс сборки с помощью Prefab , который предоставляет собственные библиотеки и исходный код вашему проекту CMake или сборке NDK .

  1. Следуйте инструкциям на странице Jetpack Android Games , чтобы добавить зависимость библиотеки GameActivity в файл build.gradle вашей игры.

  2. Включите префаб, выполнив следующие действия с помощью Android Plugin Version (AGP) 4.1+ :

    • Добавьте следующее в блок android файла build.gradle вашего модуля:
    buildFeatures {
        prefab true
    }
    
    • Выберите версию Prefab и установите ее в файл gradle.properties :
    android.prefabVersion=2.0.0
    

    Если вы используете более ранние версии AGP, следуйте документации по сборному блоку для получения соответствующих инструкций по настройке.

  3. Импортируйте либо статическую библиотеку C/C++, либо исходный код C/++ в свой проект следующим образом.

    Статическая библиотека

    В файле CMakeLists.txt вашего проекта импортируйте статическую библиотеку game-activity в сборный модуль game-activity_static :

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

    Исходный код

    В файле CMakeLists.txt вашего проекта импортируйте пакет game-activity и добавьте его в цель. Для пакета game-activity требуется libandroid.so , поэтому, если он отсутствует, вам также необходимо его импортировать.

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

    Кроме того, включите в CmakeLists.txt вашего проекта следующие файлы: GameActivity.cpp , GameTextInput.cpp и android_native_app_glue.c .

Как Android запускает вашу активность

Система Android выполняет код в вашем экземпляре Activity, вызывая методы обратного вызова, соответствующие определенным этапам жизненного цикла действия. Чтобы Android мог запустить вашу активность и запустить игру, вам необходимо объявить свою активность с соответствующими атрибутами в манифесте Android. Для получения дополнительной информации см. Введение в действия .

Android-манифест

Каждый проект приложения должен иметь файл AndroidManifest.xml в корне исходного набора проекта. Файл манифеста содержит важную информацию о вашем приложении для инструментов сборки Android, операционной системы Android и Google Play. Это включает в себя:

Внедрите GameActivity в свою игру.

  1. Создайте или определите класс Java основного действия (тот, который указан в элементе activity внутри файла AndroidManifest.xml ). Измените этот класс, чтобы расширить GameActivity из пакета com.google.androidgamesdk :

    import com.google.androidgamesdk.GameActivity;
    
    public class YourGameActivity extends GameActivity { ... }
    
  2. Убедитесь, что ваша собственная библиотека загружается в начале с использованием статического блока:

    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. Добавьте свою собственную библиотеку в AndroidManifest.xml , если имя вашей библиотеки не является именем по умолчанию ( libmain.so ):

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

Реализация android_main

  1. Библиотека android_native_app_glue — это библиотека исходного кода, которую ваша игра использует для управления событиями жизненного цикла GameActivity в отдельном потоке, чтобы предотвратить блокировку в основном потоке. При использовании библиотеки вы регистрируете обратный вызов для обработки событий жизненного цикла, таких как события сенсорного ввода. В архиве GameActivity есть собственная версия библиотеки android_native_app_glue , поэтому вы не можете использовать версию, включенную в релизы NDK. Если ваши игры используют библиотеку android_native_app_glue , включенную в NDK, переключитесь на версию GameActivity .

    После того как вы добавите исходный код библиотеки android_native_app_glue в свой проект, она будет взаимодействовать с GameActivity . Реализуйте функцию android_main , которая вызывается библиотекой и используется в качестве точки входа в вашу игру. Ему передается структура под названием android_app . Это может отличаться в зависимости от вашей игры и движка. Вот пример:

    #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. Обработка android_app в основном игровом цикле, например опрос и обработка событий цикла приложения, определенных в NativeAppGlueAppCmd ​​. Например, следующий фрагмент регистрирует функцию _hand_cmd_proxy в качестве обработчика NativeAppGlueAppCmd , затем опрашивает события цикла приложения и отправляет их зарегистрированному обработчику (в android_app::onAppCmd ​​) для обработки:

    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. Для дальнейшего чтения изучите реализацию примера Endless Tunnel NDK. Основное различие будет заключаться в том, как обрабатывать события, как показано в следующем разделе.

Обработка событий

Чтобы события ввода могли достигать вашего приложения, создайте и зарегистрируйте фильтры событий с помощью android_app_set_motion_event_filter и android_app_set_key_event_filter . По умолчанию библиотека native_app_glue разрешает только события движения из ввода SOURCE_TOUCHSCREEN . Обязательно ознакомьтесь с справочным документом и кодом реализации android_native_app_glue для получения подробной информации.

Чтобы обрабатывать события ввода, получите ссылку на android_input_buffer с помощью android_app_swap_input_buffers() в игровом цикле. Они содержат события движения и ключевые события , произошедшие с момента последнего опроса. Количество содержащихся событий хранится в motionEventsCount и keyEventsCount соответственно.

  1. Повторяйте и обрабатывайте каждое событие в игровом цикле. В этом примере следующий код выполняет итерацию motionEvents и обрабатывает их через 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);
    }
    

    См. пример GitHub для реализации _cookEventForPointerIndex() и других связанных функций.

  2. Когда вы закончите, не забудьте очистить очередь событий, которые вы только что обработали:

    android_app_clear_motion_events(mApp);
    

Дополнительные ресурсы

Чтобы узнать больше о GameActivity , см. следующее:

Чтобы сообщить об ошибках или запросить новые функции в GameActivity, используйте систему отслеживания проблем GameActivity .