מעבר מ-NativeActivity   חלק מ-Android Game Development Kit.

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

GameActivity מבוסס על NativeActivity מהמסגרת של Android, עם שיפורים ותכונות חדשות:

  • תמיכה ב-Fragment מ-Jetpack.
  • הוספנו תמיכה ב-TextInput כדי להקל על השילוב עם מקלדת וירטואלית.
  • טיפול באירועי מגע ובאירועי מקש בכיתה Java‏ GameActivity במקום בממשק NativeActivity onInputEvent.

לפני ההעברה, מומלץ לקרוא את המדריך למתחילים, שבו מוסבר איך להגדיר ולשלב את GameActivity בפרויקט.

עדכונים של סקריפטים ל-build של Java

GameActivity מופץ כספרייה של Jetpack. חשוב לבצע את השלבים לעדכון סקריפט Gradle שמפורטים במדריך למתחילים:

  1. מפעילים את ספריית Jetpack בקובץ gradle.properties של הפרויקט:

    android.useAndroidX=true
    
  2. אפשר גם לציין גרסת Prefab באותו קובץ gradle.properties, לדוגמה:

    android.prefabVersion=2.0.0
    
  3. מפעילים את התכונה Prefab בקובץ build.gradle של האפליקציה:

    android {
        ... // other configurations
        buildFeatures.prefab true
    }
    
  4. מוסיפים את יחסי התלות GameActivity לאפליקציה:

    1. מוסיפים את הספריות core ו-games-activity.
    2. אם רמת ה-API הנתמכת הנוכחית היא פחות מ-16, צריך לעדכן אותה ל-16 לפחות.
    3. מעדכנים את גרסת ה-SDK המתומצתת לגרסת ה-SDK שנדרשת לספרייה games-activity. בדרך כלל, כדי להשתמש ב-Jetpack נדרשת גרסת ה-SDK העדכנית ביותר בזמן ה-build של הגרסה.

    קובץ build.gradle המעודכן עשוי להיראות כך:

    android {
        compiledSdkVersion 33
        ... // other configurations.
        defaultConfig {
            minSdkVersion 16
        }
        ... // other configurations.
    
        buildFeatures.prefab true
    }
    dependencies {
        implementation 'androidx.core:core:1.9.0'
        implementation 'androidx.games:games-activity:1.2.2'
    }
    

עדכוני קוד ב-Kotlin או ב-Java

אפשר להשתמש ב-NativeActivity כפעילות לטעינה בזמן ההפעלה, והיא יוצרת אפליקציה במסך מלא. בשלב זה, אי אפשר להשתמש ב-GameActivity כפעילות ההפעלה. אפליקציות צריכות להסיק סיווג מ-GameActivity ולהשתמש בו כפעילות ההפעלה. צריך גם לבצע שינויים נוספים בהגדרות כדי ליצור אפליקציה במסך מלא.

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

  1. יוצרים קובץ Kotlin או Java שיארח את הפעילות החדשה של סטארט-אפ. לדוגמה, הקוד הבא יוצר את MainActivity כפעילות ההפעלה ומטעין את הספרייה הראשית של האפליקציה, libAndroidGame.so:

    Kotlin

    class MainActivity : GameActivity() {
       override fun onResume() {
           super.onResume()
           // Use the function recommended from the following page:
           // https://d.android.com/training/system-ui/immersive
           hideSystemBars()
       }
       companion object {
           init {
               System.loadLibrary("AndroidGame")
           }
       }
    }

    Java

      public class MainActivity extends GameActivity {
          protected void onResume() {
              super.onResume();
              // Use the function recommended from
              // https://d.android.com/training/system-ui/immersive
              hideSystemBars();
          }
          static {
              System.loadLibrary("AndroidGame");
          }
      }
  2. יוצרים עיצוב של אפליקציה במסך מלא בקובץ res\values\themes.xml:

    <resources xmlns:tools="http://schemas.android.com/tools">
        <!-- Base application theme. -->
        <style name="Application.Fullscreen" parent="Theme.AppCompat.Light.NoActionBar">
            <item name="android:windowFullscreen">true</item>
            <item name="android:windowContentOverlay">@null</item>"
        </style>
    </resources>
    
  3. מחילים את העיצוב על האפליקציה בקובץ AndroidManifest.xml:

    <application  android:theme=”@style/Application.Fullscreen”>
         <!-- other configurations not listed here. -->
    </application>
    

    הוראות מפורטות לגבי מצב מסך מלא זמינות במדריך למצב immersive ובדוגמה להטמעה במאגר games-samples.

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

  • קוד Kotlin או Java:

    System.loadLibrary(AndroidGame)
    
  • AndroidManifest.xml:

    <meta-data android:name="android.app.lib_name"
            android:value="AndroidGame" />
    
  • בתוך קובץ סקריפט ה-build של C/C++, לדוגמה CMakeLists.txt:

    add_library(AndroidGame ...)
    

עדכונים של סקריפטים ל-build ב-C/C++‎

בהוראות בקטע הזה נעשה שימוש ב-cmake כדוגמה. אם האפליקציה שלכם משתמשת ב-ndk-build, תצטרכו למפות אותה לפקודות המקבילות שמתוארות בדף המסמכים של ndk-build.

ההטמעה של GameActivity ב-C/C++ מספקת גרסה של קוד המקור. בגרסה 1.2.2 ואילך, קיימת גרסה סטטית של הספרייה. הספרייה הסטטית היא סוג הגרסה המומלץ.

הגרסה נארזת בתוך ה-AAR באמצעות הכלי prefab. הקוד המקורי כולל את מקורות ה-C/C++ של GameActivity ואת הקוד native_app_glue. צריך לבנות אותם יחד עם קוד ה-C/C++ של האפליקציה.

אפליקציות NativeActivity כבר משתמשות בקוד native_app_glue שנשלח ב-NDK. צריך להחליף אותו בגרסה של native_app_glue ב-GameActivity. חוץ מזה, כל cmake השלבים שמפורטים במדריך לתחילת העבודה רלוונטיים:

  • מייבאים את הספרייה הסטטית של 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)
    
  • מסירים את כל ההפניות לקוד native_app_glue של NDK, למשל:

    ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c
        ...
    set(CMAKE_SHARED_LINKER_FLAGS
        "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate")
    
  • אם אתם משתמשים במהדורת קוד המקור, צריך לכלול את קובצי המקור של GameActivity. אם לא, מדלגים על השלב הזה.

    get_target_property(game-activity-include
                        game-activity::game-activity
                        INTERFACE_INCLUDE_DIRECTORIES)
    add_library(${PROJECT_NAME} SHARED
        main.cpp
        ${game-activity-include}/game-activity/native_app_glue/android_native_app_glue.c
        ${game-activity-include}/game-activity/GameActivity.cpp
        ${game-activity-include}/game-text-input/gametextinput.cpp)
    

פתרון לבעיה UnsatisfiedLinkError

אם נתקלתם ב-UnsatsifiedLinkError לפונקציה com.google.androidgamesdk.GameActivity.initializeNativeCode(), מוסיפים את הקוד הזה לקובץ CMakeLists.txt:

set(CMAKE_SHARED_LINKER_FLAGS
    "${CMAKE_SHARED_LINKER_FLAGS} -u \
    Java_com_google_androidgamesdk_GameActivity_initializeNativeCode")

עדכונים בקוד המקור של C/C++‎

כדי להחליף את ההפניות ל-NativeActivity באפליקציה ב-GameActivity:

  • משתמשים ב-native_app_glue שפורסם עם GameActivity. מחפשים את כל השימוש ב-android_native_app_glue.h ומחליפים אותו בקוד הבא:

    #include <game-activity/native_app_glue/android_native_app_glue.h>
    
  • מגדירים את מסנן אירועי התנועה ואת מסנן האירועים המרכזיים ל-NULL כדי שהאפליקציה תוכל לקבל אירועי קלט מכל מכשירי הקלט. בדרך כלל עושים זאת בתוך הפונקציה android_main():

    void android_main(android_app* app) {
        ... // other init code.
    
        android_app_set_key_event_filter(app, NULL);
        android_app_set_motion_event_filter(app, NULL);
    
        ... // additional init code, and game loop code.
    }
    
  • מסירים את הקוד שקשור ל-AInputEvent ומחליפים אותו בהטמעה של InputBuffer ב-GameActivity:

    while (true) {
        // Read all pending events.
        int events;
        struct android_poll_source* source;
    
        // If not animating, block forever waiting for events.
        // If animating, loop until all events are read, then continue
        // to draw the next frame of animation.
        while ((ALooper_pollOnce(engine.animating ? 0 : -1, nullptr, &events,
                                (void**)&source)) >= 0) {
           // Process this app cycle or inset change event.
           if (source) {
               source->process(source->app, source);
           }
    
              ... // Other processing.
    
           // Check if app is exiting.
           if (state->destroyRequested) {
               engine_term_display(&engine);
               return;
           }
        }
        // Process input events if there are any.
        engine_handle_input(state);
    
       if (engine.animating) {
           // Draw a game frame.
       }
    }
    
    // Implement input event handling function.
    static int32_t engine_handle_input(struct android_app* app) {
       auto* engine = (struct engine*)app->userData;
       auto ib = android_app_swap_input_buffers(app);
       if (ib && ib->motionEventsCount) {
           for (int i = 0; i < ib->motionEventsCount; i++) {
               auto *event = &ib->motionEvents[i];
               int32_t ptrIdx = 0;
               switch (event->action & AMOTION_EVENT_ACTION_MASK) {
                   case AMOTION_EVENT_ACTION_POINTER_DOWN:
                   case AMOTION_EVENT_ACTION_POINTER_UP:
                       // Retrieve the index for the starting and the ending of any secondary pointers
                       ptrIdx = (event->action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >>
                                AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
                   case AMOTION_EVENT_ACTION_DOWN:
                   case AMOTION_EVENT_ACTION_UP:
                       engine->state.x = GameActivityPointerAxes_getAxisValue(
                           &event->pointers[ptrIdx], AMOTION_EVENT_AXIS_X);
                       engine->state.y = GameActivityPointerAxes_getAxisValue(
                           &event->pointers[ptrIdx], AMOTION_EVENT_AXIS_Y);
                       break;
                    case AMOTION_EVENT_ACTION_MOVE:
                    // Process the move action: the new coordinates for all active touch pointers
                    // are inside the event->pointers[]. Compare with our internally saved
                    // coordinates to find out which pointers are actually moved. Note that there is
                    // no index embedded inside event->action for AMOTION_EVENT_ACTION_MOVE (there
                    // might be multiple pointers moved at the same time).
                        ...
                       break;
               }
           }
           android_app_clear_motion_events(ib);
       }
    
       // Process the KeyEvent in a similar way.
           ...
    
       return 0;
    }
    
  • בודקים ומעדכנים את הלוגיקה שמצורפת ל-AInputEvent של NativeActivity. כפי שמוצג בשלב הקודם, העיבוד של InputBuffer ב-GameActivity מתבצע מחוץ ל-loop‏ ALooper_pollOnce().

  • מחליפים את השימוש ב-android_app::activity->clazz ב-android_app:: activity->javaGameActivity. GameActivity משנה את השם של מכונה GameActivity ב-Java.

שלבים נוספים

השלבים הקודמים מתארים את הפונקציונליות של NativeActivity, אבל ל-GameActivity יש תכונות נוספות שיכול להיות שתרצו להשתמש בהן:

מומלץ לבדוק את התכונות האלה ולהשתמש בהן בהתאם למשחקים שלכם.

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