טיפול בפעולות של השלט רחוק

ברמת המערכת, Android מדווחת על קודי אירועי קלט ממכשירי בקרת משחקים בתור קודי מפתחות וערכים של צירים ב-Android. במשחק, תוכלו לקבל את הקודים והערכים האלה ולהמיר אותם לפעולות ספציפיות במשחק.

כששחקנים מחברים פיזית או מתאימים באופן אלחוטי שלט משחק למכשירים שלהם עם Android, המערכת מזהה את השלט באופן אוטומטי כמכשיר קלט ומתחילה לדווח על אירועי הקלט שלו. המשחק יכול לקבל את אירועי הקלט האלה על ידי הטמעת השיטות הבאות לקריאה חוזרת (callback) ב-Activity הפעיל או ב-View המודגש (צריך להטמיע את הקריאות החוזרות ל-Activity או ל-View, אבל לא לשניהם):

הגישה המומלצת היא לתעד את האירועים מהאובייקט הספציפי של View שאליו המשתמש יוצר אינטראקציה. כדי לקבל מידע על סוג אירוע הקלט שהתקבל, בודקים את האובייקטים הבאים שסופקו על ידי הקריאות החוזרות:

KeyEvent
אובייקט שמתאר אירועים של לחצנים במשטח הכיוונים (D-pad) ובג'וי פד. אירועים מרכזיים מלווים בקוד מפתח שמציין את הלחצן הספציפי שהופעל, למשל DPAD_DOWN או BUTTON_A. אפשר לקבל את קוד המפתח באמצעות קריאה ל-getKeyCode() או מקריאות חזרה של אירועי מפתח, כמו onKeyDown().
MotionEvent
אובייקט שמתאר קלט מהתנועות של מוט ההיגוי וממנוף הכתף. אירועי תנועה מלווים בקוד פעולה ובקבוצה של ערכי צירים. קוד הפעולה מציין את שינוי המצב שהתרחש, למשל ג'ויסטיק שהועבר. ערכי הציר מתארים את המיקום ומאפייני תנועה אחרים של אמצעי בקרה פיזי ספציפי, כמו AXIS_X או AXIS_RTRIGGER. כדי לקבל את קוד הפעולה, צריך להפעיל את הפונקציה getAction(), ואת ערך הציר באמצעות הפונקציה getAxisValue().

בשיעור הזה נתמקד באופן שבו אפשר להתמודד עם קלט מהסוגים הנפוצים ביותר של אמצעי בקרה פיזיים (לחצני בקר משחקים, רפידות כיוון וג'ויסטיקים) במסך משחק על ידי הטמעת שיטות הקריאה החוזרת (callback) ועיבוד האובייקטים KeyEvent ו-MotionEvent שצוינו למעלה.View

מוודאים שבקר המשחק מחובר

כשמדווחים על אירועי קלט, מערכת Android לא מבדילה בין אירועים שהגיעו ממכשיר שאינו בקר משחקים לבין אירועים שהגיעו מבקר משחקים. לדוגמה, פעולה במסך מגע יוצרת אירוע AXIS_X שמייצג את קואורדינטת ה-X של משטח המגע, אבל ג'ויסטיק יוצר אירוע AXIS_X שמייצג את המיקום ה-X של הג'ויסטיק. אם המשחק שלכם מטפל בקלט של בקר משחקים, תחילה עליכם לוודא שאירוע הקלט מגיע מסוג מקור רלוונטי.

כדי לוודא שמכשיר קלט מחובר הוא שלט לגיימינג, צריך להפעיל את הפונקציה getSources() כדי לקבל שדה ביט משולב של סוגי מקורות הקלט הנתמכים במכשיר הזה. לאחר מכן תוכלו לבדוק אם השדות הבאים מוגדרים:

  • סוג המקור של SOURCE_GAMEPAD מציין שמכשיר הקלט כולל לחצני בקר משחקים (לדוגמה, BUTTON_A). שימו לב שסוג המקור הזה לא מציין במפורש אם בקר המשחקים כולל לחצני D-pad, אבל לרוב הגיימפאד יש לחצני כיוון.
  • סוג מקור של 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 ואילך (רמת API 19 ואילך), אפשר לבדוק אם קוד מקש נתמך בנגן משחקים מחובר באמצעות קריאה ל-hasKeys(int...).
  • ב-Android מגרסה 3.1 ואילך (רמת API 12 ואילך), כדי למצוא את כל הצירים הזמינים שנתמכים ב-game controller מחובר, קודם צריך להפעיל את הפונקציה getMotionRanges(). לאחר מכן, בכל אובייקט InputDevice.MotionRange שמוחזר, צריך לבצע קריאה ל-getAxis() כדי לקבל את מזהה הציר.

עיבוד לחיצות על לחצני בקר המשחקים

באיור 1 מוצג איך Android ממפה קודי מפתחות וערכים של צירים לפקדים הפיזיים ברוב בקרי המשחקים.

איור 1. פרופיל של שלט גיימינג כללי.

נכסי היתרונות המרכזיים שמופיעים באיור מתייחסים לפרטים הבאים:

קודי מפתח נפוצים שנוצרים על ידי לחיצות על לחצני משחקי הווידאו כוללים את BUTTON_A,‏ BUTTON_B,‏ BUTTON_SELECT ו-BUTTON_START. בחלק ממכשירי הבקרה למשחקים, קוד המקש DPAD_CENTER מופעל גם כשלוחצים על מרכז מוט האחיזה של D-pad. המשחק יכול לבדוק את קוד המפתח באמצעות קריאה ל-getKeyCode() או מתוך קריאות חזרה (callbacks) של אירועי מפתח, כמו onKeyDown(). אם הקוד מייצג אירוע שרלוונטי למשחק, אפשר לעבד אותו כפעולה במשחק. בטבלה 1 מפורטות הפעולות המומלצות במשחקים ללחצנים הנפוצים ביותר במשטח המשחק.

טבלה 1. פעולות מומלצות במשחקים ללחצני גיימפאד.

פעולה במשחק קוד מפתח לחצן
הפעלת משחק בתפריט הראשי, או השהיה/ביטול השהיה במהלך המשחק BUTTON_START*
הצגת התפריט BUTTON_SELECT* ו-KEYCODE_MENU*
זהה להתנהגות הניווט הקודם של Android, שמתוארת במדריך העיצוב של ניווט. KEYCODE_BACK
ניווט חזרה לפריט קודם בתפריט BUTTON_B
אישור הבחירה או ביצוע הפעולה הראשית במשחק BUTTON_A וגם DPAD_CENTER

* אסור שהמשחק יסתמך על הנוכחות של הלחצנים 'התחלה', 'בחירה' או 'תפריט'.

טיפ: מומלץ להוסיף למשחק מסך הגדרות כדי לאפשר למשתמשים להתאים אישית את המיפויים של בקר המשחק שלהם לפעולות במשחק.

קטע הקוד הבא מראה איך אפשר לשנות את 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;
    }
}

הערה: ב-Android 4.2 (רמת API 17) ומטה, המערכת מתייחסת ל-BUTTON_A כמקש Back של Android כברירת מחדל. אם האפליקציה תומכת בגרסאות Android האלה, חשוב להתייחס ל-BUTTON_A כפעולת המשחק הראשית. כדי לקבוע את הגרסה הנוכחית של Android SDK במכשיר, עיינו בערך Build.VERSION.SDK_INT.

עיבוד קלט לחצני החיצים

לחצן הכיוונים (D-pad) בן 4 הכיוונים (D-pad) הוא אמצעי בקרה פיזי נפוץ בבקרים רבים במשחקים. ב-Android מדווחים על לחצני החיצים למעלה ולמטה בתור AXIS_HAT_Y אירועים עם טווח מ-1.0 (למעלה) ל-1.0 (למטה), ולחצני החיצים (D-pad) LEFT או RIGHT לוחץ על AXIS_HAT_X עם טווח בין -1.0 (שמאל) ל-1.0 (ימין).

בחלק מהשלטים, במקום זאת, דיווח על לחיצות על D-pad מתבצע באמצעות קוד מפתח. אם במשחק שלכם חשוב לדעת על לחיצות על D-pad, עליכם להתייחס לאירועים של ציר הכובע ולקודי המקשים של D-pad כאל אותם אירועי קלט, כפי שמומלץ בטבלה 2.

טבלה 2. פעולות מומלצות שמוגדרות כברירת מחדל במשחקים, עבור קודי מקשים של D-pad וערכים של ציר הכובע.

פעולה במשחק קוד מקש של לוח החיצים (D-pad) קוד ציר כובע
הזזה למעלה 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)

קטע הקוד הבא מציג סיווג עזר שמאפשר לבדוק את הערכים של ציר הכובע ואת קוד המפתח מאירוע קלט כדי לקבוע את הכיוון של מתג הכיוונים.

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;
         }
     }
}

אפשר להשתמש במחלקה המסייעת הזו במשחק בכל מקום שבו רוצים לעבד קלט D-pad (לדוגמה, בקריאות 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 נמצא באזור השטוח, צריך להעביר את השלט רחוק למצב מנוחה (כלומר, ללא תנועה בשני הצירים).

בקטע הקוד הבא מוצגת שיטה מסייעת שמחשבת את התנועה לאורך כל ציר. מפעילים את ה-helper הזה ב-method‏ 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_ כדי להישאר תואמים לכל בקרי המשחק הנפוצים, אבל אם בקר מדווח על שניהם, עדיף לבחור באירוע הכי רלוונטי למשחק. ב-Android 4.3 ואילך (רמת API 18 ואילך), בקר שיוצר AXIS_LTRIGGER מדווח גם על ערך זהה לציר AXIS_BRAKE. אותו עיקרון נכון גם לגבי AXIS_RTRIGGER וגם לגבי AXIS_GAS. מערכת Android מדווחת על כל לחיצות הטריגר האנלוגיות עם ערך מנורמל מ-0.0 (פורסם) ל-1.0 (בלחיצה מלאה).
  • התנהגויות ותמיכה ספציפיות עשויות להיות שונות בסביבות מופעלות. התנהגות הפלטפורמות המשוכפלות, כמו Google Play Games, עשויה להיות שונה במקצת בהתאם ליכולות של מערכת ההפעלה המארחת. לדוגמה, בקרים מסוימים שפולטים גם אירועי AXIS_ וגם אירועי KEYCODE_BUTTON_ פולטים רק אירועי AXIS_, ויכול להיות שהתמיכה בחלק מהבקרים תהיה חסרה לחלוטין.