Как обрабатывать действия контроллера

У контроллеров есть два типа действий:

  • KeyEvent для любой кнопки с двумя состояниями: "включено" и "выключено".
  • MotionEvent используется для любой оси, которая возвращает диапазон значений. Например, от -1 до 1 для аналоговых стиков или от 0 до 1 для аналоговых триггеров.

Вы можете считывать эти данные с View, в котором есть focus.

Kotlin

override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
  if (event.isFromSource(SOURCE_GAMEPAD)
      && event.repeatCount == 0
  ) {
      Log.d("GameView", "Gamepad key pressed: $keyCode")
      return true
  }

  return super.onKeyDown(keyCode, event)
}

override fun onGenericMotionEvent(event: MotionEvent): Boolean {
  if (event.isFromSource(SOURCE_JOYSTICK)) {
      Log.d("GameView", "Gamepad event: $event")
      return true
  }

  return super.onGenericMotionEvent(event)
}

Java

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
  if (event.isFromSource(SOURCE_GAMEPAD)
          && event.getRepeatCount() == 0
  ) {
      Log.d("GameView", "Gamepad key pressed: " + keyCode);
      return true;
  }

  return super.onKeyDown(keyCode, event);
}

@Override
public boolean onGenericMotionEvent(MotionEvent event) {
  if (event.isFromSource(SOURCE_JOYSTICK)) {
      Log.d("GameView", "Gamepad event: " + event);
      return true;
  }
  return super.onGenericMotionEvent(event);
}

При необходимости вы можете считывать события непосредственно из Activity.

Убедитесь, что игровой контроллер подключен.

При регистрации входных событий Android будет использовать одни и те же идентификаторы клавиш или осей для разных типов устройств ввода. Например, при действии на сенсорном экране генерируется событие AXIS_X, которое представляет собой координату X сенсорной поверхности, а при действии на геймпаде – событие AXIS_X, которое представляет собой положение левого стика по оси X. Это означает, что для правильной интерпретации входных событий необходимо проверять тип источника.

Чтобы проверить, является ли подключенное устройство InputDevice игровым контроллером, используйте функцию supportsSource(int):

  • Тип источника SOURCE_GAMEPAD указывает, что у устройства ввода есть кнопки контроллера (например, KEYCODE_BUTTON_A). Обратите внимание, что этот тип источника не указывает, есть ли у игрового контроллера кнопки D-pad, хотя у большинства контроллеров обычно есть кнопки управления направлением.
  • Тип источника SOURCE_DPAD указывает, что на устройстве ввода есть кнопки d-pad (например, 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 (supportsSource(SOURCE_GAMEPAD)
              || supportsSource(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);

      if (dev == null) {
          continue;
      }

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

Входные данные контроллера процесса

В этом разделе описаны типы игровых контроллеров, поддерживаемых на устройствах Android.

Разработчикам на C++ следует использовать библиотеку контроллера игр. Он объединяет все контроллеры в наиболее распространенный набор функций и обеспечивает согласованный интерфейс между ними, включая возможность определять раскладку кнопок.

На этом рисунке показано, как выглядит обычный контроллер в Android.

Универсальный игровой контроллер с обозначенными элементами управления, включая крестовину, аналоговые стики и кнопки
Рисунок 1. Профиль для обычного игрового контроллера.

В таблице перечислены стандартные названия и типы событий для игровых контроллеров. Полный список событий приведен в разделе Распространенные варианты. Система отправляет события MotionEvent через события onGenericMotionEvent и KeyEvent, а события onKeyDown и onKeyUp – через события onKeyDown и onKeyUp.

Ввод с контроллера KeyEvent MotionEvent
1. Крестовина
AXIS_HAT_X
(горизонтальный ввод)
AXIS_HAT_Y
(вертикальный ввод)
2. Левый аналоговый стик
KEYCODE_BUTTON_THUMBL
(при нажатии)
AXIS_X
(горизонтальное движение)
AXIS_Y
(вертикальное движение)
3. Правый аналоговый стик
KEYCODE_BUTTON_THUMBR
(при нажатии)
AXIS_Z
(горизонтальное движение)
AXIS_RZ
(вертикальное движение)
4. Кнопка X KEYCODE_BUTTON_X
5. Кнопка A KEYCODE_BUTTON_A
6. Кнопка Y KEYCODE_BUTTON_Y
7. Кнопка B KEYCODE_BUTTON_B
8. Правый бампер
KEYCODE_BUTTON_R1
9. Правый триггер
AXIS_RTRIGGER
10. Левый триггер AXIS_LTRIGGER
11. Левый бампер KEYCODE_BUTTON_L1
12. Начать KEYCODE_BUTTON_START
13. Выбрать KEYCODE_BUTTON_SELECT

Обработка нажатий кнопок

Поскольку Android сообщает о нажатиях кнопок контроллера так же, как и о нажатиях кнопок клавиатуры, вам необходимо:

  • Убедитесь, что событие происходит из SOURCE_GAMEPAD.
  • Убедитесь, что вы получаете нажатие кнопки только один раз с помощью KeyEvent.getRepeatCount(). Android будет отправлять повторяющиеся ключевые события, как если бы вы удерживали клавишу на клавиатуре.
  • Чтобы указать, что событие обработано, верните значение true.
  • Передавайте необработанные события в super, чтобы убедиться, что различные уровни совместимости Android работают правильно.

    Kotlin

    class GameView : View {
    // ...
    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        event.apply {
            var handled = false
    
            // make sure we're handling gamepad events
            if (isFromSource(SOURCE_GAMEPAD)) {
    
                // avoid processing the keycode repeatedly
                if (repeatCount == 0) {
                    when (keyCode) {
                        // handle the "A" button
                        KEYCODE_BUTTON_A -> {
                          handled = true
                        }
                    }
                    // ...
                }
            }
            if (handled) {
                return true
            }
       }
       return super.onKeyDown(keyCode, event)
      }
    }
    

    Java

    public class GameView extends View {
    // ...
    
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        boolean handled = false;
        // make sure we're handling gamepad events
        if (event.isFromSource(SOURCE_GAMEPAD)) {
            // avoid processing the keycode repeatedly
            if (event.getRepeatCount() == 0) {
                switch (keyCode) {
                    case KEYCODE_BUTTON_A:
                        // handle the "A" button
                        handled = true;
                        break;
                    // ...
                }
            }
            // mark this event as handled
            if (handled) {
                return true;
            }
        }
        // Always do this instead of "return false"
        // it allows Android's input compatibility layers to work
        return super.onKeyDown(keyCode, event);
      }
    }
    

Обработка нажатий на кнопки навигации

Крестовина, или D-pad, – это распространенный элемент управления на многих игровых контроллерах. В Android нажатия на кнопки ВВЕРХ и ВНИЗ навигационной панели передаются как события AXIS_HAT_Y, где -1.0 означает нажатие кнопки ВВЕРХ, а 1.0 – ВНИЗ. Нажатия навигационной кнопки влево или вправо регистрируются как события AXIS_HAT_X, где -1.0 означает влево, а 1.0 – вправо.

Некоторые контроллеры вместо этого сообщают о нажатиях на крестовину с помощью кода клавиши. Если в вашей игре важны нажатия на крестовину, то события оси шляпки и коды клавиш крестовины следует рассматривать как одинаковые входные события, как рекомендовано в таблице 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)

В приведенном ниже фрагменте кода показан вспомогательный класс, который позволяет проверять значения оси шляпки и кода клавиши из входного события, чтобы определить направление 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.
            return event.isFromSource(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.
        return event.isFromSource(InputDevice.SOURCE_DPAD);
     }
}

Этот вспомогательный класс можно использовать в игре везде, где нужно обрабатывать ввод с 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.

Чтобы точно отображать движение игрового объекта на основе ввода с джойстика, можно использовать информацию, предоставленную объектами MotionEvent.

Получить текущие и исторические значения можно следующими способами:

В приведенном ниже фрагменте кода показано, как переопределить обратный вызов 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_. Если контроллер сообщает об обоих событиях, выбирайте то, которое лучше подходит для вашей игры. На устройствах с 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_, а поддержка некоторых контроллеров может отсутствовать.

Распространенные варианты

Android поддерживает множество контроллеров, поэтому может быть сложно понять, как создать и протестировать игру, чтобы она работала без ошибок на устройствах всех пользователей. Мы обнаружили, что, несмотря на кажущееся разнообразие, производители контроллеров по всему миру придерживаются трех основных стилей. Некоторые устройства позволяют переключаться между ними с помощью аппаратных переключателей.

Это значит, что вы можете протестировать игру всего на трех контроллерах и быть уверенными, что она будет работать без списков разрешенных и запрещенных устройств.

Распространенные типы контроллеров

Чаще всего контроллеры имитируют раскладку популярных игровых консолей. Это касается как внешнего вида кнопок и макета, так и функциональности, связанной с событиями. Контроллеры с аппаратными переключателями между разными типами консолей будут отправлять разные события, а иногда даже менять логическую раскладку кнопок.

При тестировании рекомендуем убедиться, что игра работает с одним контроллером из каждой категории. Вы можете использовать контроллеры от сторонних производителей или собственные контроллеры. Как правило, мы сопоставляем самые популярные контроллеры с определением выше.

Тип контроллера Различия в поведении Варианты маркировки
Контроллеры в стиле Xbox

Это контроллеры, обычно предназначенные для платформы Microsoft Xbox и Windows*.

Эти контроллеры соответствуют набору функций, описанному в разделе Обработка входных данных контроллера. Кнопки L2 и R2 на этих контроллерах обозначены как LT и RT.
Контроллеры Switch Style

Обычно такие контроллеры предназначены для консолей семейства Nintendo Switch*.

Эти контроллеры отправляют KeyEvent KEYCODE_BUTTON_R2 KEYCODE_BUTTON_L2 MotionEvent. Кнопки L2/R2 на этих контроллерах обозначены как ZL/ZR.

На таких контроллерах кнопки A и B, а также X и Y поменяны местами, поэтому KEYCODE_BUTTON_A – это кнопка B и наоборот.

Контроллеры PlayStation

Обычно такие контроллеры предназначены для консолей семейства Sony PlayStation*.

Эти контроллеры отправляют MotionEvent, как контроллеры в стиле Xbox, но также отправляют KeyEvent, как контроллеры в стиле Switch, когда полностью нажаты. На этих контроллерах для кнопок используются другие символы.

* Microsoft, Xbox и Windows – зарегистрированные товарные знаки компании Microsoft; Nintendo Switch – зарегистрированный товарный знак компании Nintendo of America Inc.; PlayStation – зарегистрированный товарный знак компании Sony Interactive Entertainment Inc.

Кнопки для уточнения запроса

Некоторые контроллеры отправляют события AXIS_LTRIGGER и AXIS_RTRIGGER, другие – KEYCODE_BUTTON_L2 и KEYCODE_BUTTON_R2, а третьи – все эти события в зависимости от возможностей оборудования. Чтобы обеспечить максимальную совместимость, поддерживайте все эти события.

Все контроллеры, которые отправляют AXIS_LTRIGGER, также отправляют AXIS_BRAKE. Аналогично, AXIS_RTRIGGER и AXIS_GAS помогают обеспечить максимальную совместимость между рулями и обычными контроллерами. Обычно это не вызывает проблем, но может быть важно для таких функций, как переназначение клавиш.

Триггер MotionEvent KeyEvent
Левый триггер AXIS_LTRIGGER
AXIS_BRAKE
KEYCODE_BUTTON_L2
Правый триггер AXIS_RTRIGGER
AXIS_GAS
KEYCODE_BUTTON_R2

Чтобы обеспечить совместимость с как можно большим количеством контроллеров, убедитесь, что игра поддерживает как KeyEvent, так и MotionEvent, и что события не дублируются.

Поддерживаемые контроллеры

При тестировании рекомендуем убедиться, что игра работает с одним контроллером из каждой категории.

  • Стиль Xbox
  • Стиль Nintendo Switch
  • Стиль PlayStation

Вы можете тестировать контроллеры от сторонних производителей или собственные контроллеры. Мы стараемся сопоставлять популярные контроллеры с определением как можно точнее.