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

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

GameActivity עוזר לכם להביא את C או משחק C++ ל-Android בעזרת פישוט של תהליך השימוש בממשקי API קריטיים. בעבר NativeActivity היה הכיתה המומלצת למשחקים. GameActivity מחליף אותו בתור המומלץ class for Games, והיא תואמת לאחור לרמת 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++ יש גם ספרייה סטטית. במקרים הרלוונטיים, מומלץ להשתמש בספרייה הסטטית במקום בקוד המקור.

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

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

  2. כדי להפעיל את הרכיב הקבוע, צריך לבצע את הפעולות הבאות עם גרסת הפלאגין של Android (AGP) 4.1 ואילך:

    • מוסיפים את הקוד הבא לבלוק android בקובץ build.gradle של המודול:
    buildFeatures {
        prefab true
    }
    
    • בוחרים גרסה Prefab. ומגדירים אותו לקובץ gradle.properties:
    android.prefabVersion=2.0.0
    

    אם אתם משתמשים בגרסאות AGP קודמות, צריך לפעול לפי את התיעוד של המוצרים המועדפים כדי למצוא את הוראות ההגדרה המתאימות.

  3. מייבאים את הספרייה הסטטית C/C++ או את קוד המקור C/++ אל פרויקט כפי שמפורט בהמשך.

    ספרייה סטטית

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

מניפסט ל-Android

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

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

הטמעת android_main

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