بدء استخدام 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 Games لإضافة مكتبة "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 للنشاط الرئيسي أو حدِّدها (القيمة المحددة في العنصر 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. لمزيد من القراءة، ادرس تنفيذ مثال NDK Endless Tunnel. سيكون الاختلاف الرئيسي هو كيفية التعامل مع الأحداث كما هو موضح في القسم التالي.

التعامل مع الأحداث

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