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

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

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

  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。这包括

  • 包名和应用 ID,用于在 Google Play 上唯一标识您的游戏。

  • 应用组件,例如 Activity、服务、广播接收器和内容提供程序。

  • 权限,用于访问系统的受保护部分或其他应用。

  • 设备兼容性,用于指定游戏的硬件和软件要求。

  • GameActivityNativeActivity 的原生库名称(默认为 libmain.so)。

在您的游戏中实现 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 的新功能,请使用GameActivity 问题跟踪器