刷機後的 Stadia 控制器會像標準遊戲手把,因此並非所有按鈕都能透過 Gamepad API 存取。現在透過 WebHID,您就能存取缺少的按鈕。
自從 Stadia 關閉後,許多人擔心控制器會變成毫無用處的硬體,最終只能丟進垃圾掩埋場。幸好,Stadia 團隊決定開放使用 Stadia 控制器,並提供自訂韌體,只要前往 Stadia 藍牙模式頁面,即可將韌體刷入控制器。這樣一來,Stadia 控制器就會顯示為標準遊戲手把,你可以透過 USB 傳輸線連線,也可以透過藍牙以無線方式連線。Stadia 藍牙頁面榮登 Project Fugu API 展示頁面,本身使用 WebHID 和 WebUSB,但這不是本文的主題。在這篇文章中,我將說明如何透過 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
中讀取及寫入多種數字類型。如要從這個表示法取得更易消化的內容,請建立 Uint8Array
,這樣就能看到個別的 8 位元無正負號整數。ArrayBuffer
const data = new Uint8Array(event.data.buffer);
當您再次記錄輸入報告事件資料時,情況就會開始變得更有意義,「助理按鈕按下」和「助理按鈕放開」事件開始變得可解讀。第一個整數 (兩個事件中的 8
) 似乎與按鈕按壓次數有關,第二個整數 (2
和 0
) 則似乎與是否按下Google 助理按鈕有關。
按下「擷取」按鈕 (而非「Google 助理」按鈕),您會發現第二個整數會在按鈕按下時從 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 控制),以及「Google 助理」和「擷取」按鈕 (由 WebHID API 控制)。在控制器圖片下方,你可以看到原始 WebHID 資料,瞭解控制器上的所有按鈕和軸。
結論
更新韌體後,Stadia 控制器就能當做標準遊戲手把使用,共有 17 個按鈕,在大多數情況下,這已足夠控制常見的網頁遊戲。如果基於任何原因,您需要控制器上所有 19 個按鈕的資料,WebHID 可讓您存取低階輸入報表,並逐一進行反向工程,解讀這些報表。讀完本文後,如果您剛好撰寫了完整的 WebHID 驅動程式,請務必與我聯絡,我很樂意在此連結您的專案。祝您順利使用 WebHID!
特別銘謝
本文由 François Beaufort 審查。