必须先触发用户交互(如按键)后调用 navigator.getGamepads() 才能获取有效数据,且需用 requestAnimationFrame 轮询并校验 gp.timestamp 防止冻结;buttons/axes 长度因设备而异,应优先检测 gp.mapping === 'standard' 并结合 gp.id 动态映射。
navigator.getGamepads() 怎么用才拿到数据直接调用 navigator.getGamepads() 返回的永远是空数组或全是 null,不是 API 坏了,而是它只在「用户主动交互后」才开始返回有效手柄对象。浏览器出于隐私和安全限制,禁止页面静默访问输入设备。
必须等用户完成一次按键、摇杆移动或连接手柄后的首次按键触发(gamepadconnected 事件),之后才能稳定读取:
window.addEventListener('gamepadconnected', (e) => {
console.log('已连接:', e.gamepad.id);
// 此时再调用 getGamepads 才可靠
});
// 主循环中读取(推荐 requestAnimationFrame)
function pollGamepads() {
const gamepads = navigator.getGamepads();
const gp = gamepads[0]; // 取第一个手柄
if (gp) {
console.log('左摇杆 X:', gp.axes[0]); // -1.0 ~ +1.0
console.log('A 键按下:', gp.buttons[0].pressed);
}
requestAnimationFrame(pollGamepads);
}
buttons 和 axes 数组长度不固定不同手柄硬件规格差异大,buttons 长度可能是 4(经典 Xbox 360)、15(PS5 DualSense)、19(Switch Pro);axes 通常是 2(左摇杆)、4(加右摇杆)或 6(含扳机轴)。不能硬写 gp.buttons[1] 就代表 B 键——它可能在另一款手柄上是肩键。
gp.mapping === 'standard' 判断是否支持 Web 标准布局(Xbox/PS 系列较稳)gp.buttons[0].pressed 是 A/X/× 键,但需结合 gp.id 字符串判断厂商(含 'xbox' 或 'playstation')gamepadconnected 和 gamepaddisconnected 事件监听要点这两个事件只在用户操作时触发:插拔 USB 手柄、蓝牙配对成功、或首次按键唤醒。但注意,部分蓝牙手柄(尤其 Switch Joy-Con)在系统级休眠后断开,不会触发 gamepaddisconnected,得靠轮询检测 gp.timestamp 是否停滞。
e.gamepad 是完整手柄实例,可直接缓存,不用再从 getGamepads() 查找e.gamepad.index 是唯一 ID,对应 getGamepads()[index],不要用 id 字符串做数组索引pressed === false
最常被忽略的是「未启用活动状态」:GamepadAPI 要求手柄对象处于「活跃帧」中才更新数据。如果页面不可见(标签页切走、窗口最小化),或主线程长时间阻塞(比如跑了个死循环),axes 和 buttons 就会冻结在最后值或全零。
requestAnimationFrame 而非 setInterval 驱动轮询——前者受浏览器节流策略保护,能维持更新gp.timestamp:每次轮询时对比前后值,若不变说明数据未刷新,可能是手柄休眠或页面失活getGamepads() 返回空
id 解析和轴/键动态映射做成配置表。