開始使用 GameActivity Android Game Development Kit 提供的一項工具

本指南說明如何設定及整合 GameActivity,並處理 Android 遊戲中的事件。

GameActivity 能簡化重要 API 的使用流程,協助將 C 或 C++ 遊戲發布到 Android 平台。我們原先建議在遊戲中使用 NativeActivity 類別,但現在建議改為使用 GameActivity,後者能回溯相容至 API 級別 19。

如需整合 GameActivity 的示例,請前往 games-samples 存放區

事前準備

請查看 GameActivity 發布位置,取得發行版。

設定版本

在 Android 中,Activity 不僅是遊戲的進入點,也提供可呈現內容的 Window。許多遊戲會採用自己的 Java 或 Kotlin 類別來擴充這個 Activity,以打破 NativeActivity 的限制,同時使用 JNI 程式碼橋接至其 C 或 C++ 遊戲程式碼。

GameActivity 提供下列功能:

GameActivity 的發行格式為 Android ARchive (AAR)。這個 AAR 包含您在 AndroidManifest.xml 中使用的 Java 類別,以及連結 GameActivity Java 端與應用程式 C/C++ 實作項目的 C 和 C++ 原始碼。如果使用 GameActivity 1.2.2 以上版本,即可取得 C/C++ 靜態程式庫。如果適用,建議您優先使用靜態資料庫,不要使用原始碼。

請在建構程序中透過 Prefab 附上這些來源檔案或靜態程式庫,將原生資料庫和原始碼提供給 CMake 專案NDK 建構系統

  1. 按照 Jetpack Android 遊戲頁面上的操作說明,將 GameActivity 程式庫依附元件新增至遊戲的 build.gradle 檔案。

  2. 透過 Android 外掛程式 (AGP) 4.1 以上版本執行下列操作,即可啟用 Prefab:

    • 將以下內容加入模組 build.gradle 檔案的 android 區塊:
    buildFeatures {
        prefab true
    }
    
    • 選擇 Prefab 版本,將其設為 gradle.properties 檔案:
    android.prefabVersion=2.0.0
    

    如果使用較舊的 AGP 版本,請按照 Prefab 說明文件取得相對應的設定操作說明。

  3. 將 C/C++ 靜態程式庫或 C/++ 原始碼匯入專案,如下所示。

    靜態資料庫

    在專案的 CMakeLists.txt 檔案中,將 game-activity 靜態資料庫匯入 game-activity_static prefab 模組中:

    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.cppGameTextInput.cppandroid_native_app_glue.c

Android 如何啟動活動

Android 系統會叫用與活動生命週期特定階段相對應的回呼方法,執行活動例項中的程式碼。為了讓 Android 系統啟動活動和遊戲,您需要在 Android 資訊清單中以適當的屬性宣告活動。詳情請參閱「活動簡介」一節。

Android 資訊清單

每個應用程式專案都必須在專案來源集的根目錄中有一個 AndroidManifest.xml 檔案。資訊清單檔案會將您應用程式的基本資訊提供給 Android 建構工具、Android 作業系統和 Google Play。其中包括:

在遊戲中實作 GameActivity

  1. 建立或識別主要活動 Java 類別 (在 AndroidManifest.xml 檔案的 activity 元素中指定的類別)。變更這個類別,以透過 com.google.androidgamesdk 套件擴充 GameActivity

    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. 如果資料庫名稱並非預設名稱 (libmain.so),請將原生資料庫新增至 AndroidManifest.xml

    <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 版本中包含的版本。如果您的遊戲使用的是 NDK 中包含的 android_native_app_glue 程式庫,請切換至 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_filterandroid_app_set_key_event_filter 建立及註冊事件篩選器。根據預設,native_app_glue 程式庫只接受來自 SOURCE_TOUCHSCREEN 輸入內容的動作事件。如要瞭解詳情,請務必查看參考資料文件以及 android_native_app_glue 實作程式碼。

如要處理輸入事件,請在遊戲迴圈中使用 android_app_swap_input_buffers() 取得指向 android_input_buffer 的參照。這包含上次輪詢以來發生過的動作事件重要事件,所含事件數分別儲存在 motionEventsCountkeyEventsCount 中。

  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 Issue Tracker