איך מתחילים לעבוד עם GameActivity   חלק מ-Android Game Development Kit.

במדריך הזה מוסבר איך להגדיר ולשלב את GameActivity ולטפל באירועים במשחק ל-Android.

GameActivity עוזר לכם להעביר משחקים ב-C או ב-C++‎ ל-Android על ידי פישוט התהליך של שימוש בממשקי API חשובים. בעבר, NativeActivity היה הסיווג המומלץ למשחקים. ‫GameActivity מחליף אותו כסיווג המומלץ למשחקים, ויש לו תאימות לאחור עד לרמת API‏ 19.

דוגמה לשילוב של GameActivity זמינה במאגר הדוגמאות של משחקים.

לפני שמתחילים

אפשר לעיין בGameActivityמהדורות כדי לקבל הפצה.

הגדרת הדמות

ב-Android, ‏Activity משמש כנקודת הכניסה למשחק, וגם מספק את Window לציור בתוכו. הרבה משחקים מרחיבים את Activity באמצעות מחלקה משלהם ב-Java או ב-Kotlin כדי להתגבר על מגבלות ב-NativeActivity, תוך שימוש בקוד JNI כדי ליצור גשר לקוד המשחק ב-C או ב-C++‎.

GameActivity מציע את היכולות הבאות:

GameActivity מופץ כארכיון Android ‏(AAR). קובץ ה-AAR הזה מכיל את מחלקת Java שבה אתם משתמשים ב-AndroidManifest.xml, וגם את קוד המקור של C ו-C++‎ שמקשר בין הצד של Java ב-GameActivity לבין ההטמעה של C/C++‎ באפליקציה. אם אתם משתמשים בגרסה GameActivity 1.2.2 ואילך, מסופקת גם ספרייה סטטית של C/C++. במקרים שבהם זה רלוונטי, מומלץ להשתמש בספרייה הסטטית במקום בקוד המקור.

צריך לכלול את קובצי המקור האלה או את הספרייה הסטטית כחלק מתהליך הבנייה באמצעות Prefab, שחושף ספריות Native וקוד מקור לפרויקט CMake או לבניית NDK.

  1. פועלים לפי ההוראות שבדף Jetpack Android Games כדי להוסיף את התלות בספרייה GameActivity לקובץ build.gradle של המשחק.

  2. כדי להפעיל את התכונה Prefab, מבצעים את הפעולות הבאות עם Android Plugin Version‏ (AGP) 4.1 ואילך:

    • מוסיפים את הקוד הבא לבלוק android בקובץ build.gradle של המודול:
    buildFeatures {
        prefab true
    }
    
    • בוחרים גרסת Prefab ומגדירים אותה לקובץ gradle.properties:
    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)
    

    בנוסף, צריך לכלול את הקבצים הבאים ב-CmakeLists.txt של הפרויקט: GameActivity.cpp,‏ GameTextInput.cpp ו-android_native_app_glue.c.

איך Android מפעיל את הפעילות

מערכת Android מריצה קוד במופע של Activity על ידי הפעלת שיטות של קריאה חוזרת (callback) שתואמות לשלבים ספציפיים במחזור החיים של פעילות. כדי שמערכת Android תפעיל את הפעילות ותתחיל את המשחק, צריך להצהיר על הפעילות עם המאפיינים המתאימים במניפסט של Android. מידע נוסף זמין במאמר בנושא מבוא לפעילויות.

קובץ מניפסט של Android

לכל פרויקט אפליקציה צריך להיות קובץ AndroidManifest.xml בבסיס של קבוצת המקורות של הפרויקט. קובץ המניפסט מתאר מידע חיוני על האפליקציה לכלי הבנייה של 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. מוודאים שספריית Native נטענת בהתחלה באמצעות בלוק סטטי:

    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), צריך להוסיף את ספריית Native אל AndroidManifest.xml:

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

הטמעה של android_main

  1. הספרייה android_native_app_glue היא ספריית קוד מקור שהמשחק משתמש בה כדי לנהל אירועים במחזור החיים של GameActivity ב-thread נפרד, כדי למנוע חסימה ב-thread הראשי. כשמשתמשים בספרייה, רושמים את פונקציית הקריאה החוזרת כדי לטפל באירועים של מחזור החיים, כמו אירועים של קלט מגע. ארכיון 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.