نقل البيانات من NativeActivity   جزء من Android Game Development Kit

توضّح هذه الصفحة كيفية نقل البيانات من NativeActivity إلى GameActivity في مشروع لعبة Android.

تستند GameActivity إلى NativeActivity من إطار عمل Android، مع تحسينات وميزات جديدة:

  • تتوافق مع Fragment من Jetpack.
  • تضيف هذه السمة إمكانية استخدام TextInput لتسهيل دمج لوحة المفاتيح الافتراضية.
  • يتعامل مع أحداث اللمس والمفاتيح في فئة GameActivity Java بدلاً من واجهة NativeActivity onInputEvent.

قبل نقل البيانات، ننصحك بقراءة دليل البدء الذي يوضّح كيفية إعداد GameActivity ودمجه في مشروعك.

تعديلات على نص برمجة إنشاء 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. إذا كان الحد الأدنى الحالي لمستوى واجهة برمجة التطبيقات المتوافق أقل من 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 ...)
    

تعديلات على نصوص إنشاء C/C++ البرمجية

تستخدِم التعليمات الواردة في هذا القسم cmake كمثال. إذا كان تطبيقك يستخدم ndk-build، عليك ربطها بالأوامر المكافئة الموضّحة في صفحة مستندات ndk-build.

كانت عملية تنفيذ C/C++ في GameActivity توفّر إصدارًا من رمز المصدر. بالنسبة إلى الإصدار 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/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)
    
  • أزِل كل الإشارات إلى رمز 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. كما هو موضّح في الخطوة السابقة، تتم معالجة InputBufferGameActivity خارج حلقة ALooper_pollOnce().

  • استبدِل استخدام android_app::activity->clazz بـ android_app:: activity->javaGameActivity. تعيد GameActivity تسمية مثيل GameActivity Java.

خطوات إضافية

تغطّي الخطوات السابقة وظائف NativeActivity، ولكن GameActivity يتضمّن ميزات إضافية قد تحتاج إلى استخدامها:

ننصحك باستكشاف هذه الميزات واستخدامها بما يتناسب مع ألعابك.

إذا كانت لديك أي أسئلة أو اقتراحات بشأن GameActivity أو مكتبات أخرى في &quot;حزمة تطوير ألعاب Android&quot;، يُرجى إنشاء خطأ لإعلامنا بذلك.