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 架构组件。渲染到
SurfaceView
,允许您与任何其他 Android UI 元素交互。处理 Java 活动事件。这允许任何 Android UI 元素(例如
EditText
、WebView
或Ad
)通过 C 接口集成到您的游戏中。提供类似于
NativeActivity
的 C API 以及android_native_app_glue
库。
GameActivity
作为 Android 归档 (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 如何启动您的 Activity
Android 系统通过调用与 Activity 生命周期特定阶段相对应的回调方法来执行 Activity 实例中的代码。为了让 Android 启动您的 Activity 并开始您的游戏,您需要在 Android 清单中使用适当的属性声明您的 Activity。有关更多信息,请参阅Activity 简介。
Android 清单
每个应用项目都必须在项目源集的根目录下有一个AndroidManifest.xml 文件。清单文件将有关您应用的基本信息描述给 Android 构建工具、Android 操作系统和 Google Play。这包括
包名和应用 ID,用于在 Google Play 上唯一标识您的游戏。
应用组件,例如 Activity、服务、广播接收器和内容提供程序。
权限,用于访问系统的受保护部分或其他应用。
设备兼容性,用于指定游戏的硬件和软件要求。
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); }
有关
_cookEventForPointerIndex()
和其他相关函数的实现,请参阅GitHub 示例。完成后,请务必清除您刚刚处理过的事件队列
android_app_clear_motion_events(mApp);
其他资源
要了解有关 GameActivity
的更多信息,请参阅以下内容
- GameActivity 和 AGDK 发行说明.
- 在 GameActivity 中使用 GameTextInput.
- NativeActivity 迁移指南.
- GameActivity 参考文档.
- GameActivity 实现.
要报告错误或请求 GameActivity 的新功能,请使用GameActivity 问题跟踪器。