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

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

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

  • תמיכה ב-Fragment מ-Jetpack.
  • הוספנו תמיכה ב-TextInput כדי להקל על שילוב של מקלדת וירטואלית.
  • מטפל באירועי מגע ואירועי מקשים ב-GameActivity Java class במקום בממשק 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 שעברה קומפילציה לגרסה שנדרשת בספרייה games-activity. בדרך כלל, כדי להשתמש ב-Jetpack צריך את גרסת ה-SDK העדכנית ביותר בזמן בניית הגרסה.

    קובץ 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>
    

    הוראות מפורטות לגבי מצב מסך מלא זמינות במדריך המקיף ודוגמה להטמעה זמינה במאגר הדוגמאות של משחקים.

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

  • קוד Kotlin או Java:

    System.loadLibrary(AndroidGame)
    
  • AndroidManifest.xml:

    <meta-data android:name="android.app.lib_name"
            android:value="AndroidGame" />
    
  • בתוך קובץ הסקריפט של 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 למודול 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)
    
  • מסירים את כל ההפניות לקוד 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;
    }
    
  • בודקים ומעדכנים את הלוגיקה שמצורפת ל-NativeActivity’s AInputEvent. כפי שמוצג בשלב הקודם, העיבוד של GameActivity‏ InputBuffer מתבצע מחוץ ללולאה ALooper_pollOnce().

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

שלבים נוספים

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

מומלץ להתנסות בתכונות האלה ולהטמיע אותן במשחקים שלכם לפי הצורך.

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