GameActivity 入门 属于 Android 游戏开发套件。
本指南介绍了如何在 Android 游戏中设置和集成 GameActivity
并处理事件。
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 Architecture Components。渲染到
SurfaceView
中,可让您与任何其他 Android UI 元素交互。处理 Java Activity 事件。这允许任何 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
在构建过程中包含这些源文件或静态库,Prefab 将原生库和源代码公开给您的 CMake 项目或 NDK 构建。
按照 Jetpack Android Games 页面上的说明,将
GameActivity
库依赖项添加到游戏的build.gradle
文件中。使用 Android Plugin Version (AGP) 4.1+ 执行以下操作以启用 prefab:
- 将以下内容添加到模块的
build.gradle
文件的android
块中:
buildFeatures { prefab true }
- 选择 一个 Prefab 版本,并将其设置为
gradle.properties
文件。
android.prefabVersion=2.0.0
如果您使用较早的 AGP 版本,请按照 prefab 文档查看相应的配置说明。
- 将以下内容添加到模块的
按如下方式将 C/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 如何启动您的 Activity
Android 系统通过调用与 Activity 生命周期特定阶段对应的回调方法来执行 Activity 实例中的代码。为了让 Android 启动您的 Activity 并开始您的游戏,您需要在 Android Manifest 中使用适当的属性声明您的 Activity。如需了解更多信息,请参阅“Activity 简介”。
Android Manifest
每个应用项目都必须在项目源集的根目录下有一个 AndroidManifest.xml 文件。清单文件向 Android 构建工具、Android 操作系统和 Google Play 描述了关于应用的基本信息。这包括:
软件包名称和应用 ID,用于在 Google Play 上唯一标识您的游戏。
应用组件,例如 Activity、Service、广播接收器和内容提供程序。
权限,用于访问系统或其他应用的受保护部分。
设备兼容性,用于指定游戏的硬件和软件要求。
GameActivity 和 NativeActivity 的原生库名称(默认为 libmain.so)。
在您的游戏中实现 GameActivity
创建或标识您的主 Activity 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_pollOnce(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 报告 bug 或请求新功能,请使用 GameActivity 问题跟踪器。