处理控制器操作

控制器有两类操作:

  • KeyEvent 用于任何具有“开启”和“关闭”两种状态的按钮
  • MotionEvent 用于返回一系列值的任何轴。例如,模拟摇杆的范围为 -1 到 1,模拟扳机的范围为 0 到 1。

您可以从具有 focusView 中读取这些输入。

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 会针对不同的输入设备类型重复使用相同的键或轴 ID。例如,触摸屏操作会生成一个表示触控面的 X 坐标的 AXIS_X 事件,但游戏手柄会生成一个表示左摇杆的 X 轴位置的 AXIS_X 事件。这意味着,您必须检查来源类型才能正确解读输入事件。

如需验证已连接的 InputDevice 是否为游戏控制器,请使用 supportsSource(int) 函数:

  • SOURCE_GAMEPAD 来源类型表示输入设备具有控制器按钮(例如,KEYCODE_BUTTON_A)。请注意,尽管大多数控制器通常都具有方向控件,但从严格意义上来说,此来源类型并不指示游戏控制器是否具有方向键按钮。
  • SOURCE_DPAD 来源类型表示输入设备具有方向键按钮(例如,DPAD_UP)。
  • SOURCE_JOYSTICK 来源类型表示输入设备具有模拟控制摇杆(例如,记录沿 AXIS_XAXIS_Y 的移动的操纵杆)。

以下代码段显示了一种辅助程序方法,通过该方法,您可以检查连接的输入设备是否是游戏控制器。如果是,则该方法会检索游戏控制器的设备 ID。然后,您可以将每个设备 ID 与游戏中的一位玩家相关联,并分别处理每位已连接的玩家的游戏操作。如需详细了解如何支持同时连接到同一台 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++ 开发者应使用 Game Controller 库。它将所有控制器统一到最常见的功能子集,并在它们之间提供一致的接口,包括检测按钮布局的功能。

此图显示了 Android 游戏开发者可以预期在 Android 上看到的常见控制器外观。

通用游戏控制器,带有标记的输入,包括方向键、模拟摇杆和按钮
图 1. 常规游戏控制器的剖面图。

下表列出了游戏控制器的标准事件名称和类型。如需查看完整列表,请参阅常见变体。系统通过 onGenericMotionEvent 发送 MotionEvent 事件,并通过 onKeyDownonKeyUp 发送 KeyEvent 事件。

控制器输入 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. Left Trigger 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. 建议为方向键键码和帽子轴值使用的默认游戏操作。

游戏操作 方向键键码 帽子轴代码
上移 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.
            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);
     }
}

您可以在游戏中任何需要处理方向键输入的位置使用该辅助程序类(例如,在 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_LTRIGGERKEYCODE_BUTTON_L2。对于右侧触发器,这些值分别为 AXIS_RTRIGGERKEYCODE_BUTTON_R2。只有当触发器发出介于 0 和 1 之间的一系列值时,才会发生轴事件;一些具有模拟输出的控制器除了轴事件之外,还会发出按钮事件。游戏必须同时支持 AXIS_KEYCODE_BUTTON_ 事件,才能与所有常见游戏控制器保持兼容,但如果控制器同时报告这两个事件,则最好选择最适合游戏玩法的事件。在 Android 4.3(API 级别 18)及更高版本中,产生 AXIS_LTRIGGER 的控制器也会针对 AXIS_BRAKE 轴报告完全相同的值。AXIS_RTRIGGERAXIS_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。

这些控制器还会交换 AB 按钮以及 XY 按钮,因此 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_LTRIGGERAXIS_RTRIGGER,有些会发送 KEYCODE_BUTTON_L2KEYCODE_BUTTON_R2,还有些会根据其硬件功能发送所有这些事件。通过支持所有这些事件,最大限度地提高兼容性。

发送 AXIS_LTRIGGER 的所有控制器也会发送 AXIS_BRAKEAXIS_RTRIGGERAXIS_GAS 也是如此,以最大限度地提高赛车方向盘与典型游戏控制器之间的兼容性。一般来说,这不会导致问题,但请注意按键重映射屏幕等功能。

触发条件 MotionEvent KeyEvent
Left Trigger AXIS_LTRIGGER
AXIS_BRAKE
KEYCODE_BUTTON_L2
Right Trigger AXIS_RTRIGGER
AXIS_GAS
KEYCODE_BUTTON_R2

应注意验证游戏是否可以同时处理 KeyEventMotionEvent,以尽可能保持与更多控制器的兼容性,并确保事件已去重。

支持的控制器

在测试时,我们建议您验证游戏是否适用于每个类别中的一个控制器。

  • Xbox 样式
  • Nintendo Switch 样式
  • PlayStation 风格

您可以使用第一方控制器或热门的第三方制造商的控制器进行测试,我们通常会将最热门的控制器尽可能紧密地映射到定义。