
文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese点击查看免费下载本教程选自开源仓库 android-training-course-in-chineseAndroid 官方培训课程中文版的「支持游戏控制器」章节原文对应 Android 开发者官网Handling Controller Actions一课。在 Android 系统层面来自游戏控制器的输入事件会被统一报告为按键码值Key Code和坐标值Axis Value。本篇文章将以仓库中的 controller-inputs.md 为核心骨架系统讲解如何通过 Activity / View 回调接收这些事件并将其转化为具体的游戏行为包括验证控制器是否已连接、处理游戏手柄按键、处理四方向 D-pad 输入以及处理摇杆模拟量运动。读完本文你将掌握一套完整、可直接落地的控制器输入接入方案并能结合 兼容性适配 与 多控制器支持 构建跨 Android 版本的多人游戏输入层。输入事件模型按键码与坐标值Android 将游戏控制器的输入抽象为两类事件对象这是后续所有处理逻辑的基础KeyEvent描述方向键D-pad和游戏按键事件的对象。按键事件伴随一个表示特定按键触发的按键码值key code例如KEYCODE_DPAD_DOWN或KEYCODE_BUTTON_A。我们可以通过调用getKeyCode()获得按键码值或者直接在按键事件回调方法如onKeyDown()中取得。MotionEvent描述摇杆和肩键trigger运动的输入对象。动作事件伴随一个动作码action code和一系列坐标值axis values。动作码表示发生变化的状态例如摇动一个摇杆坐标值描述特定物理操控的位置和其它运动属性例如AXIS_X或AXIS_RTRIGGER。通过getAction()获得动作码通过getAxisValue()获得坐标值。在 Activity 与 View 中接收输入事件当玩家将游戏控制器通过有线连接或无线配对到 Android 设备时系统会自动检测控制器、将其设置为输入设备并开始报告输入事件。游戏应用可以通过在 Activity 或 View 中实现以下回调方法来接收这些事件——注意二选一不要在 Activity 和 View 两处同时实现回调在Activity中dispatchGenericMotionEvent(MotionEvent)处理一般的运动事件如摇动摇杆dispatchKeyEvent(KeyEvent)处理按键事件如按下或释放游戏键盘按键或 D-pad 按钮。在View中onGenericMotionEvent(MotionEvent)处理一般的运动事件如摇动摇杆onKeyDown(int, KeyEvent)处理按下按键的事件onKeyUp(int, KeyEvent)处理释放按键的事件。官方推荐的做法是从与用户交互的 View 对象捕获事件这符合 Android 事件分发机制中 View 优先消费事件的设计。本章后续示例均以 View 回调为主线展开。验证游戏控制器是否已连接在报告输入事件时Android不会区分游戏控制器事件与非游戏控制器事件。例如一个触屏动作会产生表示触摸表面 X 坐标的AXIS_X而摇杆动作产生的AXIS_X则表示摇杆水平移动的位置。因此如果游戏关注控制器输入必须首先检测事件来源类型。通过调用InputDevice.getSources()获得设备支持的输入类型位字段bit field并测试以下字段是否被设置SOURCE_GAMEPAD表示输入设备有游戏手柄按键如BUTTON_A。注意虽然一般游戏手柄都带有方向控制键但此源类型并不代表设备具有 D-pad 按钮SOURCE_DPAD表示输入设备有 D-pad 按钮如DPAD_UPSOURCE_JOYSTICK表示输入设备有遥控杆如通过AXIS_X和AXIS_Y记录动作的摇杆。下面的 helper 方法用于检验已接入的输入设备是否为游戏控制器若是则返回游戏控制器的设备 IDpublic ArrayList getGameControllerIds() { ArrayList gameControllerDeviceIds new ArrayList(); 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; }返回的设备 ID 应进一步与游戏中的玩家建立关联并针对每个已接入玩家单独处理其操作。关于在一台 Android 设备上同时支持多个控制器的方法详见仓库的 支持多个游戏控制器——其中指出可通过InputDevice.getDeviceIds()取得设备 ID并通过SparseArray将设备 ID 映射到玩家化身如Ship对象在onInputDeviceAdded()/onInputDeviceRemoved()回调中动态创建与移除化身。检测单个控制器的输入能力除了判断是否为游戏控制器我们可能还希望检查单个已接入控制器的具体输入性能——例如当游戏只依赖兼容的物理操控时。用下面两个方法检测控制器是否支持特定的按键码或坐标码在 Android 4.4API level 19及更高系统中调用InputDevice.hasKeys(int...)确定控制器是否支持某个按键码在 Android 3.1API level 12及更高系统中先调用InputDevice.getMotionRanges()再对返回的每个InputDevice.MotionRange对象调用getAxis()获得坐标轴 ID从而得到控制器支持的所有坐标轴。处理游戏手柄按键下图展示了 Android 如何将按键码和坐标值映射到实际的游戏手柄上图片来自 input/game-controller/game-controller-profiles.png。通用游戏手柄的按键码与坐标值映射示意图Figure 1.一个常用游戏手柄的外形图中标注对应的按键码 / 坐标值如下AXIS_HAT_X、AXIS_HAT_Y、DPAD_UP、DPAD_DOWN、DPAD_LEFT、DPAD_RIGHT——十字方向键AXIS_X、AXIS_Y、BUTTON_THUMBL——左摇杆AXIS_Z、AXIS_RZ、BUTTON_THUMBR——右摇杆BUTTON_XBUTTON_ABUTTON_YBUTTON_BBUTTON_R1——右肩键AXIS_RTRIGGER、AXIS_THROTTLE——右扳机AXIS_LTRIGGER、AXIS_BRAKE——左扳机BUTTON_L1——左肩键。通用按键码与推荐游戏动作游戏手柄产生的通用按键码包括BUTTON_A、BUTTON_B、BUTTON_SELECT和BUTTON_START。当按下 D-pad 中间的交叉按键时部分控制器会触发DPAD_CENTER按键码。游戏通过getKeyCode()或按键事件回调得到按键码后若事件与游戏相关就将其处理为对应的游戏动作。下表列出大多数通用游戏手柄按钮的推荐映射游戏动作按键码在主菜单中启动游戏或者在游戏过程中暂停/取消暂停BUTTON_START显示菜单BUTTON_SELECT和KEYCODE_MENU与 Android 导航设计规范中的 Back 导航行为一致KEYCODE_BACK返回到菜单中上一项BUTTON_B确认选择或者执行主要的游戏动作BUTTON_A和DPAD_CENTER* 游戏不应依赖Start、Select 或 Menu 按键的存在——不同控制器的按键配置差异很大。Tip可以考虑在游戏中提供配置界面让用户个性化控制器按键与游戏动作之间的映射。在 onKeyDown() 中处理按键下面的代码展示了如何重写onKeyDown()将BUTTON_A和DPAD_CENTER合并到一个游戏动作开火中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; } }需要注意几个关键点通过event.getSource()与SOURCE_GAMEPAD做位与判断先确认事件来自游戏手柄通过event.getRepeatCount() 0过滤按键重复触发的KEY_DOWN事件长按重复返回true表示事件已被消费否则交给super.onKeyDown()继续沿事件分发链传递。Note在 Android 4.2API level 17及更低版本中系统默认会把BUTTON_A当作 Android 的Back返回键。如果应用支持这些版本请务必通过Build.VERSION.SDK_INT判断当前 SDK 版本并将BUTTON_A转换为主游戏动作避免行为冲突。处理 D-pad 输入四方向的方向键D-pad是很多游戏控制器常见的物理控制。Android 将 D-pad 的上/下按压报告为AXIS_HAT_Y事件范围从 -1.0上到 1.0下将左/右按压报告为AXIS_HAT_X事件范围从 -1.0左到 1.0右。部分游戏控制器会把 D-pad 按压报告为按键码。因此游戏在处理 D-pad 输入时应把坐标值事件和 D-pad 按键码当作同一输入事件处理。下表是推荐的默认映射游戏动作D-pad 按键码坐标值向上KEYCODE_DPAD_UPAXIS_HAT_Y从 0 到 -1.0向下KEYCODE_DPAD_DOWNAXIS_HAT_Y从 0 到 1.0向左KEYCODE_DPAD_LEFTAXIS_HAT_X从 0 到 -1.0向右KEYCODE_DPAD_RIGHTAXIS_HAT_X从 0 到 1.0Dpad 统一解析 Helper下面的 helper 类同时兼容按键码与 hat 轴坐标值两种事件来源从InputEventKeyEvent与MotionEvent的共同父类中解析出 D-pad 方向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; } } }注意代码中isDpadDevice()的判断条件当(source SOURCE_DPAD) ! SOURCE_DPAD时返回true与常规语义相反实际使用时建议改为逻辑仅当设备位字段包含SOURCE_DPAD时才视为 D-pad 设备避免非 D-pad 输入误入解析分支。这也是从源码结构推断、值得在实战中修正的细节。可以在任意需要处理 D-pad 输入的位置使用该 helper 类例如onGenericMotionEvent()或onKeyDown()回调中Dpad mDpad 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 mDpad.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 会高效地批量打包坐标值历史值比当前坐标值旧但比之前报告的任意移动事件新。这是摇杆输入特有的关键特性决定了处理顺序调用getAxisValue()或getHistoricalAxisValue(int, int)获取当前值和历史值调用getHistorySize()获取摇杆事件的历史样本数量。下面的代码重写onGenericMotionEvent()处理摇杆输入先处理历史坐标值再处理当前值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 区域使用摇杆输入前需要先确定摇杆是否居中再计算相应的坐标移动距离。一般摇杆会有一个平面区flat region——即坐标 (0, 0) 附近的一个值范围范围内的坐标点都被视为中点。如果 Android 报告坐标值落在平面区内应认为控制器处于静止沿 x、y 轴均无移动。下面的 helper 方法用于计算沿每个坐标轴的移动距离通过InputDevice.MotionRange.getFlat()获取平面区半径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; }综合处理摇杆移动将以上方法组合起来得到完整的摇杆移动处理逻辑。processJoystickInput()对左摇杆、hat 轴和右摇杆分别取样并回退到兼容的坐标轴private void processJoystickInput(MotionEvent event, int historyPos) { InputDevice mInputDevice 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, mInputDevice, MotionEvent.AXIS_X, historyPos); if (x 0) { x getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_X, historyPos); } if (x 0) { x getCenteredAxis(event, mInputDevice, 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, mInputDevice, MotionEvent.AXIS_Y, historyPos); if (y 0) { y getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_HAT_Y, historyPos); } if (y 0) { y getCenteredAxis(event, mInputDevice, MotionEvent.AXIS_RZ, historyPos); } // Update the ship object based on the new x and y values }双摇杆与肩键/扳机扩展为了支持比单个摇杆更复杂的功能需要额外考虑以下两类输入处理两个控制器摇杆。很多游戏控制器左右两侧各有一个摇杆左摇杆的水平移动报告为AXIS_X、垂直移动报告为AXIS_Y右摇杆的水平移动报告为AXIS_Z、垂直移动报告为AXIS_RZ。务必在代码中同时处理两个摇杆上述processJoystickInput()已通过AXIS_X → AXIS_HAT_X → AXIS_Z与AXIS_Y → AXIS_HAT_Y → AXIS_RZ的回退链覆盖了这一场景。处理肩键按压但需提供替代输入方式。一些控制器带左右肩键Android 将左肩键按压报告为AXIS_LTRIGGER、右肩键按压报告为AXIS_RTRIGGER。在 Android 4.3API level 18中产生AXIS_LTRIGGER的控制器也会报告完全一致的AXIS_BRAKE坐标值同理AXIS_RTRIGGER对应AXIS_GAS。Android 将模拟按键按压报告为从 0.0释放到 1.0按下的标准值。并非所有控制器都有肩键因此需要允许玩家用其它按钮执行这些游戏动作。进阶让输入层跨版本、多人可用单篇 controller-inputs 教程解决了如何读输入的问题而要让输入层真正可商用仓库同目录下的两篇姊妹篇提供了必要补充在不同的 Android 系统版本支持控制器通过InputManagerCompat抽象接口 InputManagerV16Android 4.1代理系统InputManager与InputManagerV9Android 2.3 起基于Handler轮询InputDevice.getDevice()检测连接/断开两套实现配合 factory 按Build.VERSION.SDK_INT运行时切换使游戏在 Android 2.3API level 9及更高版本上行为一致。其中onGenericMotionEvent()的实现正是本文介绍的 View 回调支持多个游戏控制器用SparseArray维护设备 ID → 玩家化身映射并在onKeyDown()中通过event.getDeviceId()定位对应玩家。多控制器的最佳实践是——当用户的控制器断开时暂停游戏并询问是否重新连接。此外在 tv/games/index.md 的 TV 游戏开发指南中还给出了与本文配套的实战约束TV 设备以 D-pad 为默认控制方案应坚持A 确认、B 取消的一致按键映射如需声明游戏必须使用手柄应在 Manifest 中加入uses-feature android:nameandroid.hardware.gamepad/非必需则加android:requiredfalse。这些细节共同构成了一份完整的控制器接入与产品化清单。小结处理控制器输入的本质是把 Android 系统层面的两类事件——KeyEvent按键码与MotionEvent坐标值——通过 View 回调正确地翻译成游戏动作。本文完整覆盖了验证连接getSources()位字段检测、手柄按键映射推荐动作表 onKeyDown()实现、D-pad 双通道解析AXIS_HAT_X/Y与KEYCODE_DPAD_*统一处理、摇杆批处理历史样本 flat 区居中判定 双摇杆/扳机回退链四个核心环节。将这些代码与仓库中的 多控制器、跨版本兼容 方案组合即可构建一套健壮、可移植的游戏输入层。赞分享文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese点击查看免费下载相关推荐Amethyst 输入处理实战从 InputHandler 到 RON 按键配置文件的完整指南Amethyst 输入处理实战从 InputHandler 到 RON 按键配置文件的完整指南 本篇技术指南围绕 Amethyst用 Rust 编写的数据导GameAISDK 实战Windows 模拟器接入、按键动作配置与游戏录制完整指南GameAISDK 实战Windows 模拟器接入、按键动作配置与游戏录制完整指南 GameAISDK 是基于图像的游戏 AI 自动化框架其配套的 SDKT人工智能强化学习计算机视觉游戏开发测试LibreHardwareMonitor 使用指南免费、完整地监控 CPU 温度、电压和风扇转速LibreHardwareMonitor 使用指南免费、完整地监控 CPU 温度、电压和风扇转速 LibreHardwareMonitor 是一款免费开源的硬指标监控上一篇HS2游戏终极增强指南3步完成模组安装的完整教程下一篇泉盛UV-K5/K6全功能固件深度解析与实战配置创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考