GameActivity 使用入门 Android Game Development Kit 的一部分。
本指南介绍了如何在 Android 游戏中设置和集成 GameActivity
以及处理事件。
GameActivity
可简化关键 API 的使用过程,帮助您将 C 或 C++ 游戏发布到 Android 平台上。
以前,我们推荐在游戏中使用 NativeActivity
类。GameActivity
取代它成为推荐在游戏中使用的类,并可向后兼容至 API 级别 19。
如需查看集成了 GameActivity 的示例,请参阅 games-samples 代码库。
准备工作
请参阅 GameActivity
版本以获取发行版。
设置 build
在 Android 上,Activity
充当游戏的入口点,并且还提供 Window
以供在其中进行绘制。许多游戏使用自己的 Java 或 Kotlin 类扩展此 Activity
,以打破 NativeActivity
中的限制,同时利用 JNI
代码桥接到其 C 或 C++ 游戏代码。
GameActivity
提供以下功能:
自
AppCompatActivity
继承,这让您可以使用 Android Jetpack 架构组件。渲染到
SurfaceView
中,您可以通过它与任何其他 Android 界面元素进行交互。处理 Java activity 事件。这样任何 Android 界面元素(例如
EditText
、WebView
或Ad
)都可以通过 C 接口集成到游戏中。提供一个类似于
NativeActivity
的 C API 以及android_native_app_glue
库。
GameActivity
以 Android Archive (AAR) 的形式进行分发。AAR 文件中包含您在 AndroidManifest.xml
中使用的 Java 类,以及可将 GameActivity
的 Java 端与应用的 C/C++ 实现相连的 C 和 C++ 源代码。如果您使用的是 GameActivity
1.2.2 或更高版本,AAR 文件中还会提供 C/C++ 静态库。我们建议您尽可能使用静态库(而非源代码)。
若要在构建过程中添加这些源文件或静态库,请使用 Prefab
,它会将原生库和源代码提供给您的 CMake 项目或 NDK build。
按照 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 如何启动 activity
Android 系统会通过调用与 activity 生命周期的特定阶段对应的回调方法,在您的 activity 实例中执行代码。为了让 Android 启动您的 activity 并启动游戏,您需要在 Android 清单中使用适当的属性声明您的 activity。如需了解详情,请参阅 activity 简介。
Android 清单
每个应用项目的源代码集的根目录下都必须有一个 AndroidManifest.xml 文件。该清单文件会向 Android 构建工具、Android 操作系统和 Google Play 描述有关应用的基本信息。其中包括:
软件包名称和应用 ID,用于在 Google Play 上唯一标识您的游戏。
应用组件,例如 activity、服务、广播接收器和 content provider。
权限,用于访问系统的受保护部分或其他应用。
设备兼容性,用于指定游戏的硬件和软件要求。
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_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 报告 bug 或请求新功能,请使用 GameActivity 问题跟踪器。