איך מתחילים להשתמש ב-GameActivity   חלק מ-Android Game Development Kit.

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

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

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

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

אפשר להוריד הפצה מGameActivity גרסאות.

הגדרת ה-build

ב-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++. במקרים שבהם זה רלוונטי, מומלץ להשתמש בספרייה הסטטית במקום בקוד המקור.

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

  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 למודול 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 מריצה קוד במופע של Activity על ידי הפעלת שיטות של קריאה חוזרת (callback) שתואמות לשלבים ספציפיים במחזור החיים של Activity. כדי שמערכת Android תפעיל את הפעילות ותתחיל את המשחק, צריך להצהיר על הפעילות עם המאפיינים המתאימים בקובץ Android Manifest. מידע נוסף זמין במאמר מבוא לפעילויות.

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

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

    אפשר לראות את ההטמעה של _cookEventForPointerIndex() ופונקציות קשורות אחרות בדוגמה ב-GitHub.

  2. כשמסיימים, חשוב לזכור לנקות את תור האירועים שטופלו:

    android_app_clear_motion_events(mApp);
    

מקורות מידע נוספים

מידע נוסף על GameActivity זמין במקורות הבאים:

כדי לדווח על באגים או לבקש תכונות חדשות ב-GameActivity, אפשר להשתמש בכלי למעקב אחר בעיות ב-GameActivity.