從 NativeActivity 遷移 Android Game Development Kit 提供的一項工具

本頁面說明如何將 Android 遊戲專案中的 NativeActivity 改成 GameActivity

GameActivity 以 Android 架構的 NativeActivity 為基礎,包含以下強化項目與新功能:

  • 支援 Jetpack 中的 Fragment
  • 新增 TextInput 支援,以協助整合螢幕鍵盤。
  • 處理 GameActivity Java 類別 (而非 NativeActivity onInputEvent 介面) 中的觸控和重要事件。

遷移前,建議您閱讀入門指南,瞭解如何在專案中設定及整合 GameActivity

Java 版本指令碼更新

GameActivity 是以 Jetpack 程式庫的形式發布。請務必套用入門指南所述的 Gradle 指令碼更新步驟:

  1. 在專案的 gradle.properties 檔案中啟用 Jetpack 程式庫:

    android.useAndroidX=true
    
  2. 視需在相同的 gradle.properties 檔案中指定 Prefab 版本,例如:

    android.prefabVersion=2.0.0
    
  3. 在應用程式的 build.gradle 檔案中啟用 Prefab 功能:

    android {
        ... // other configurations
        buildFeatures.prefab true
    }
    
  4. GameActivity 依附元件新增至應用程式:

    1. 新增 coregames-activity 程式庫。
    2. 如果您目前支援的最低 API 級別低於 16,請將支援級別更新到至少 16。
    3. 將已編譯的 SDK 版本更新為 games-activity 程式庫所需的版本。Jetpack 在發布子版本期間通常需要使用最新的 SDK 版本。

    更新後的 build.gradle 檔案大致如下:

    android {
        compiledSdkVersion 33
        ... // other configurations.
        defaultConfig {
            minSdkVersion 16
        }
        ... // other configurations.
    
        buildFeatures.prefab true
    }
    dependencies {
        implementation 'androidx.core:core:1.9.0'
        implementation 'androidx.games:games-activity:1.2.2'
    }
    

Kotlin 或 Java 程式碼更新

NativeActivity 可用來做為啟動活動,並建立全螢幕應用程式。目前,GameActivity 不得做為啟動活動使用。應用程式必須從 GameActivity 衍生類別,並以此做為啟動活動。您必須做出其他設定變更,才能建立全螢幕應用程式。

下列步驟假設您的應用程式使用 NativeActivity 做為啟動活動。如果不是,您可以略過大多數步驟。

  1. 建立 Kotlin 或 Java 檔案來代管新的啟動活動。舉例來說,下列程式碼會建立 MainActivity 做為啟動活動,並載入應用程式的主要原生程式庫 libAndroidGame.so

    Kotlin

    class MainActivity : GameActivity() {
       override fun onResume() {
           super.onResume()
           // Use the function recommended from the following page:
           // https://d.android.com/training/system-ui/immersive
           hideSystemBars()
       }
       companion object {
           init {
               System.loadLibrary("AndroidGame")
           }
       }
    }
    

    Java

      public class MainActivity extends GameActivity {
          protected void onResume() {
              super.onResume();
              // Use the function recommended from
              // https://d.android.com/training/system-ui/immersive
              hideSystemBars();
          }
          static {
              System.loadLibrary("AndroidGame");
          }
      }
    
  2. res\values\themes.xml 檔案中建立全螢幕應用程式主題:

    <resources xmlns:tools="http://schemas.android.com/tools">
        <!-- Base application theme. -->
        <style name="Application.Fullscreen" parent="Theme.AppCompat.Light.NoActionBar">
            <item name="android:windowFullscreen">true</item>
            <item name="android:windowContentOverlay">@null</item>"
        </style>
    </resources>
    
  3. AndroidManifest.xml 檔案中針對應用程式套用主題:

    <application  android:theme=”@style/Application.Fullscreen”>
         <!-- other configurations not listed here. -->
    </application>
    

    如需全螢幕模式的詳細操作說明,請參閱沉浸模式指南games-samples 存放區中的實作示例。

本遷移指南不會變更原生程式庫的名稱。如您確實要變更,原生程式庫的名稱在下列三個位置必須保持一致:

  • Kotlin 或 Java 程式碼:

    System.loadLibrary(“AndroidGame”)
    
  • AndroidManifest.xml

    <meta-data android:name="android.app.lib_name"
            android:value="AndroidGame" />
    
  • C/C++ 建構指令碼檔案內部,例如 CMakeLists.txt

    add_library(AndroidGame ...)
    

C/C++ 建構指令碼更新

本節的操作說明使用 cmake 做為範例。如果您的應用程式使用 ndk-build,請務必對照 ndk-build 說明文件頁面採用相應指令。

我們已針對 GameActivity 提供 C/C++ 實作原始碼,而且在 1.2.2 a 以上版本中提供了靜態資料庫。靜態資料庫是建議的發布類型。

這些釋出內容與 prefab 公用程式一起封裝在 AAR 中。原生程式碼包含 GameActivity 的 C/C++ 來源和 native_app_glue 程式碼。必須隨應用程式的 C/C++ 程式碼一起建構。

NativeActivity 應用程式已使用 NDK 內隨附的 native_app_glue 程式碼。必須將其取代為 GameActivity 版本的 native_app_glue。除此之外,這份入門指南所述的 cmake 步驟都能直接採用:

  • 將 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)
    
  • 「移除」NDK native_app_glue 程式碼的所有參照,例如:

    ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c
        ...
    set(CMAKE_SHARED_LINKER_FLAGS
        "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate")
    
  • 如果您使用的是原始碼,請附上 GameActivity 來源檔案,否則請略過這個步驟。

    get_target_property(game-activity-include
                        game-activity::game-activity
                        INTERFACE_INCLUDE_DIRECTORIES)
    add_library(${PROJECT_NAME} SHARED
        main.cpp
        ${game-activity-include}/game-activity/native_app_glue/android_native_app_glue.c
        ${game-activity-include}/game-activity/GameActivity.cpp
        ${game-activity-include}/game-text-input/gametextinput.cpp)
    

解決 UnsatisfiedLinkError 問題

如果遇到 com.google.androidgamesdk.GameActivity.initializeNativeCode() 函式的 UnsatsifiedLinkError,請在 CMakeLists.txt 檔案中加入以下程式碼:

set(CMAKE_SHARED_LINKER_FLAGS
    "${CMAKE_SHARED_LINKER_FLAGS} -u \
    Java_com_google_androidgamesdk_GameActivity_initializeNativeCode")

C/C++ 原始碼更新

請按照以下步驟,將應用程式中的 NativeActivity 參照取代為 GameActivity

  • 使用透過 GameActivity 發布的 native_app_glue。搜尋並取代所有 android_native_app_glue.h 使用方式:

    #include <game-activity/native_app_glue/android_native_app_glue.h>
    
  • 將動作事件篩選器及關鍵事件篩選器設定為 NULL,以便應用程式從所有輸入裝置接收輸入事件。您通常可以透過 android_main() 函式達成此目的:

    void android_main(android_app* app) {
        ... // other init code.
    
        android_app_set_key_event_filter(app, NULL);
        android_app_set_motion_event_filter(app, NULL);
    
        ... // additional init code, and game loop code.
    }
    
  • 移除 AInputEvent 相關程式碼,並取代為 GameActivity 的 InputBuffer 實作:

    while (true) {
        // Read all pending events.
        int events;
        struct android_poll_source* source;
    
        // If not animating, block forever waiting for events.
        // If animating, loop until all events are read, then continue
        // to draw the next frame of animation.
        while ((ALooper_pollAll(engine.animating ? 0 : -1, nullptr, &events,
                                (void**)&source)) >= 0) {
           // Process this app cycle or inset change event.
           if (source) {
               source->process(source->app, source);
           }
    
              ... // Other processing.
    
           // Check if app is exiting.
           if (state->destroyRequested) {
               engine_term_display(&engine);
               return;
           }
        }
        // Process input events if there are any.
        engine_handle_input(state);
    
       if (engine.animating) {
           // Draw a game frame.
       }
    }
    
    // Implement input event handling function.
    static int32_t engine_handle_input(struct android_app* app) {
       auto* engine = (struct engine*)app->userData;
       auto ib = android_app_swap_input_buffers(app);
       if (ib && ib->motionEventsCount) {
           for (int i = 0; i < ib->motionEventsCount; i++) {
               auto *event = &ib->motionEvents[i];
               int32_t ptrIdx = 0;
               switch (event->action & AMOTION_EVENT_ACTION_MASK) {
                   case AMOTION_EVENT_ACTION_POINTER_DOWN:
                   case AMOTION_EVENT_ACTION_POINTER_UP:
                       // Retrieve the index for the starting and the ending of any secondary pointers
                       ptrIdx = (event->action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >>
                                AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
                   case AMOTION_EVENT_ACTION_DOWN:
                   case AMOTION_EVENT_ACTION_UP:
                       engine->state.x = GameActivityPointerAxes_getAxisValue(
                           &event->pointers[ptrIdx], AMOTION_EVENT_AXIS_X);
                       engine->state.y = GameActivityPointerAxes_getAxisValue(
                           &event->pointers[ptrIdx], AMOTION_EVENT_AXIS_Y);
                       break;
                    case AMOTION_EVENT_ACTION_MOVE:
                    // Process the move action: the new coordinates for all active touch pointers
                    // are inside the event->pointers[]. Compare with our internally saved
                    // coordinates to find out which pointers are actually moved. Note that there is
                    // no index embedded inside event->action for AMOTION_EVENT_ACTION_MOVE (there
                    // might be multiple pointers moved at the same time).
                        ...
                       break;
               }
           }
           android_app_clear_motion_events(ib);
       }
    
       // Process the KeyEvent in a similar way.
           ...
    
       return 0;
    }
    
  • 審查並更新附加至 NativeActivity 的 AInputEvent 的邏輯。如上一個步驟所示,GameActivity 的 InputBuffer 處理在 ALooper_pollAll() 迴圈之外。

  • android_app::activity->clazz 使用方式取代為 android_app:: activity->javaGameActivity。GameActivity 會重新命名 Java GameActivity 執行個體。

其他步驟

上述步驟涵蓋 NativeActivity 的功能,但您可以考慮使用 GameActivity 提供的其他功能,:

建議您瞭解這些功能,並視需要在遊戲中使用。

如對 GameActivity 或其他 AGDK 程式庫有任何疑問或建議,敬請建立錯誤告訴我們。