بدء استخدام GameActivity   جزء من Android Game Development Kit

يوضّح هذا الدليل كيفية إعداد ودمج 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++‎ في التطبيق. إذا كنت تستخدم الإصدار GameActivity 1.2.2 أو إصدارًا أحدث، سيتم أيضًا توفير مكتبة C/C++‎ الثابتة. ننصحك باستخدام المكتبة الثابتة بدلاً من رمز المصدر كلما كان ذلك ممكنًا.

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

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

  2. فعِّل العنصر الجاهز من خلال تنفيذ ما يلي باستخدام الإصدار 4.1 من المكوّن الإضافي لنظام Android المتوافق مع Gradle أو الإصدارات الأحدث:

    • أضِف ما يلي إلى قسم 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 من بدء نشاطك وتشغيل لعبتك، عليك تعريف نشاطك باستخدام السمات المناسبة في ملف AndroidManifest. لمزيد من المعلومات، يُرجى الاطّلاع على مقدمة عن الأنشطة.

ملف بيان Android

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

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

  1. أنشئ فئة 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_pollOnce(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. للحصول على معلومات إضافية، يمكنك الاطّلاع على مثال النفق اللانهائي في 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.