เริ่มต้นใช้งาน GameActivity   ซึ่งเป็นส่วนหนึ่งของ Android Game Development Kit.

คู่มือนี้อธิบายวิธีตั้งค่าและผสานรวม GameActivity รวมถึงวิธีจัดการเหตุการณ์ในเกม Android

GameActivity ช่วยให้คุณนำเกมที่สร้างด้วย C หรือ C++ มาใช้ใน Android ได้ด้วยการทำให้กระบวนการใช้ API ที่สำคัญง่ายขึ้น ก่อนหน้านี้ NativeActivity เป็นคลาสที่แนะนำสำหรับเกม GameActivity จะเข้ามาแทนที่ในฐานะคลาสที่แนะนำสำหรับเกม และเข้ากันได้แบบย้อนหลังกับ API ระดับ 19

ดูตัวอย่างที่ผสานรวม GameActivity ได้ที่ ที่เก็บ games-samples

ก่อนจะเริ่มต้น

ดูการเผยแพร่ GameActivity เพื่อ รับการเผยแพร่

ตั้งค่าบิลด์ของคุณ

ใน Android นั้น Activity จะทำหน้าที่เป็นจุดเริ่มต้นของเกม และยังให้ Window สำหรับวาดด้วย เกมจำนวนมากขยาย สิ่งนี้ Activityด้วยคลาส Java หรือ Kotlin ของตนเองเพื่อขจัดข้อจำกัดใน NativeActivityขณะที่ใช้ JNIโค้ดเพื่อเชื่อมโยง กับโค้ดเกม C หรือ C++

GameActivity มีความสามารถดังต่อไปนี้

  • สืบทอดมาจาก AppCompatActivityซึ่งช่วยให้คุณใช้คอมโพเนนต์สถาปัตยกรรม Android Jetpackได้

  • แสดงผลใน SurfaceView ที่ช่วยให้คุณ เชื่อมต่อกับองค์ประกอบ UI อื่นๆ ของ Android ได้

  • จัดการเหตุการณ์กิจกรรม Java ซึ่งช่วยให้องค์ประกอบ UI ของ Android (เช่น EditText, a WebView หรือ an Ad) ผสานรวมกับเกมของคุณผ่านอินเทอร์เฟซ C ได้

  • มี C API ที่คล้ายกับ NativeActivity และ android_native_app_glue ไลบรารี

GameActivity เผยแพร่เป็น Android Archive (AAR) AAR นี้มีคลาส Java ที่ คุณใช้ใน AndroidManifest.xml รวมถึงซอร์สโค้ด C และ C++ ที่เชื่อมต่อฝั่ง Java ของ GameActivity กับการติดตั้งใช้งาน C/C++ ของแอป หากคุณใช้ GameActivity 1.2.2 ขึ้นไป ระบบจะให้ไลบรารีแบบคงที่ C/C++ ด้วย เราขอแนะนำให้คุณใช้ไลบรารีแบบคงที่แทนซอร์สโค้ดทุกครั้งที่ทำได้

รวมไฟล์ซอร์สหรือไลบรารีแบบคงที่เหล่านี้เป็นส่วนหนึ่งของกระบวนการบิลด์ ผ่าน Prefab, ซึ่งแสดงไลบรารีแบบเนทีฟและซอร์สโค้ดไปยัง โปรเจ็กต์ CMake หรือ การสร้าง NDK

  1. ทำตามวิธีการในหน้า Jetpack Android Games เพื่อเพิ่มทรัพยากร Dependency ของไลบรารี GameActivity ลงในไฟล์ build.gradle ของเกม

  2. เปิดใช้ Prefab โดยทำดังนี้ด้วย Android Plugin Version (AGP) 4.1 ขึ้นไป:

    • เพิ่มโค้ดต่อไปนี้ลงในบล็อก android ของไฟล์ build.gradle ของโมดูล
    buildFeatures {
        prefab true
    }
    
    android.prefabVersion=2.0.0
    

    หากคุณใช้ AGP เวอร์ชันก่อนหน้า ให้ทำตาม เอกสารประกอบของ Prefab เพื่อดูวิธีการกำหนดค่าที่เกี่ยวข้อง

  3. นำเข้าไลบรารีแบบคงที่ C/C++ หรือซอร์สโค้ด C/++ ลงในโปรเจ็กต์ดังนี้

    ไลบรารีแบบคงที่

    ในไฟล์ CMakeLists.txt ของโปรเจ็กต์ ให้นำเข้าไลบรารีแบบคงที่ game-activity ลงในโมดูล Prefab game-activity_static ดังนี้

    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)
    

    นอกจากนี้ ให้รวมไฟล์ GameActivity.cpp, GameTextInput.cpp และ android_native_app_glue.c ลงใน CmakeLists.txt ของโปรเจ็กต์ด้วย

วิธีที่ Android เปิดใช้กิจกรรม

ระบบ Android จะเรียกใช้โค้ดในอินสแตนซ์กิจกรรมโดยเรียกใช้เมธอดเรียกกลับที่สอดคล้องกับระยะต่างๆ ของวงจรกิจกรรม หากต้องการให้ Android เปิดใช้กิจกรรมและเริ่มเกม คุณต้องประกาศกิจกรรมด้วยแอตทริบิวต์ที่เหมาะสมในไฟล์ Manifest ของ Android ดูข้อมูลเพิ่มเติมได้ที่ ข้อมูลเบื้องต้นเกี่ยวกับกิจกรรม

ไฟล์ Manifest ของ Android

ทุกโปรเจ็กต์แอปต้องมีไฟล์ AndroidManifest.xml ที่ รูทของชุดซอร์สโค้ดของโปรเจ็กต์ ไฟล์ Manifest อธิบายข้อมูลสำคัญเกี่ยวกับแอปให้กับเครื่องมือสร้าง Android, ระบบปฏิบัติการ Android และ Google Play ซึ่งรวมถึงสิ่งต่อไปนี้

ติดตั้งใช้งาน GameActivity ในเกม

  1. สร้างหรือระบุคลาส Java ของกิจกรรมหลัก (คลาสที่ระบุในองค์ประกอบ activity ภายในไฟล์ AndroidManifest.xml) เปลี่ยนคลาสนี้ให้ขยาย GameActivity จากแพ็กเกจ com.google.androidgamesdk ดังนี้

    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. เพิ่มไลบรารีแบบเนทีฟลงใน AndroidManifest.xml หากชื่อไลบรารีไม่ใช่ชื่อเริ่มต้น (libmain.so)

    <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 ไม่ได้ หากเกมของคุณใช้ไลบรารี android_native_app_glue ที่รวมอยู่ใน NDK ให้เปลี่ยนไปใช้เวอร์ชัน 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_filter และ android_app_set_key_event_filter โดยค่าเริ่มต้น ไลบรารี native_app_glue จะอนุญาตเฉพาะเหตุการณ์การเคลื่อนไหวจากการป้อนข้อมูล SOURCE_TOUCHSCREEN โปรดดูรายละเอียดใน เอกสารอ้างอิง และโค้ดการติดตั้งใช้งาน android_native_app_glue

หากต้องการจัดการเหตุการณ์การป้อนข้อมูล ให้รับการอ้างอิงไปยัง android_input_buffer ด้วย android_app_swap_input_buffers() ในลูปเกม บัฟเฟอร์เหล่านี้มีเหตุการณ์การเคลื่อนไหวและเหตุการณ์สำคัญที่เกิดขึ้นนับตั้งแต่ครั้งล่าสุดที่ โพล จำนวนเหตุการณ์ที่รวมอยู่จะจัดเก็บไว้ใน motionEventsCount และ keyEventsCount ตามลำดับ

  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);
    }
    

    ดูตัวอย่าง GitHub สำหรับการติดตั้งใช้งาน _cookEventForPointerIndex() และฟังก์ชันอื่นๆ ที่เกี่ยวข้อง

  2. เมื่อเสร็จแล้ว อย่าลืมล้างคิวเหตุการณ์ที่คุณเพิ่งจัดการ

    android_app_clear_motion_events(mApp);
    

แหล่งข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับ GameActivity ได้ที่

หากต้องการรายงานข้อบกพร่องหรือส่งคำขอฟีเจอร์ใหม่สำหรับ GameActivity ให้ใช้ เครื่องมือติดตามปัญหาของ GameActivity