开始使用 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 提供以下功能

GameActivity 作为 Android 存档 (AAR) 分发。此 AAR 包含您在 AndroidManifest.xml 中使用的 Java 类,以及将 Java 侧的 GameActivity 连接到应用程序 C/C++ 实现的 C 和 C++ 源代码。如果您使用的是 GameActivity 1.2.2 或更高版本,则还会提供 C/C++ 静态库。在适用情况下,我们建议您使用静态库而不是源代码。

通过 Prefab 将这些源文件或静态库作为构建过程的一部分包含在内,Prefab 会将本机库和源代码公开给您的 CMake 项目NDK 构建

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

  2. 使用 Android 插件版本 (AGP) 4.1+ 启用 Prefab,方法如下

    • 将以下内容添加到模块的 build.gradle 文件的 android 块中
    buildFeatures {
        prefab true
    }
    
    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 如何启动您的活动

Android 系统通过调用与活动生命周期特定阶段相对应的回调方法来执行 Activity 实例中的代码。为了让 Android 启动您的活动并启动您的游戏,您需要在 Android 清单中使用适当的属性声明您的活动。有关更多信息,请参阅 活动简介

Android 清单

每个应用项目都必须在其项目源集的根目录中包含一个 AndroidManifest.xml 文件。清单文件描述了您的应用的基本信息,供 Android 构建工具、Android 操作系统和 Google Play 使用。这包括

在您的游戏中实现 GameActivity

  1. 创建或识别您的主活动 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 的错误或请求新功能,请使用 GameActivity 问题跟踪器