開始使用 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
提供下列功能:
沿用
AppCompatActivity
設定,您可以使用 Android Jetpack 架構元件。算繪為
SurfaceView
,方便您與任何其他 Android UI 元素互動。處理 Java 活動事件。允許任何 Android UI 元素 (例如
EditText
、WebView
或Ad
),使用 C 介面整合至您的遊戲。提供類似
NativeActivity
和android_native_app_glue
程式庫的 C API。
GameActivity
的發行格式為 Android ARchive (AAR)。這個 AAR 包含您在 AndroidManifest.xml
中使用的 Java 類別,以及連結 GameActivity
Java 端與應用程式 C/C++ 實作項目的 C 和 C++ 原始碼。如果使用 GameActivity
1.2.2 以上版本,即可取得 C/C++ 靜態程式庫。如果適用,建議您優先使用靜態資料庫,不要使用原始碼。
請在建構程序中透過 Prefab
附上這些來源檔案或靜態程式庫,將原生資料庫和原始碼提供給 CMake 專案或 NDK 建構系統。
按照 Jetpack Android 遊戲頁面上的操作說明,將
GameActivity
程式庫依附元件新增至遊戲的build.gradle
檔案。透過 Android 外掛程式 (AGP) 4.1 以上版本執行下列操作,即可啟用 Prefab:
- 將以下內容加入模組
build.gradle
檔案的android
區塊:
buildFeatures { prefab true }
- 選擇 Prefab 版本,將其設為
gradle.properties
檔案:
android.prefabVersion=2.0.0
如果使用較舊的 AGP 版本,請按照 Prefab 說明文件取得相對應的設定操作說明。
- 將以下內容加入模組
將 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.cpp
、GameTextInput.cpp
和android_native_app_glue.c
。
Android 如何啟動活動
Android 系統會叫用與活動生命週期特定階段相對應的回呼方法,執行活動例項中的程式碼。為了讓 Android 系統啟動活動和遊戲,您需要在 Android 資訊清單中以適當的屬性宣告活動。詳情請參閱「活動簡介」一節。
Android 資訊清單
每個應用程式專案都必須在專案來源集的根目錄中有一個 AndroidManifest.xml 檔案。資訊清單檔案會將您應用程式的基本資訊提供給 Android 建構工具、Android 作業系統和 Google Play。其中包括:
套件名稱和應用程式 ID,可明確識別 Google Play 上的遊戲。
應用程式元件,例如活動、服務、廣播接收器和內容供應者。
權限,可存取系統或其他應用程式受保護的部分。
裝置相容性,可指定遊戲的硬體和軟體需求。
GameActivity
和NativeActivity
的原生資料庫名稱 (預設為 libmain.so)。
在遊戲中實作 GameActivity
建立或識別主要活動 Java 類別 (在
AndroidManifest.xml
檔案的activity
元素中指定的類別)。變更這個類別,以透過com.google.androidgamesdk
套件擴充GameActivity
:import com.google.androidgamesdk.GameActivity; public class YourGameActivity extends GameActivity { ... }
確保您的原生資料庫在開始時使用靜態區塊載入:
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"); } ... }
如果資料庫名稱並非預設名稱 (
libmain.so
),請將原生資料庫新增至AndroidManifest.xml
:<meta-data android:name="android.app.lib_name" android:value="android-game" />
實作 android_main
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; }
處理主要遊戲迴圈中的
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(); } } }
如要進一步瞭解,請參閱 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_app_swap_input_buffers()
取得指向 android_input_buffer
的參照。這包含上次輪詢以來發生過的動作事件和重要事件,所含事件數分別儲存在 motionEventsCount
和 keyEventsCount
中。
疊代並處理遊戲迴圈中的各個事件。在這個範例中,以下程式碼會疊代
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()
和其他程式碼 相關函式完成後,請記得清除剛處理的事件佇列:
android_app_clear_motion_events(mApp);
其他資源
如要進一步瞭解 GameActivity
,請參閱以下說明:
- GameActivity 和 AGDK 版本資訊。
- 在 GameActivity 中使用 GameTextInput。
- NativeActivity 遷移指南。
- GameActivity 參考說明文件。
- GameActivity 實作說明。
如要向 GameActivity 回報錯誤或要求新功能,請使用 GameActivity Issue Tracker。