نقل البيانات من NativeActivity جزء من مجموعة أدوات تطوير ألعاب Android.

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

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

  • يمكن استخدام Fragment من Jetpack.
  • إضافة دعم TextInput لتسهيل دمج لوحة المفاتيح اللينة.
  • لمعالجة أحداث اللمس والأحداث الرئيسية في فئة Java GameActivity بدلاً من واجهة 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 والإصدارات الأحدث. المكتبة الثابتة هي نوع الإصدار المقترَح.

تتم تعبئة الإصدار داخل ميزة "الاقتراحات المطبّقة تلقائيًا" مع أداة 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":

    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_pollAll(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 خارج نطاق حلقة ALooper_pollAll().

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

خطوات إضافية

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

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

إذا كان لديك أي أسئلة أو اقتراحات بشأن GameActivity أو غير ذلك من مكتبات AGDK، يُرجى إنشاء خطأ لإعلامنا بذلك.