開始使用 GameActivity Android Game Development Kit 提供的一項工具
本指南說明如何設定及整合 GameActivity
,並處理 Android 遊戲中的事件。
GameActivity
能簡化重要 API 的使用流程,協助您將 C 或 C++ 遊戲發布到 Android 平台上。我們原先建議在遊戲中使用 NativeActivity
類別,但現在建議使用 GameActivity
取代這個類別,後者能回溯相容於 Android 級別 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 Games 頁面上的操作說明,將
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)
在遊戲現有的
.cpp
檔案或新的.cpp
檔案中加入下列程式碼,藉此納入GameActivity
、GameTextInput
以及配套的 glue 原生實作內容:#include <game-activity/GameActivity.cpp> #include <game-text-input/gametextinput.cpp> extern "C" { #include <game-activity/native_app_glue/android_native_app_glue.c> }
Android 如何啟動 Activity
Android 系統會叫用與活動生命週期特定階段相對應的回呼方法,執行活動例項中的程式碼。為了讓 Android 系統啟動您的 Activity 和遊戲,您必須在 Android 資訊清單中使用適當的屬性宣告 Activity。詳情請參閱「活動簡介」一節。
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); }
_cookEventForPointerIndex()
實作和其他相關函式的資訊請見 GitHub 範例。完成後,請記得清除剛處理的事件佇列:
android_app_clear_motion_events(mApp);
其他資源
如要進一步瞭解 GameActivity
,請參閱以下說明:
- GameActivity 和 AGDK 版本資訊。
- 在 GameActivity 中使用 GameTextInput。
- NativeActivity 遷移指南。
- GameActivity 參考說明文件。
- GameActivity 實作說明。
如要向 GameActivity 回報錯誤或要求新功能,請使用 GameActivity Issue Tracker。