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

GameActivityAndroid Archive (AAR) 的形式分发。此 AAR 包含您在 AndroidManifest.xml 中使用的 Java 类,以及将 GameActivity 的 Java 端连接到应用的 C/C++ 实现的 C 和 C++ 源代码。如果您使用的是 GameActivity 1.2.2 或更高版本,还提供了 C/C++ 静态库。在适用时,我们建议您使用静态库而不是源代码。

通过 Prefab 在构建过程中包含这些源文件或静态库,Prefab 将原生库和源代码公开给您的 CMake 项目NDK 构建

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

  2. 使用 Android Plugin Version (AGP) 4.1+ 执行以下操作以启用 prefab:

    • 将以下内容添加到模块的 build.gradle 文件的 android 块中:
    buildFeatures {
        prefab true
    }
    
    android.prefabVersion=2.0.0
    

    如果您使用较早的 AGP 版本,请按照 prefab 文档查看相应的配置说明。

  3. 按如下方式将 C/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 Manifest 中使用适当的属性声明您的 Activity。如需了解更多信息,请参阅“Activity 简介”

Android Manifest

每个应用项目都必须在项目源集的根目录下有一个 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_pollOnce(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 问题跟踪器