بدء استخدام 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 project أو إصدار NDK.

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

  2. قم بتفعيل ميزة Prefab من خلال تنفيذ ما يلي باستخدام إصدار 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" المضمّنة في اتفاقية عدم الإفصاح، يُرجى التبديل إلى إصدار 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. يكمن الاختلاف الرئيسي في كيفية التعامل مع الأحداث كما هو موضّح في القسم التالي.

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

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