เริ่มต้นใช้งาน 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, WebView หรือ 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 เพื่อเพิ่มการอ้างอิงไลบรารี GameActivity ลงในไฟล์ build.gradle ของเกม

  2. เปิดใช้ Prefab โดยทำดังนี้ด้วย ปลั๊กอิน Android เวอร์ชัน (AGP) 4.1 ขึ้นไป

    • เพิ่มโค้ดต่อไปนี้ลงในบล็อก android ของไฟล์ build.gradle ของโมดูล
    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.cpp, GameTextInput.cpp และ android_native_app_glue.c

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

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

ไฟล์ 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. หากต้องการอ่านเพิ่มเติม ให้ศึกษาการใช้งานตัวอย่าง NDK ของ Endless Tunnel ความแตกต่างหลักๆ จะอยู่ที่วิธีจัดการเหตุการณ์ดังที่แสดงใน ส่วนถัดไป

จัดการเหตุการณ์

หากต้องการให้เหตุการณ์อินพุตเข้าถึงแอป ให้สร้างและลงทะเบียนตัวกรองเหตุการณ์ด้วย android_app_set_motion_event_filter และ android_app_set_key_event_filter โดยค่าเริ่มต้น native_app_glue library จะอนุญาตเฉพาะเหตุการณ์การเคลื่อนไหวจากอินพุต 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);
    }
    

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

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

    android_app_clear_motion_events(mApp);
    

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

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

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