Gamepad API 是浏览器中读取物理游戏手柄的唯一标准接口,需用户首次交互后才能调用 navigator.getGamepads(),并通过 gamepadconnected 事件监听连接、requestAnimationFrame 定期读取 axes/buttons(注意阈值判断与 timestamp 防重入),不依赖 HTML 标签。

HTML 中没有“手柄”这个东西,Gamepad API 才是浏览器里真正能读取物理游戏手柄的接口——它不依赖 HTML 标签,而是靠 JavaScript 主动探测和轮询。
怎么检测并连接手柄(navigator.getGamepads() 为什么总返回空数组)
常见现象:页面加载后调用 navigator.getGamepads(),结果全是 null,以为手柄没识别。其实这是正常行为:浏览器默认不主动监听手柄插拔,必须等用户有**一次主动的交互事件**(比如 click、keydown、gamepadconnected)后才允许访问设备数据。
- 必须在用户手势触发的回调里首次调用,例如:
button.addEventListener('click', () => { const pads = navigator.getGamepads(); console.log(pads[0]); // 此时才可能有有效数据 }); - 监听连接/断开事件更可靠:
window.addEventListener('gamepadconnected', e => { console.log('手柄已连接:', e.gamepad.id); }); window.addEventListener('gamepaddisconnected', e => { console.log('手柄已断开:', e.gamepad.id); }); - 不要在页面初始化时就轮询
getGamepads(),早期返回的数组是“快照”,且索引位置不稳定;应结合gamepadconnected后再开始定时读取
怎么读取摇杆和按键(gamepad.axes 和 gamepad.buttons 的实际取值逻辑)
摇杆不是“方向键”,按钮也不是“true/false”开关——它们是连续模拟信号,需自行设定阈值判断是否“按下”或“偏移”。不同手柄轴序、按钮映射差异极大(Xbox、PlayStation、Switch Pro 各不相同),不能硬编码索引。
-
gamepad.axes是浮点数组,典型范围[-1.0, 1.0],中间值约0.0;左右摇杆常为[axes[0], axes[1]]和[axes[2], axes[3]],但需用gamepad.mapping === 'standard'判断是否符合标准映射 -
gamepad.buttons是对象数组,每个元素含pressed(布尔)、value(模拟按钮如扳机键,0.0–1.0);不要只看pressed,像L2/R2在 Xbox 手柄上是buttons[6].value,不是buttons[6].pressed - 推荐用
gamepad.timestamp避免重复触发:仅当timestamp变化时才处理输入,防止一帧内多次读取导致逻辑错乱
为什么手柄操作有延迟或卡顿(requestAnimationFrame 轮询 vs setInterval)
用 setInterval(..., 16) 模拟 60fps 读取手柄,反而容易掉帧或累积延迟——浏览器无法保证定时器准时执行,且与屏幕刷新节奏脱节。真正的同步方式是绑定到渲染循环。
立即学习“前端免费学习笔记(深入)”;
- 必须用
requestAnimationFrame循环读取:function pollGamepad() { const pad = navigator.getGamepads()[0]; if (pad) handleInput(pad); requestAnimationFrame(pollGamepad); } pollGamepad(); - 注意:
getGamepads()返回的是同一引用对象,内部状态会随硬件实时更新,所以无需缓存整个对象,只需每次取最新axes/buttons - 移动端 Safari 对
Gamepad API支持极弱(iOS 17+ 仅部分支持,且需手动开启“辅助触控”模拟),别指望手机网页靠蓝牙手柄稳定运行
手柄输入不是“即插即用”的 HTML 属性,它是一套需要手动管理生命周期、适配硬件差异、对齐渲染节奏的底层交互系统。最常被忽略的,是那个必须由用户点击触发的初始权限,以及摇杆/扳机键必须用 value 而非 pressed 判断的细节。



















