Làm quen với GameActivity Một phần của Android Game Development Kit.

Hướng dẫn này mô tả cách thiết lập, tích hợp GameActivity và xử lý các sự kiện trong trò chơi Android.

GameActivity giúp bạn chuyển trò chơi được viết bằng ngôn ngữ C hoặc C++ sang Android bằng cách đơn giản hoá quy trình sử dụng các API quan trọng. Trước đây, NativeActivity là là lớp được đề xuất cho trò chơi. GameActivity thay thế lớp này làm lớp được đề xuất cho trò chơi và tương thích ngược với API cấp 19.

Để xem các mẫu tích hợp GameActivity, hãy truy cập kho lưu trữ trò chơi-mẫu.

Trước khi bắt đầu

Xem bản phát hành GameActivity để nhận bản phân phối.

Thiết lập bản dựng

Trên Android, Activity đóng vai trò là điểm bắt đầu của trò chơi, đồng thời cung cấp Window để vẽ trong đó. Nhiều trò chơi mở rộng Activity này bằng lớp Java hoặc Kotlin riêng để vượt qua các giới hạn trong NativeActivity trong khi sử dụng mã JNI để làm cầu nối cho mã trò chơi được viết bằng C hoặc C++.

GameActivity có các tính năng sau:

GameActivity được phân phối dưới dạng một Android Archive (AAR). AAR này chứa lớp Java mà bạn sử dụng trong AndroidManifest.xml cũng như mã nguồn C và C++ kết nối phía Java của GameActivity với quá trình triển khai C/C++ của ứng dụng. Nếu bạn đang sử dụng GameActivity 1.2.2 trở lên, thì thư viện tĩnh C/C++ cũng được cung cấp. Nếu có thể, bạn nên sử dụng thư viện tĩnh thay cho mã nguồn.

Đưa thư viện tĩnh hoặc các tệp nguồn này vào quy trình xây dựng thông qua Prefab để hiển thị thư viện gốc và mã nguồn cho Dự án CMake hoặc bản dựng NDK.

  1. Làm theo hướng dẫn trên trang Jetpack Android Games để thêm phần phụ thuộc thư viện GameActivity vào tệp build.gradle của trò chơi.

  2. Thực hiện các thao tác sau để bật prefab với Phiên bản Plugin Android (AGP) 4.1 trở lên:

    • Thêm đoạn mã sau vào khối android trong tệp build.gradle trên mô-đun của bạn:
    buildFeatures {
        prefab true
    }
    
    android.prefabVersion=2.0.0
    

    Nếu bạn sử dụng các phiên bản AGP cũ, hãy làm theo tài liệu về prefab để biết hướng dẫn về cấu hình tương ứng.

  3. Nhập thư viện tĩnh C/C++ hoặc mã nguồn C/++ vào dự án của bạn theo cách dưới đây.

    Thư viện tĩnh

    Trong tệp CMakeLists.txt của dự án, hãy nhập thư viện tĩnh game-activity vào mô-đun prefab game-activity_static:

    find_package(game-activity REQUIRED CONFIG)
    target_link_libraries(${PROJECT_NAME} PUBLIC log android
    game-activity::game-activity_static)
    

    Mã nguồn

    Trong tệp CMakeLists.txt của dự án, hãy nhập gói game-activity và thêm gói đó vào mục tiêu của bạn. Gói game-activity cần libandroid.so, vì vậy, nếu còn thiếu, bạn cũng phải nhập gói này.

    find_package(game-activity REQUIRED CONFIG)
    ...
    target_link_libraries(... android game-activity::game-activity)
    

    Ngoài ra, hãy đưa các tệp sau vào CmakeLists.txt của dự án: GameActivity.cpp, GameTextInput.cppandroid_native_app_glue.c.

Cách Android khởi chạy Hoạt động của bạn

Hệ thống Android thực thi mã trong thực thể của Hoạt động khi gọi phương thức gọi lại tương ứng với các giai đoạn cụ thể trong vòng đời hoạt động. Để Android khởi chạy hoạt động và bắt đầu trò chơi, bạn cần khai báo hoạt động với các thuộc tính thích hợp trong Tệp kê khai Android. Để biết thêm thông tin, hãy xem phần Giới thiệu về Hoạt động.

Tệp kê khai Android

Mỗi dự án ứng dụng phải có một tệp AndroidManifest.xml tại gốc của nhóm tài nguyên dự án. Tệp kê khai mô tả thông tin thiết yếu về ứng dụng của bạn cho các công cụ xây dựng của Android, hệ điều hành Android và Google Play. Trong đó có:

Triển khai GameActivity trong trò chơi của bạn

  1. Tạo hoặc xác định lớp Java hoạt động chính (lớp được chỉ định tại thành phần activity bên trong tệp AndroidManifest.xml). Thay đổi lớp này để mở rộng GameActivity từ gói com.google.androidgamesdk:

    import com.google.androidgamesdk.GameActivity;
    
    public class YourGameActivity extends GameActivity { ... }
    
  2. Bảo đảm thư viện gốc của bạn được tải ngay từ đầu bằng cách sử dụng một khối tĩnh:

    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. Thêm thư viện gốc của bạn vào AndroidManifest.xml nếu thư viện của bạn không có tên như mặc định (libmain.so):

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

Triển khai android_main

  1. Thư viện android_native_app_glue là một thư viện mã nguồn mà trò chơi của bạn dùng để quản lý các sự kiện trong vòng đời của GameActivity trong một luồng riêng nhằm tránh bị chặn trong luồng chính. Khi sử dụng thư viện, bạn đăng ký lệnh gọi lại để xử lý các sự kiện trong vòng đời (chẳng hạn như các sự kiện nhập bằng cách chạm). Kho lưu trữ GameActivity bao gồm phiên bản riêng của thư viện android_native_app_glue, vì vậy, bạn không thể sử dụng phiên bản có trong bản phát hành NDK. Nếu trò chơi của bạn đang sử dụng thư viện android_native_app_glue có trong NDK, hãy chuyển sang phiên bản GameActivity.

    Sau khi bạn thêm mã nguồn thư viện android_native_app_glue vào dự án, mã này sẽ tương tác với GameActivity. Triển khai một hàm có tên là android_main. Hàm này do thư viện gọi và được dùng làm điểm bắt đầu cho trò chơi của bạn. Hàm này nhận được một cấu trúc được gọi là android_app. Điều này có thể khác với trò chơi và công cụ của bạn. Ví dụ:

    #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. Xử lý android_app trong vòng lặp chính của trò chơi, chẳng hạn như thăm dò và xử lý các sự kiện trong chu kỳ ứng dụng được xác định trong NativeAppGlueAppCmd. Ví dụ: đoạn mã sau đây đăng ký hàm _hand_cmd_proxy là trình xử lý NativeAppGlueAppCmd, sau đó thăm dò sự kiện chu kỳ ứng dụng và gửi các sự kiện đó đến trình xử lý đã đăng ký (trong android_app::onAppCmd) để xử lý:

    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. Để biết thêm thông tin, hãy nghiên cứu cách triển khai mẫu NDK Endless Tunnel. Điểm khác biệt chính sẽ là cách xử lý các sự kiện như được trình bày trong phần tiếp theo.

Xử lý sự kiện

Để cho phép các sự kiện đầu vào truy cập ứng dụng, hãy tạo và đăng ký bộ lọc sự kiện bằng android_app_set_motion_event_filterandroid_app_set_key_event_filter. Theo mặc định, thư viện native_app_glue chỉ cho phép các sự kiện chuyển động từ phương thức nhập SOURCE_TOUCHSCREEN. Vui lòng xem tài liệu tham khảo và mã triển khai android_native_app_glue để biết thêm thông tin chi tiết.

Để xử lý các sự kiện đầu vào, hãy tham chiếu đến android_input_buffer bằng android_app_swap_input_buffers() trong vòng lặp trò chơi của bạn. Các tham số này chứa sự kiện chuyển độngsự kiện chính đã xảy ra kể từ lần gần đây nhất sự kiện đó được thăm dò ý kiến. Số lượng sự kiện trong đó được lưu trữ lần lượt tại motionEventsCountkeyEventsCount.

  1. Lặp và xử lý từng sự kiện trong vòng lặp trò chơi. Trong ví dụ này, đoạn mã sau đây sẽ lặp motionEvents và xử lý các sự kiện đó qua 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);
    }
    

    Hãy xem mẫu GitHub để biết cách triển khai _cookEventForPointerIndex() và các hàm có liên quan khác.

  2. Nhớ xoá các sự kiện mà bạn vừa xử lý sau khi thực hiện xong:

    android_app_clear_motion_events(mApp);
    

Tài nguyên khác

Để tìm hiểu thêm về GameActivity, hãy xem các nội dung sau:

Để báo cáo lỗi hoặc yêu cầu tính năng mới cho GameActivity, hãy sử dụng công cụ theo dõi lỗi của GameActivity.