التعامل مع إجراءات وحدة التحكّم

على مستوى النظام، يُبلغ Android عن رموز أحداث الإدخال من أجهزة تحكّم الألعاب ك رموز مفاتيح Android وقيم المحاور. في لعبتك، يمكنك الحصول على هذه الرموز والقيم وتحويلها إلى إجراءات محددة داخل اللعبة.

عندما يربط اللاعبون وحدة تحكّم في الألعاب بشكلٍ مباشر أو يقرنونها لاسلكيًا بأجهزتهم التي تعمل بنظام التشغيل Android، يرصد النظام تلقائيًا وحدة التحكّم كجهاز إدخال ويبدأ في الإبلاغ عن أحداث الإدخال. يمكن للعبة تلقّي أحداث الإدخال هذه من خلال تنفيذ طرق الاستدعاء التالية في Activity النشط أوView الذي تم التركيز عليه (يجب تنفيذ عمليات الاستدعاء إما لActivity أو View، ولكن ليس كليهما):

والطريقة المقترحة هي تسجيل الأحداث من عنصر View المحدّد الذي يتفاعل معه المستخدم. تحقّق من العناصر التالية المقدَّمة من وظائف الاستدعاء للحصول على معلومات عن نوع حدث الإدخال الذي تم تلقّيه:

KeyEvent
عنصر يصف أحداث keypad (D-pad) وزرّات لوحة الألعاب تَترافق الأحداث الرئيسية مع رمز مفتاح يشير إلى الزر المحدّد الذي تم تفعيله، مثل DPAD_DOWN أو BUTTON_A. يمكنك الحصول على رمز المفتاح من خلال استدعاء getKeyCode() أو من استدعاءات الأحداث الرئيسية مثل onKeyDown().
MotionEvent
عبارة عن كائن يصف المدخلات من الحركات التي تحركها ذراع التحكم والكتف. تَترافق أحداث الحركة مع رمز إجراء ومجموعة من قيم المحاور. يحدِّد رمز الإجراء تغيير الحالة الذي حدث، مثل تحريك ذراع تحكم. تصف قيم المحاور الموضع وسمات التحرك الأخرى لعنصر تحكّم مادي محدّد، مثل AXIS_X أو AXIS_RTRIGGER. يمكنك الحصول على رمز الإجراء عن طريق استدعاء getAction() وقيمة المحور من خلال استدعاء getAxisValue().

يركز هذا الدرس على كيفية معالجة الإدخال من الأنواع الأكثر شيوعًا من عناصر التحكّم المادية (أزرار لوحة الألعاب ولوحات التوجيه ومقبض التوجيه) في شاشة اللعبة من خلال تنفيذ مثيل View المذكور أعلاه ومعالجة كائنَي KeyEvent وMotionEvent.

التأكّد من توصيل ذراع التحكّم في الألعاب

عند الإبلاغ عن أحداث الإدخال، لا يميز نظام التشغيل Android بين الأحداث الواردة من جهاز غير ذراع تحكّم في الألعاب والأحداث الواردة من ذراع تحكّم في الألعاب. على سبيل المثال، يُنشئ إجراء الشاشة التي تعمل باللمس حدثًا AXIS_X يمثّل الإحداثي x لسطح اللمس، ولكنّ ذراع التحكم يُنشئ حدثًا AXIS_X يمثّل موضع x لذراع التحكم. إذا كانت لعبتك تهتم بمعالجة إدخالات وحدة التحكم في الألعاب، يجب أولاً التحقق من أن حدث الإدخال يأتي من نوع مصدر ذي صلة.

للتأكّد من أنّ جهاز الإدخال المتصل هو جهاز تحكّم في الألعاب، اتصل بالخدمة getSources() للحصول على حقل بتات مجمّع لأنواع مصادر الإدخال المتوافقة على هذا الجهاز. يمكنك بعد ذلك اختبار ما إذا كانت الحقول التالية قد تم ضبطها:

  • يشير نوع المصدر SOURCE_GAMEPAD إلى أنّ جهاز الإدخال يحتوي على أزرار جهاز تحكّم في الألعاب (على سبيل المثال، BUTTON_A). يُرجى العِلم أنّ نوع المصدر هذا لا يشير بدقة إلى ما إذا كان جهاز التحكّم في الألعاب يحتوي على أزرار لوحة ألعاب، على الرغم من أنّ معظم أجهزة التحكّم في الألعاب تحتوي عادةً على عناصر تحكّم اتجاهية.
  • يشير نوع المصدر SOURCE_DPAD إلى أنّه يحتوي جهاز الإدخال على أزرار لوحة التوجيه (مثل DPAD_UP).
  • يشير نوع المصدر SOURCE_JOYSTICK إلى أنّ جهاز الإدخال يتضمّن أعواد تحكّم تناظرية (على سبيل المثال، ذراع تحكّم يسجّل الحركات على طول AXIS_X وAXIS_Y).

يعرض مقتطف الرمز التالي طريقة مساعدة تتيح لك التحقّق مما إذا كانت أجهزة الإدخال المتصلة هي وحدات تحكّم في الألعاب. إذا كان الأمر كذلك، تسترجع الطريقة أرقام تعريف الأجهزة لذراعات التحكّم في الألعاب. يمكنك بعد ذلك ربط كل رقم تعريف جهاز بلاعب في لعبتك، ومعالجة إجراءات اللعبة لكل لاعب مرتبط بشكل منفصل. لمزيد من المعلومات حول إتاحة استخدام عدّة أذرع تحكّم في الألعاب تكون متصلة في الوقت نفسه على جهاز Android نفسه، يُرجى الاطّلاع على مقالة إتاحة استخدام عدّة أذرع تحكّم في الألعاب.

Kotlin

fun getGameControllerIds(): List<Int> {
    val gameControllerDeviceIds = mutableListOf<Int>()
    val deviceIds = InputDevice.getDeviceIds()
    deviceIds.forEach { deviceId ->
        InputDevice.getDevice(deviceId).apply {

            // Verify that the device has gamepad buttons, control sticks, or both.
            if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD
                    || sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) {
                // This device is a game controller. Store its device ID.
                gameControllerDeviceIds
                        .takeIf { !it.contains(deviceId) }
                        ?.add(deviceId)
            }
        }
    }
    return gameControllerDeviceIds
}

Java

public ArrayList<Integer> getGameControllerIds() {
    ArrayList<Integer> gameControllerDeviceIds = new ArrayList<Integer>();
    int[] deviceIds = InputDevice.getDeviceIds();
    for (int deviceId : deviceIds) {
        InputDevice dev = InputDevice.getDevice(deviceId);
        int sources = dev.getSources();

        // Verify that the device has gamepad buttons, control sticks, or both.
        if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
                || ((sources & InputDevice.SOURCE_JOYSTICK)
                == InputDevice.SOURCE_JOYSTICK)) {
            // This device is a game controller. Store its device ID.
            if (!gameControllerDeviceIds.contains(deviceId)) {
                gameControllerDeviceIds.add(deviceId);
            }
        }
    }
    return gameControllerDeviceIds;
}

بالإضافة إلى ذلك، يمكنك التحقّق من إمكانات الإدخال الفردية التي توفّرها وحدة تحكّم متصلة في الألعاب. قد يكون هذا مفيدًا، على سبيل المثال، إذا أردت أن تستخدم لعبتك الإدخال من مجموعة عناصر التحكّم الملموسة التي تفهمها فقط.

لرصد ما إذا كان رمز مفتاح أو رمز محور معيّنان متوافقَين مع وحدة تحكّم ألعاب مرتبطة، استخدِم الأساليب التالية:

  • في الإصدار Android 4.4 (المستوى 19 من واجهة برمجة التطبيقات) أو الإصدارات الأحدث، يمكنك تحديد ما إذا كان رمز المفتاح متوافقًا مع ذراع التحكّم في الألعاب المرتبطة من خلال طلب الرمز hasKeys(int...).
  • في الإصدار 3.1 من نظام التشغيل Android (المستوى 12 من واجهة برمجة التطبيقات) أو الإصدارات الأحدث، يمكنك العثور على جميع المحاور المتاحة المتوافقة مع جهاز تحكّم في الألعاب متصل من خلال استدعاء getMotionRanges() أولاً. بعد ذلك، في كل عنصر InputDevice.MotionRange يتم إرجاعه، يمكنك استدعاء getAxis() للحصول على معرّف محوره.

معالجة الضغطات على أزرار جهاز التحكّم في الألعاب

يوضح الشكل 1 كيف يربط Android الرموز الرئيسية وقيم المحاور لعناصر التحكم الفعلية في معظم أذرع الألعاب.

الشكل 1. ملف شخصي لذراع تحكّم عام في الألعاب

تشير وسائل الشرح في الشكل إلى ما يلي:

تتضمّن رموز المفاتيح الشائعة التي يتم إنشاؤها عند الضغط على أزرار جهاز التحكّم BUTTON_A وBUTTON_B وBUTTON_SELECT وBUTTON_START. تؤدي بعض وحدات التحكّم في الألعاب أيضًا إلى تشغيل رمز المفتاح DPAD_CENTER عند الضغط على وسط شريط التمرير في لوحة التحكّم. يمكن لتطبيق اللعبة فحص رمز المفتاح من خلال استدعاء getKeyCode() أو من خلال عمليات الاستدعاء للأحداث الرئيسية، مثل onKeyDown()، وإذا كان يمثّل حدثًا ذا صلة بتطبيقك، يمكنك معالجته كأحد إجراءات اللعبة. يسرد الجدول 1 إجراءات الألعاب المقترَحة لأزرار gamepad الأكثر شيوعًا.

الجدول 1. الإجراءات المقترَحة للألعاب في أزرار gamepad

أنشطة الألعاب رمز مفتاح الزر
بدء اللعبة في القائمة الرئيسية أو إيقافها مؤقتًا أو إعادة تشغيلها أثناء اللعب BUTTON_START*
عرض القائمة BUTTON_SELECT* وKEYCODE_MENU*
هذا الخيار مماثل لسلوك التنقّل السابق في نظام Android رجوع كما هو موضّح في دليل التصميم الخاص بالتنقل. KEYCODE_BACK
الرجوع إلى عنصر سابق في إحدى القوائم BUTTON_B
تأكيد الاختيار أو تنفيذ الإجراء الأساسي في اللعبة BUTTON_A وDPAD_CENTER

* يجب ألا تعتمد لعبتك على توفّر أزرار Start (ابدأ) أو Select (اختيار) أو Menu (قائمة).

نصيحة: يمكنك توفير شاشة تهيئة في لعبتك للسماح للمستخدمين بتخصيص تعيين وحدة التحكم في الألعاب لإجراءات اللعبة.

يوضّح المقتطف التالي كيفية إلغاء onKeyDown() لربط الضغطات على الزرّ BUTTON_A وDPAD_CENTER بإجراء لعبة.

Kotlin

class GameView(...) : View(...) {
    ...

    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        var handled = false
        if (event.source and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD) {
            if (event.repeatCount == 0) {
                when (keyCode) {
                    // Handle gamepad and D-pad button presses to navigate the ship
                    ...

                    else -> {
                        keyCode.takeIf { isFireKey(it) }?.run {
                            // Update the ship object to fire lasers
                            ...
                            handled = true
                        }
                    }
                }
            }
            if (handled) {
                return true
            }
        }
        return super.onKeyDown(keyCode, event)
    }

    // Here we treat Button_A and DPAD_CENTER as the primary action
    // keys for the game.
    private fun isFireKey(keyCode: Int): Boolean =
            keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_BUTTON_A
}

Java

public class GameView extends View {
    ...

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        boolean handled = false;
        if ((event.getSource() & InputDevice.SOURCE_GAMEPAD)
                == InputDevice.SOURCE_GAMEPAD) {
            if (event.getRepeatCount() == 0) {
                switch (keyCode) {
                    // Handle gamepad and D-pad button presses to
                    // navigate the ship
                    ...

                    default:
                         if (isFireKey(keyCode)) {
                             // Update the ship object to fire lasers
                             ...
                             handled = true;
                         }
                     break;
                }
            }
            if (handled) {
                return true;
            }
        }
        return super.onKeyDown(keyCode, event);
    }

    private static boolean isFireKey(int keyCode) {
        // Here we treat Button_A and DPAD_CENTER as the primary action
        // keys for the game.
        return keyCode == KeyEvent.KEYCODE_DPAD_CENTER
                || keyCode == KeyEvent.KEYCODE_BUTTON_A;
    }
}

ملاحظة: في الإصدار 4.2 من Android (المستوى 17 لواجهة برمجة التطبيقات) والإصدارات الأقدم، يتعامل النظام تلقائيًا مع رمز BUTTON_A على أنّه مفتاح رجوع في Android. إذا كان تطبيقك متوافقًا مع إصدارات Android هذه، احرص على التعامل مع BUTTON_A على أنّه خطوة التحرك الأساسية في اللعبة. لتحديد الإصدار الحالي من حزمة تطوير البرامج (SDK) لنظام التشغيل Android على الجهاز، اطّلِع على قيمة Build.VERSION.SDK_INT.

معالجة الإدخال من لوحة الاتجاهات

لوحة التوجيه ذات الاتجاهات الأربعة (D-pad) هي عنصر تحكّم شائع في العديد من أدوات التحكّم في الألعاب. يُبلغ نظام التشغيل Android عن الضغط على زرَّي السهم المتّجه للأعلى أو للأسفل في لوحة التحكّم على أنّه حدث AXIS_HAT_Y بمدى من -1.0 (للأعلى) إلى 1.0 (للأسفل)، ويُبلغ عن الضغط على زرَّي السهم المتّجه لليسار أو لليمين في لوحة التحكّم على أنّه حدث AXIS_HAT_Y بمدى من -1.0 (لليسار) إلى 1.0 (لليمين).

وبدلاً من ذلك، تُبلغ بعض وحدات التحكّم عن الضغطات على لوحة التحكّم باستخدام رمز مفتاح. إذا كانت لعبتك تهتم بالضغط على لوحة التوجيه، عليك التعامل مع أحداث محور القبّعة ورموز مفاتيح لوحة التوجيه كأحداث الإدخال نفسها، كما هو مقترَح في الجدول 2.

الجدول 2. الإجراءات التلقائية المقترَحة في الألعاب لأرقام مفاتيح لوحة التوجيه وقيم محور القبّعة

لعبة حركة رمز مفتاح لوحة التحكّم رمز محور القبعة
نقل للأعلى KEYCODE_DPAD_UP AXIS_HAT_Y (للقيم من 0 إلى -1.0)
نقل للأسفل KEYCODE_DPAD_DOWN AXIS_HAT_Y (للقيم من 0 إلى 1.0)
نقل لليسار KEYCODE_DPAD_LEFT AXIS_HAT_X (للقيم من 0 إلى -1.0)
نقل لليمين KEYCODE_DPAD_RIGHT AXIS_HAT_X (للقيم من 0 إلى 1.0)

يعرض مقتطف الرمز التالي فئة مساعدة تتيح لك التحقّق من قيم محور hat ورمز المفتاح من حدث إدخال لتحديد اتجاه لوحة D-pad.

Kotlin

class Dpad {

    private var directionPressed = -1 // initialized to -1

    fun getDirectionPressed(event: InputEvent): Int {
        if (!isDpadDevice(event)) {
            return -1
        }

        // If the input event is a MotionEvent, check its hat axis values.
        (event as? MotionEvent)?.apply {

            // Use the hat axis value to find the D-pad direction
            val xaxis: Float = event.getAxisValue(MotionEvent.AXIS_HAT_X)
            val yaxis: Float = event.getAxisValue(MotionEvent.AXIS_HAT_Y)

            directionPressed = when {
                // Check if the AXIS_HAT_X value is -1 or 1, and set the D-pad
                // LEFT and RIGHT direction accordingly.
                xaxis.compareTo(-1.0f) == 0 -> Dpad.LEFT
                xaxis.compareTo(1.0f) == 0 -> Dpad.RIGHT
                // Check if the AXIS_HAT_Y value is -1 or 1, and set the D-pad
                // UP and DOWN direction accordingly.
                yaxis.compareTo(-1.0f) == 0 -> Dpad.UP
                yaxis.compareTo(1.0f) == 0 -> Dpad.DOWN
                else -> directionPressed
            }
        }
        // If the input event is a KeyEvent, check its key code.
        (event as? KeyEvent)?.apply {

            // Use the key code to find the D-pad direction.
            directionPressed = when(event.keyCode) {
                KeyEvent.KEYCODE_DPAD_LEFT -> Dpad.LEFT
                KeyEvent.KEYCODE_DPAD_RIGHT -> Dpad.RIGHT
                KeyEvent.KEYCODE_DPAD_UP -> Dpad.UP
                KeyEvent.KEYCODE_DPAD_DOWN -> Dpad.DOWN
                KeyEvent.KEYCODE_DPAD_CENTER ->  Dpad.CENTER
                else -> directionPressed
            }
        }
        return directionPressed
    }

    companion object {
        internal const val UP = 0
        internal const val LEFT = 1
        internal const val RIGHT = 2
        internal const val DOWN = 3
        internal const val CENTER = 4

        fun isDpadDevice(event: InputEvent): Boolean =
            // Check that input comes from a device with directional pads.
            event.source and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD
    }
}

Java

public class Dpad {
    final static int UP       = 0;
    final static int LEFT     = 1;
    final static int RIGHT    = 2;
    final static int DOWN     = 3;
    final static int CENTER   = 4;

    int directionPressed = -1; // initialized to -1

    public int getDirectionPressed(InputEvent event) {
        if (!isDpadDevice(event)) {
           return -1;
        }

        // If the input event is a MotionEvent, check its hat axis values.
        if (event instanceof MotionEvent) {

            // Use the hat axis value to find the D-pad direction
            MotionEvent motionEvent = (MotionEvent) event;
            float xaxis = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X);
            float yaxis = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y);

            // Check if the AXIS_HAT_X value is -1 or 1, and set the D-pad
            // LEFT and RIGHT direction accordingly.
            if (Float.compare(xaxis, -1.0f) == 0) {
                directionPressed =  Dpad.LEFT;
            } else if (Float.compare(xaxis, 1.0f) == 0) {
                directionPressed =  Dpad.RIGHT;
            }
            // Check if the AXIS_HAT_Y value is -1 or 1, and set the D-pad
            // UP and DOWN direction accordingly.
            else if (Float.compare(yaxis, -1.0f) == 0) {
                directionPressed =  Dpad.UP;
            } else if (Float.compare(yaxis, 1.0f) == 0) {
                directionPressed =  Dpad.DOWN;
            }
        }

        // If the input event is a KeyEvent, check its key code.
        else if (event instanceof KeyEvent) {

           // Use the key code to find the D-pad direction.
            KeyEvent keyEvent = (KeyEvent) event;
            if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) {
                directionPressed = Dpad.LEFT;
            } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) {
                directionPressed = Dpad.RIGHT;
            } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) {
                directionPressed = Dpad.UP;
            } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) {
                directionPressed = Dpad.DOWN;
            } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) {
                directionPressed = Dpad.CENTER;
            }
        }
        return directionPressed;
    }

    public static boolean isDpadDevice(InputEvent event) {
        // Check that input comes from a device with directional pads.
        if ((event.getSource() & InputDevice.SOURCE_DPAD)
             != InputDevice.SOURCE_DPAD) {
             return true;
         } else {
             return false;
         }
     }
}

يمكنك استخدام فئة المساعدة هذه في لعبتك في أي مكان تريد معالجة الإدخال من لوحة التوجيه (على سبيل المثال، في وظائف callback الخاصة بكل من onGenericMotionEvent() أو onKeyDown() ).

مثلاً:

Kotlin

private val dpad = Dpad()
...
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
    if (Dpad.isDpadDevice(event)) {
        when (dpad.getDirectionPressed(event)) {
            Dpad.LEFT -> {
                // Do something for LEFT direction press
                ...
                return true
            }
            Dpad.RIGHT -> {
                // Do something for RIGHT direction press
                ...
                return true
            }
            Dpad.UP -> {
                // Do something for UP direction press
                ...
                return true
            }
            ...
        }
    }

    // Check if this event is from a joystick movement and process accordingly.
    ...
}

Java

Dpad dpad = new Dpad();
...
@Override
public boolean onGenericMotionEvent(MotionEvent event) {

    // Check if this event if from a D-pad and process accordingly.
    if (Dpad.isDpadDevice(event)) {

       int press = dpad.getDirectionPressed(event);
       switch (press) {
            case LEFT:
                // Do something for LEFT direction press
                ...
                return true;
            case RIGHT:
                // Do something for RIGHT direction press
                ...
                return true;
            case UP:
                // Do something for UP direction press
                ...
                return true;
            ...
        }
    }

    // Check if this event is from a joystick movement and process accordingly.
    ...
}

معالجة حركات ذراع التحكّم

عندما يحرّك اللاعبون ذراع التحكّم على أذرع التحكّم في الألعاب، يعرض نظام Android علامة MotionEvent تحتوي على رمز إجراء ACTION_MOVE والأوضاع المعدّلة لمحاور ذراع التحكّم. يمكن للعبة استخدام البيانات المقدَّمة منMotionEvent لتحديد ما إذا حدثت حركة عصا تحكم مهمة.

لاحظ أن أحداث حركة ذراع التحكم قد تجمع عينات متعددة من الحركة معًا داخل كائن واحد. يحتوي العنصر MotionEvent على الموضع الحالي لكل محور من محاور ذراع التحكم، بالإضافة إلى عدة مواضع سابقة لكل محور. عند الإبلاغ عن أحداث الحركة باستخدام رمز الإجراء ACTION_MOVE (مثل حركات عصا التحكم)، يجمع Android قيم محور الالتفاف لزيادة الكفاءة. تتألّف القيم السابقة لأحد المحاور من مجموعة من القيم المميزة الأقدم من قيمة المحور الحالية، وأحدث من القيم التي تم الإبلاغ عنها في أي أحداث حركة سابقة. يُرجى الاطّلاع على مرجع MotionEvent للحصول على التفاصيل.

يمكنك استخدام المعلومات السابقة لعرض حركة كائن اللعبة بشكل أكثر دقة استنادًا إلى إدخال ذراع التحكّم. لاسترداد القيم الحالية والسابقة، اتصل بالرقم getAxisValue() أو getHistoricalAxisValue(). يمكنك أيضًا العثور على عدد النقاط السابقة في حدث ذراع التحكّم من خلال طلب القيمة getHistorySize().

يوضّح المقتطف التالي كيفية إلغاء الإجراء المُعاد تدخُّله في onGenericMotionEvent() لمعالجة إدخال ذراع التحكم. عليك أولاً معالجة القيم السابقة لمحور معيّن، ثم معالجة موضعه الحالي.

Kotlin

class GameView(...) : View(...) {

    override fun onGenericMotionEvent(event: MotionEvent): Boolean {

        // Check that the event came from a game controller
        return if (event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
                && event.action == MotionEvent.ACTION_MOVE) {

            // Process the movements starting from the
            // earliest historical position in the batch
            (0 until event.historySize).forEach { i ->
                // Process the event at historical position i
                processJoystickInput(event, i)
            }

            // Process the current movement sample in the batch (position -1)
            processJoystickInput(event, -1)
            true
        } else {
            super.onGenericMotionEvent(event)
        }
    }
}

Java

public class GameView extends View {

    @Override
    public boolean onGenericMotionEvent(MotionEvent event) {

        // Check that the event came from a game controller
        if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) ==
                InputDevice.SOURCE_JOYSTICK &&
                event.getAction() == MotionEvent.ACTION_MOVE) {

            // Process all historical movement samples in the batch
            final int historySize = event.getHistorySize();

            // Process the movements starting from the
            // earliest historical position in the batch
            for (int i = 0; i < historySize; i++) {
                // Process the event at historical position i
                processJoystickInput(event, i);
            }

            // Process the current movement sample in the batch (position -1)
            processJoystickInput(event, -1);
            return true;
        }
        return super.onGenericMotionEvent(event);
    }
}

قبل استخدام إدخال ذراع التحكم، تحتاج إلى تحديد ما إذا كان ذراع التحكم ممركزًا، ثم حساب حركات محوره وفقًا لذلك. تحتوي عصي التحكم عادةً على منطقة مسطّحة، أي نطاق من القيم بالقرب من الإحداثي (0,0) الذي يُعتبر محوره في المنتصف. إذا كانت قيمة المحور التي تم الإبلاغ عنها بواسطة Android تقع داخل المنطقة المسطحة، يجب أن تتعامل مع وحدة التحكم لتكون في حالة عدم نشاط (أي أنها ثابتة على كلا المحورين).

يعرض المقتطف أدناه طريقة مساعدة تحسب الحركة على طول كل محور. لقد استدعيت هذا المساعد في طريقة processJoystickInput() الموضحة أدناه.

Kotlin

private fun getCenteredAxis(
        event: MotionEvent,
        device: InputDevice,
        axis: Int,
        historyPos: Int
): Float {
    val range: InputDevice.MotionRange? = device.getMotionRange(axis, event.source)

    // A joystick at rest does not always report an absolute position of
    // (0,0). Use the getFlat() method to determine the range of values
    // bounding the joystick axis center.
    range?.apply {
        val value: Float = if (historyPos < 0) {
            event.getAxisValue(axis)
        } else {
            event.getHistoricalAxisValue(axis, historyPos)
        }

        // Ignore axis values that are within the 'flat' region of the
        // joystick axis center.
        if (Math.abs(value) > flat) {
            return value
        }
    }
    return 0f
}

Java

private static float getCenteredAxis(MotionEvent event,
        InputDevice device, int axis, int historyPos) {
    final InputDevice.MotionRange range =
            device.getMotionRange(axis, event.getSource());

    // A joystick at rest does not always report an absolute position of
    // (0,0). Use the getFlat() method to determine the range of values
    // bounding the joystick axis center.
    if (range != null) {
        final float flat = range.getFlat();
        final float value =
                historyPos < 0 ? event.getAxisValue(axis):
                event.getHistoricalAxisValue(axis, historyPos);

        // Ignore axis values that are within the 'flat' region of the
        // joystick axis center.
        if (Math.abs(value) > flat) {
            return value;
        }
    }
    return 0;
}

بتجميع كل شيء معًا، إليك كيفية معالجة حركات ذراع التحكم في لعبتك:

Kotlin

private fun processJoystickInput(event: MotionEvent, historyPos: Int) {

    val inputDevice = event.device

    // Calculate the horizontal distance to move by
    // using the input value from one of these physical controls:
    // the left control stick, hat axis, or the right control stick.
    var x: Float = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_X, historyPos)
    if (x == 0f) {
        x = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_X, historyPos)
    }
    if (x == 0f) {
        x = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_Z, historyPos)
    }

    // Calculate the vertical distance to move by
    // using the input value from one of these physical controls:
    // the left control stick, hat switch, or the right control stick.
    var y: Float = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_Y, historyPos)
    if (y == 0f) {
        y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_Y, historyPos)
    }
    if (y == 0f) {
        y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_RZ, historyPos)
    }

    // Update the ship object based on the new x and y values
}

Java

private void processJoystickInput(MotionEvent event,
        int historyPos) {

    InputDevice inputDevice = event.getDevice();

    // Calculate the horizontal distance to move by
    // using the input value from one of these physical controls:
    // the left control stick, hat axis, or the right control stick.
    float x = getCenteredAxis(event, inputDevice,
            MotionEvent.AXIS_X, historyPos);
    if (x == 0) {
        x = getCenteredAxis(event, inputDevice,
                MotionEvent.AXIS_HAT_X, historyPos);
    }
    if (x == 0) {
        x = getCenteredAxis(event, inputDevice,
                MotionEvent.AXIS_Z, historyPos);
    }

    // Calculate the vertical distance to move by
    // using the input value from one of these physical controls:
    // the left control stick, hat switch, or the right control stick.
    float y = getCenteredAxis(event, inputDevice,
            MotionEvent.AXIS_Y, historyPos);
    if (y == 0) {
        y = getCenteredAxis(event, inputDevice,
                MotionEvent.AXIS_HAT_Y, historyPos);
    }
    if (y == 0) {
        y = getCenteredAxis(event, inputDevice,
                MotionEvent.AXIS_RZ, historyPos);
    }

    // Update the ship object based on the new x and y values
}

لتتوافق مع وحدات تحكّم الألعاب التي تتضمّن ميزات أكثر تعقيدًا بخلاف ذراع تحكم واحد، اتّبِع أفضل الممارسات التالية:

  • استخدام عصا التحكّم المزدوجة في وحدة التحكّم تحتوي العديد من وحدات التحكم في الألعاب على ذراع التحكم الأيسر والأيمن. بالنسبة إلى العصا اليسرى، يُبلغ Android عن الحركات الأفقية كأحداث AXIS_X ويُبلغ عن الحركات الرأسية كأحداث AXIS_Y. بالنسبة إلى العصا اليمنى، يسجِّل Android الحركات الأفقية كأحداث AXIS_Z والحركات العمودية كأحداث AXIS_RZ. احرص على التعامل مع كلا عصا التحكم في الرمز البرمجي.
  • تعامل مع الضغط على زرّي الالتقاط على جانبَي وحدة التحكّم (وتأكّد من أنّ لعبتك تعمل مع حدثَي AXIS_ وKEYCODE_BUTTON_). تحتوي بعض أجهزة التحكّم على زرَّي تشغيل/إيقاف على جانبَي لوحة التحكم. وعندما تتوفّر هذه المشغِّلات، فإنّها تُصدر حدث AXIS_*TRIGGER أو KEYCODE_BUTTON_*2 أو كليهما. بالنسبة إلى عامل التشغيل الأيسر، يجب ضبطهما على AXIS_LTRIGGER وKEYCODE_BUTTON_L2. بالنسبة إلى المشغّل الصحيح، يكون ذلك هو AXIS_RTRIGGER وKEYCODE_BUTTON_R2. لا تحدث أحداث المحور إلا إذا كان المشغل يُصدر نطاقًا من القيم بين 0 و1، وبعض وحدات التحكم مع أحداث زر إصدار مخرجات تناظرية بالإضافة إلى أحداث المحور. يجب أن تتوافق الألعاب مع حدثَي AXIS_ وKEYCODE_BUTTON_ كي تبقى متوافقة مع جميع وحدات التحكّم الشائعة في الألعاب، ولكن يتم تفضيل الحدث الأكثر ملاءمةً لأسلوب لعبك إذا أبلغت وحدة التحكّم عن الاثنين معًا. في الإصدار 4.3 من نظام التشغيل Android (المستوى 18 من واجهة برمجة التطبيقات) والإصدارات الأحدث، يُرسِل جهاز التحكّم الذي يُنشئ AXIS_LTRIGGER قيمة متطابقة أيضًا لمحور AXIS_BRAKE. وينطبق ذلك أيضًا على AXIS_RTRIGGER وAXIS_GAS. يُبلغ نظام Android عن جميع ضغطات المشغلات التناظرية بقيمة تمت تسويتها من 0.0 (تم إصدارها) إلى 1.0 (تم الضغط عليها بالكامل).
  • قد تختلف السلوكيات والدعم في البيئات التي تتم محاكاتها. قد يختلف سلوك المنصات المحاكية، مثل ألعاب Google Play، قليلاً استنادًا إلى إمكانات نظام التشغيل المضيف. على سبيل المثال، بعض أدوات التحكّم التي تُرسِل أحداث AXIS_ وKEYCODE_BUTTON_ لا تُرسِل سوى أحداث AXIS_، وقد لا تتوفّر أدوات التحكّم لبعض الأجهزة على الإطلاق.