在系统级别,Android 报告来自游戏控制器的输入事件代码 Android 键码和轴值的形式。在游戏中,您可以接收这些代码 和价值,并将其转化为特定的游戏内操作。
当玩家通过物理方式连接或无线配对游戏控制器
使用 Android 设备时,系统会自动检测控制器
作为输入设备,并开始报告其输入事件。您的游戏可以接收
在您的活动
Activity
或聚焦 View
(您应该
实现对 Activity
或
View
,但不能同时设置这两者):
- 来自
Activity
: <ph type="x-smartling-placeholder">- </ph>
dispatchGenericMotionEvent(android.view. MotionEvent)
可进行调用以处理一般的动作事件(例如,操纵杆移动)。
dispatchKeyEvent(android.view.KeyEvent)
调用以处理关键事件,例如按下或释放 游戏手柄或方向键按钮。
- 来自
View
: <ph type="x-smartling-placeholder">- </ph>
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 不会区分
来自非游戏控制器设备的事件和
数据。例如,触摸屏操作会生成
表示 X 的 AXIS_X
事件
坐标,而操纵杆会生成
表示操纵杆 X 位置的 AXIS_X
事件。如果
您的游戏关注处理游戏控制器输入,那么您应该先检查
输入事件是否来自相关来源类型。
要验证连接的输入设备是否为游戏控制器,请调用
getSources()
,用于获取
该设备支持的输入来源类型。然后,您可以测试
设置了以下字段:
- 来源类型
SOURCE_GAMEPAD
表示 输入设备具有游戏手柄按钮(例如,BUTTON_A
)。请注意,这个来源 type 并不严格地指明游戏控制器是否有方向键按钮, 但大多数游戏手柄通常都配备方向控件。 - 来源类型
SOURCE_DPAD
表示 输入设备具有方向键按钮(例如,DPAD_UP
)。 - 来源类型为
SOURCE_JOYSTICK
表示输入设备具有模拟控制摇杆(例如, 可记录沿AXIS_X
移动情况的操纵杆 和AXIS_Y
)。
以下代码段展示了一个 helper 方法,该方法可让您检查 连接的输入设备是游戏控制器。如果包含,该方法 游戏控制器的设备 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 如何将按键代码和轴值映射到 支持大多数游戏控制器的操控方式
![](https://developer.android.google.cn/static/images/training/game-controller-profiles.png?authuser=0000&hl=sk)
图 1. 常规游戏控制器的配置文件。
图中的标注是指以下内容:
按下游戏手柄按钮时生成的常用键码包括
BUTTON_A
,
BUTTON_B
,
BUTTON_SELECT
,
和 BUTTON_START
。游戏
此外,当按下方向键交叉栏的中心时,控制器也会触发 DPAD_CENTER
键码。您的
游戏可以通过调用 getKeyCode()
检查按键代码
或者通过关键事件回调
onKeyDown()
,
如果它代表与您的游戏相关的活动,请将其作为
。表 1 列出了建议针对最常见的操作
游戏手柄按钮。
表 1. 推荐的游戏手柄游戏操作 按钮。
游戏操作 | 按钮键码 |
---|---|
在主菜单中启动游戏,或在游戏过程中暂停/取消暂停 | BUTTON_START * |
显示菜单 | BUTTON_SELECT *
和 KEYCODE_MENU * |
与 Android“返回”导航行为中所述的 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
作为 Android
返回键。如果您的应用支持这些 Android 设备
请务必
将BUTTON_A
作为主要游戏
操作。为了确定当前的 Android SDK
版本,请参阅
Build.VERSION.SDK_INT
值。
处理方向键输入
四向方向键是许多游戏中常见的物理控件
控制器。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 之间的值) |
以下代码段展示了一个 helper 类,您可以通过该类查看帽子 轴和键码值来确定方向键方向。
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; } } }
您可以在游戏中需要处理的任何位置使用此辅助程序类
方向键输入(例如,在
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); } }
在使用操纵杆输入之前,您需要确定操纵杆是否 然后相应地计算其轴的移动。通常使用操纵杆 具有 Flat 区域,即 (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
个事件。请务必处理 两个控制器都在您的代码中。 - 处理肩部扳机按下操作(但提供替代输入)
方法)。某些控制器有左肩和右肩
触发器。如果存在这些扳机,Android 会报告一次左扳机按下操作
作为
AXIS_LTRIGGER
事件和 右扳机按下动作AXIS_RTRIGGER
事件。在 Android 设备上 4.3(API 级别 18),一个可生成AXIS_LTRIGGER
还报告了AXIS_BRAKE
轴上的值完全相同。通过AXIS_RTRIGGER
也是如此AXIS_GAS
。Android 报告所有模拟触发器 采用标准化值 0.0(已松开)到 1.0(完全按下)的压力。非 所有控制器都有触发器,因此请考虑允许玩家执行这些操作 其他按钮的游戏操作