开始使用 GameActivity 是 Android 游戏开发套件 的一部分。
本指南介绍了如何设置和集成 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 存档 (AAR) 分发。此 AAR 包含您在 AndroidManifest.xml
中使用的 Java 类,以及将 Java 侧的 GameActivity
连接到应用程序 C/C++ 实现的 C 和 C++ 源代码。如果您使用的是 GameActivity
1.2.2 或更高版本,则还会提供 C/C++ 静态库。在适用情况下,我们建议您使用静态库而不是源代码。
通过 Prefab
将这些源文件或静态库作为构建过程的一部分包含在内,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)
另外,将以下文件包含到项目的
CmakeLists.txt
中:GameActivity.cpp
、GameTextInput.cpp
和android_native_app_glue.c
。
Android 如何启动您的活动
Android 系统通过调用与活动生命周期特定阶段相对应的回调方法来执行 Activity 实例中的代码。为了让 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); }
有关
_cookEventForPointerIndex()
和其他相关函数的实现,请参阅 GitHub 示例。完成后,请务必清除您刚处理过的事件队列
android_app_clear_motion_events(mApp);
其他资源
要详细了解 GameActivity
,请参阅以下内容
- GameActivity 和 AGDK 发行说明.
- 在 GameActivity 中使用 GameTextInput.
- NativeActivity 迁移指南.
- GameActivity 参考文档.
- GameActivity 实现.
要报告 GameActivity 的错误或请求新功能,请使用 GameActivity 问题跟踪器。