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 提供以下功能:

GameActivityAndroid Archive (AAR) 的形式进行分发。AAR 文件中包含您在 AndroidManifest.xml 中使用的 Java 类,以及可将 GameActivity 的 Java 端与应用的 C/C++ 实现相连的 C 和 C++ 源代码。如果您使用的是 GameActivity 1.2.2 或更高版本,AAR 文件中还会提供 C/C++ 静态库。我们建议您尽可能使用静态库(而非源代码)。

若要在构建过程中添加这些源文件或静态库,请使用 Prefab,它会将原生库和源代码提供给您的 CMake 项目NDK build

  1. 按照 Jetpack Android 游戏页面中的说明操作,将 GameActivity 库依赖项添加到游戏的 build.gradle 文件中。

  2. Android 插件版本 (AGP) 4.1 及更高版本上执行以下操作,以启用 Prefab 功能:

    • 将以下代码添加到模块的 build.gradle 文件的 android 代码块中:
    buildFeatures {
        prefab true
    }
    
    • 选择一个 Prefab 版本,并将其设为 gradle.properties 文件:
    android.prefabVersion=2.0.0
    

    如果使用的是较低的 AGP 版本,请按照 Prefab 文档中的相应配置说明操作。

  3. 如下所示,将 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.cppGameTextInput.cppandroid_native_app_glue.c

Android 如何启动 activity

Android 系统会通过调用与 activity 生命周期的特定阶段对应的回调方法,在您的 activity 实例中执行代码。为了让 Android 启动您的 activity 并启动游戏,您需要在 Android 清单中使用适当的属性声明您的 activity。如需了解详情,请参阅 activity 简介

Android 清单

每个应用项目的源代码集的根目录下都必须有一个 AndroidManifest.xml 文件。该清单文件会向 Android 构建工具、Android 操作系统和 Google Play 描述有关应用的基本信息。其中包括:

在游戏中实现 GameActivity

  1. 创建或标识您的主 activity Java 类(在 AndroidManifest.xml 文件内的 activity 元素中指定的类)。更改此类,以便扩展 com.google.androidgamesdk 软件包中的 GameActivity

    import com.google.androidgamesdk.GameActivity;
    
    public class YourGameActivity extends GameActivity { ... }
    
  2. 确保在启动时使用静态块加载原生库:

    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");
      }
      ...
    }
    
  3. 如果您的库名称不是默认名称 (libmain.so),请将您的原生库添加到 AndroidManifest.xml 中:

    <meta-data android:name="android.app.lib_name"
     android:value="android-game" />
    

实现 android_main

  1. 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;
    }
    
  2. 在主游戏循环中处理 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();
        }
      }
    }
    
  3. 如需了解更多详细信息,请研究 Endless Tunnel NDK 示例的实现。主要区别在于如何处理事件,如下一部分所示。

处理事件

如需让输入事件到达您的应用,请使用 android_app_set_motion_event_filterandroid_app_set_key_event_filter 创建并注册事件过滤器。默认情况下,native_app_glue 库仅允许来自 SOURCE_TOUCHSCREEN 输入的动作事件。请务必查看参考文档android_native_app_glue 实现代码以了解详情。

如需处理输入事件,请在游戏循环中使用 android_app_swap_input_buffers() 获取对 android_input_buffer 的引用。这些缓冲区包含自上次轮询以来发生的动作事件按键事件。所含事件的数量分别存储在 motionEventsCountkeyEventsCount 中。

  1. 迭代和处理游戏循环中的每个事件。在此示例中,以下代码会迭代 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 示例

  2. 操作完成后,请记得清除刚刚处理的事件队列:

    android_app_clear_motion_events(mApp);
    

其他资源

如需详细了解 GameActivity,请参阅以下内容:

如需向 GameActivity 报告 bug 或请求新功能,请使用 GameActivity 问题跟踪器