刷入固件后的 Stadia 控制器就像一个标准游戏手柄,这意味着并非所有按钮都可以通过 Gamepad API 访问。借助 WebHID,您现在可以访问缺少的按钮。
自 Stadia 关闭以来,许多人担心该控制器最终会成为垃圾填埋场中毫无用处的硬件。幸运的是,Stadia 团队决定开放 Stadia 控制器,并提供自定义固件,您只需前往 Stadia 蓝牙模式页面,即可将该固件刷入控制器。这样一来,您的 Stadia 控制器就会显示为标准游戏手柄,您可以通过 USB 线或蓝牙无线连接到该控制器。Stadia Bluetooth 页面本身使用 WebHID 和 WebUSB,但这不是本文的主题。该页面很荣幸在 Project Fugu API 展示中亮相。在这篇博文中,我想介绍一下如何通过 WebHID 与 Stadia 控制器通信。
将 Stadia 控制器用作标准游戏手柄
刷写后,控制器在操作系统中显示为标准游戏手柄。有关标准游戏手柄上常见的按钮和轴排列,请参见以下屏幕截图。如 Gamepad API 规范中所定义,标准游戏手柄的按钮编号为 0 到 16,总共 17 个(方向键算作四个按钮)。如果您在游戏手柄测试器演示版中尝试使用 Stadia 控制器,会发现它运行得非常顺畅。
不过,如果您数一下 Stadia 控制器上的按钮,会发现有 19 个。如果您在手柄测试器中逐个尝试这些按钮,就会发现 Google 助理和拍摄按钮不起作用。即使游戏手柄规范中定义的 buttons
属性是开放式的,但由于 Stadia 控制器显示为标准游戏手柄,因此仅映射按钮 0-16。您仍然可以使用其他按钮,但大多数游戏不会预期这些按钮的存在。
WebHID 来助您一臂之力
借助 WebHID API,您可以与缺失的按钮 17 和 18 进行通信。如果您确实需要,甚至可以获取有关已通过 Gamepad API 提供的所有其他按钮和轴的数据。第一步是了解 Stadia 控制器如何向操作系统报告自身。一种方法是在任意随机网页上打开 Chrome 开发者工具控制台,然后通过 WebHID API 请求未经过滤的设备列表。然后,您手动选择 Stadia 控制器以进行进一步检查。只需传递一个空的 filters
选项数组,即可获取未经过滤的设备列表。
const [device] = await navigator.hid.requestDevice({filters: []});
在选择器中,倒数第二个条目看起来像 Stadia 控制器。
选择“Stadia Controller rev. A”设备后,将生成的 HIDDevice
对象记录到控制台。这会显示 Stadia 控制器的 productId
(37888
,十六进制为 0x9400
)和 vendorId
(6353
,十六进制为 0x18d1
)。如果您在官方 USB 供应商 ID 表中查找 vendorID
,您会发现 6353
映射到您预期的结果:Google Inc.
。
除了上述流程之外,您还可以前往网址栏中的 chrome://device-log/
,按清除按钮,插入 Stadia 控制器,然后按刷新。这会为您提供相同的信息。
另一种替代方案是使用 HID Explorer 工具,该工具可让您探索连接到计算机的 HID 设备的更多详细信息。
使用这两个 ID(vendorId
和 productId
)来优化选择器中显示的内容,方法是现在正确过滤出合适的 WebHID 设备。
const [stadiaController] = await navigator.hid.requestDevice({filters: [{
vendorId: 6353,
productId: 37888,
}]});
现在,所有无关设备的噪音都消失了,只显示 Stadia 控制器。
接下来,通过调用 open()
方法打开 HIDDevice
。
await stadiaController.open();
再次记录 HIDDevice
,并将 opened
标志设置为 true
。
在设备处于打开状态时,通过附加事件监听器来监听传入的 inputreport
事件。
stadiaController.addEventListener('inputreport', (e) => {
console.log(e);
});
当您按下并松开控制器上的 Google 助理按钮时,系统会在控制台中记录两个事件。您可以将它们视为“Google 助理按钮按下”和“Google 助理按钮松开”事件。除了 timeStamp
之外,这两个事件乍一看似乎没有区别。
HIDInputReportEvent
接口的 reportId
属性会返回相应报告的单字节标识前缀,如果 HID 接口不使用报告 ID,则返回 0
。在本例中,该值为 3
。密钥位于 data
属性中,表示为大小为 10 的 DataView
。DataView
提供了一个低级接口,用于在二进制 ArrayBuffer
中读取和写入多种数字类型。若要从此表示法中获得更易于理解的内容,可以从 ArrayBuffer
创建 Uint8Array
,这样您就可以看到各个 8 位无符号整数。
const data = new Uint8Array(event.data.buffer);
当您再次记录输入报告事件数据时,情况开始变得更加清晰,“助理按钮按下”和“助理按钮松开”事件开始变得可解读。第一个整数(两个事件中的 8
)似乎与按钮按压有关,而第二个整数(2
和 0
)似乎与助理按钮是否被按压有关。
按 Capture 按钮,而不是 Assistant 按钮,您会看到第二个整数在按下按钮时从 1
切换到 0
,并在释放按钮时切换回 1
。这样一来,您就可以编写一个非常简单的“驱动程序”,从而能够使用缺少的两个按钮。
stadia.addEventListener('inputreport', (event) => {
if (!e.reportId === 3) {
return;
}
const data = new Uint8Array(event.data.buffer);
if (data[0] === 8) {
if (data[1] === 1) {
hidButtons[1].classList.add('highlight');
} else if (data[1] === 2) {
hidButtons[0].classList.add('highlight');
} else if (data[1] === 3) {
hidButtons[0].classList.add('highlight');
hidButtons[1].classList.add('highlight');
} else {
hidButtons[0].classList.remove('highlight');
hidButtons[1].classList.remove('highlight');
}
}
});
通过这种逆向工程方法,您可以逐个按钮、逐个轴地了解如何使用 WebHID 与 Stadia 控制器通信。一旦掌握了这种方法,其余工作几乎就是机械式的整数映射。
现在唯一缺少的是 Gamepad API 提供的顺畅连接体验。出于安全考虑,您始终需要先完成一次初始选择器体验,才能使用 Stadia 控制器等 WebHID 设备,但对于未来的连接,您可以重新连接到已知设备。为此,请调用 getDevices()
方法。
let stadiaController;
const [device] = await navigator.hid.getDevices();
if (device && device.vendorId === 6353 && device.productId === 37888) {
stadiaController = device;
}
演示
您可以在我构建的演示中看到由 Gamepad API 和 WebHID API 共同控制的 Stadia 控制器。请务必查看源代码,该代码基于本文中的代码段构建。为简单起见,我仅显示 A、B、X 和 Y 按钮(由 Gamepad API 控制)以及助理和拍摄按钮(由 WebHID API 控制)。在控制器图片下方,您可以看到原始 WebHID 数据,从而了解控制器上的所有按钮和轴。
总结
借助新固件,Stadia 控制器现在可用作具有 17 个按钮的标准游戏手柄,在大多数情况下,这足以控制常见的网页游戏。如果您出于任何原因需要控制器上所有 19 个按钮的数据,WebHID 可让您访问低级输入报告,您可以通过对这些报告进行逆向工程来逐个解读。如果您在阅读本文后恰好编写了一个完整的 WebHID 驱动程序,请务必与我联系,我很乐意在此处添加指向您的项目的链接。祝您 WebHID 体验愉快!
致谢
本文由 François Beaufort 审核。