在系统级别,Android 将来自游戏控制器的输入事件代码报告为 Android 键代码和轴值。在您的游戏中,您可以接收这些代码和值,并将它们转换为特定的游戏内操作。
当玩家将游戏控制器物理连接或无线配对到他们的 Android 设备时,系统会自动检测控制器作为输入设备,并开始报告其输入事件。您的游戏可以通过在活动的 Activity
或聚焦的 View
中实现以下回调方法来接收这些输入事件(您应该为 Activity
或 View
实现回调,但不能同时实现两者)
- 来自
Activity
dispatchGenericMotionEvent(android.view.MotionEvent)
用于处理通用运动事件,例如操纵杆移动。
dispatchKeyEvent(android.view.KeyEvent)
用于处理按键事件,例如游戏手柄或方向键按钮的按下或释放。
- 来自
View
onGenericMotionEvent(android.view.MotionEvent)
用于处理通用运动事件,例如操纵杆移动。
onKeyDown(int, android.view.KeyEvent)
用于处理物理按键的按下,例如游戏手柄或方向键按钮。
onKeyUp(int, android.view.KeyEvent)
用于处理物理按键的释放,例如游戏手柄或方向键按钮。
推荐的方法是从用户交互的特定 View
对象捕获事件。检查回调提供的以下对象以获取有关接收到的输入事件类型的信息
KeyEvent
- 一个描述方向键(方向键)和游戏手柄按钮事件的对象。按键事件伴随着一个 _键码_,它指示触发了哪个特定的按钮,例如
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
记录移动的操纵杆)。
以下代码片段显示了一个辅助方法,该方法允许您检查已连接的输入设备是否是游戏控制器。如果是,则该方法将检索游戏控制器的设备 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 (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)或更高版本中,您可以首先调用
getMotionRanges()
来查找已连接的游戏控制器支持的所有可用轴。然后,在返回的每个InputDevice.MotionRange
对象上,调用getAxis()
以获取其轴 ID。
处理游戏手柄按钮按下
图 1 显示了 Android 如何将键码和轴值映射到大多数游戏控制器上的物理控件。
图中的标注指的是以下内容:
游戏手柄按钮按下生成的常用键码包括BUTTON_A
、BUTTON_B
、BUTTON_SELECT
和BUTTON_START
。一些游戏控制器在按下方向键十字键的中心时还会触发DPAD_CENTER
键码。您的游戏可以通过调用getKeyCode()
或来自按键事件回调(例如onKeyDown()
)来检查键码,如果它代表与您的游戏相关的事件,则将其处理为游戏动作。表1列出了最常用的游戏手柄按钮的推荐游戏动作。
游戏动作 | 按钮键码 |
---|---|
在主菜单中启动游戏,或在游戏中暂停/取消暂停 | 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; } }
注意:在Android 4.2(API级别17)及更低版本中,系统默认将BUTTON_A
视为Android *返回*键。如果您的应用支持这些Android版本,请确保将BUTTON_A
视为主要游戏动作。要确定设备上的当前Android SDK版本,请参考Build.VERSION.SDK_INT
值。
处理方向键输入
4向方向键(D-pad)是许多游戏控制器中常见的物理控制。Android将D-pad向上和向下按下报告为AXIS_HAT_Y
事件,范围从-1.0(向上)到1.0(向下),并将D-pad向左或向右按下报告为AXIS_HAT_X
事件,范围从-1.0(向左)到1.0(向右)。
一些控制器改为使用键码报告D-pad按下。如果您的游戏关心D-pad按下,则应将方向键轴事件和D-pad键码视为相同的输入事件,如表2中建议的那样。
游戏动作 | 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. 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会报告一个包含ACTION_MOVE
动作代码和摇杆轴更新位置的MotionEvent
。您的游戏可以使用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_
事件才能与所有常见的游戏控制器保持兼容,但是如果控制器同时报告这两种事件,则优先选择最符合您游戏玩法的事件。在 Android 4.3(API 级别 18)及更高版本中,产生AXIS_LTRIGGER
的控制器也会为AXIS_BRAKE
轴报告相同的值。AXIS_RTRIGGER
和AXIS_GAS
也是如此。Android 使用从 0.0(释放)到 1.0(完全按下)的标准化值报告所有模拟扳机按下。 -
模拟环境中的特定行为和支持可能有所不同。模拟平台(例如Google Play 游戏)的行为可能因主机操作系统的功能而略有不同。例如,一些同时发出
AXIS_
和KEYCODE_BUTTON_
事件的控制器只发出AXIS_
事件,并且可能完全缺少对某些控制器的支持。