بدء استخدام GameActivity جزء من حزمة تطوير ألعاب Android.

يشرح هذا الدليل طريقة إعداد GameActivity ودمجه والتعامل مع الأحداث في لعبة Android.

يساعدك GameActivity في توفير لعبة C أو C++ على Android من خلال تبسيط عملية استخدام واجهات برمجة التطبيقات المهمة. كان NativeActivity في السابق الفصل الموصى به للألعاب. GameActivity تستبدلها بالفئة المقترَحة للألعاب، وتتوافق مع الأنظمة القديمة مع المستوى 19 من واجهة برمجة التطبيقات.

للاطّلاع على نموذج يدمج GameActivity، يُرجى مراجعة مستودع نماذج الألعاب.

قبل البدء

اطّلِع على إصدارات GameActivity للحصول على عملية توزيع.

إعداد إصدارك

على نظام التشغيل Android، تمثّل Activity نقطة الدخول إلى لعبتك وتوفّر أيضًا علامة Window التي يمكن الرسم عليها. تستخدم العديد من الألعاب هذا Activity بفئة Java أو Kotlin الخاصة بها للتغلّب على القيود في NativeActivity عند استخدام الرمز JNI لنقل رمز اللعبة إلى C أو C++.

يوفّر GameActivity الإمكانيات التالية:

يتم توزيع GameActivity باعتباره أرشيف Android (AAR). يحتوي تطبيق AAR هذا على فئة Java التي تستخدمها في AndroidManifest.xml، بالإضافة إلى رمز المصدر C وC++ الذي يربط جانب Java في GameActivity بتطبيق C/C++ الخاص بالتطبيق. إذا كنت تستخدم الإصدار 1.2.2 من "GameActivity" أو إصدار أحدث، تتوفّر أيضًا المكتبة الثابتة بلغة C/C++. نوصيك باستخدام المكتبة الثابتة، بدلاً من الكود المصدر، كلما أمكن ذلك.

يمكنك تضمين ملفات المصدر هذه أو المكتبة الثابتة كجزء من عملية الإنشاء من خلال Prefab، التي تعرض المكتبات الأصلية ورمز المصدر إلى مشروع CMake أو إصدار NDK.

  1. اتّبِع التعليمات الواردة في صفحة ألعاب Jetpack على أجهزة Android لإضافة إضافة مكتبة "GameActivity" الاعتمادية إلى ملف build.gradle الخاص بلعبتك.

  2. فعّل الإعداد المسبق عن طريق تنفيذ ما يلي باستخدام إصدار مكوّن Android الإضافي (AGP) 4.1 أو الإصدارات الأحدث:

    • أضف ما يلي إلى المجموعة android من ملف build.gradle في وحدتك:
    buildFeatures {
        prefab true
    }
    
    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 التعليمات البرمجية في مثيل نشاطك عن طريق استدعاء طرق استدعاء تتوافق مع مراحل محددة من دورة حياة النشاط. لكي يشغّل Android نشاطك ويبدأ تشغيل لعبتك، يجب الإعلان عن نشاطك باستخدام السمات المناسبة في بيان Android. للحصول على مزيد من المعلومات، راجع مقدمة عن الأنشطة.

بيان Android

يجب أن يشتمل كل مشروع تطبيق على ملف AndroidManifest.xml في جذر مجموعة مصدر المشروع. يصف ملف البيان المعلومات الأساسية حول تطبيقك في أدوات إصدار Android ونظام التشغيل Android وGoogle Play. وتتضمّن المزايا ما يلي:

تنفيذ GameActivity في لعبتك

  1. أنشئ أو حدِّد النشاط الرئيسي لفئة Java (فئة 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. أضِف اسم مكتبتك الأصلية إلى AndroidManifest.xml إذا لم يكن اسم مكتبتك هو الاسم التلقائي (libmain.so):

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

تنفيذ android_main

  1. إنّ مكتبة android_native_app_glue هي مكتبة رموز مصدر تستخدمها لعبتك لإدارة أحداث مراحل نشاط GameActivity ضمن سلسلة محادثات منفصلة من أجل منع الحظر في سلسلة التعليمات الرئيسية. عند استخدام المكتبة، يتم تسجيل معاودة الاتصال لمعالجة أحداث مراحل النشاط، مثل أحداث الإدخال باللمس. يتضمن أرشيف 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 كمعالج NativeAppGlueAppCmd، ثم يستعلم أحداث دورة التطبيق ويرسلها إلى المعالج المسجَّل(في 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. لمزيد من القراءة، يمكنك دراسة تنفيذ مثال Inless 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.