使用 Gamepad API 實現控制器
本文將介紹如何使用 Gamepad API 為網頁遊戲實現一個有效的、跨瀏覽器的控制元件系統,使您可以使用遊戲主機手柄來控制您的網頁遊戲。文中將以 Enclave Games 建立的示例遊戲《Hungry Fridge》(貪吃冰箱)為例。
網頁遊戲的控制元件
在過去,連線電視玩遊戲主機上的遊戲與在 PC 上玩遊戲是完全不同的體驗,這主要是因為獨特的控制方式。最終,額外的驅動程式和外掛允許我們在桌面遊戲(無論是原生遊戲還是在瀏覽器中執行的遊戲)中使用遊戲主機手柄。現在我們有了 Gamepad API,它使我們能夠在沒有外掛的情況下,使用遊戲手柄來玩基於瀏覽器的遊戲。Gamepad API 透過提供一個介面來暴露按鈕按下和軸變化,這些資訊可以在 JavaScript 程式碼中使用以處理輸入。這對瀏覽器遊戲來說是美好的時光。
哪種遊戲手柄最好?
目前最受歡迎的遊戲手柄來自 Xbox 360、Xbox One、PS3 和 PS4——它們經過了大量測試,並且在 Windows 和 macOS 的瀏覽器中與 Gamepad API 的實現配合良好。
還有許多其他具有各種不同按鈕佈局的裝置,它們或多或少地在瀏覽器實現中工作。本文討論的程式碼使用了一些遊戲手柄進行了測試,但作者最喜歡的配置是在 macOS 上使用無線 Xbox 360 控制器和 Firefox 瀏覽器。
案例研究:《Hungry Fridge》
GitHub 的 Game Off II 比賽於 2013 年 11 月舉行,Enclave Games 決定參加。比賽的主題是“變化”,所以他們提交了一個遊戲,您需要透過點選健康的食物(蘋果、胡蘿蔔、生菜)來餵飽 Hungry Fridge,同時避開“壞”食物(啤酒、漢堡、披薩)。倒計時每隔幾秒鐘就會改變冰箱想要的食物型別,所以您必須小心並快速行動。
第二個隱藏的“變化”實現是將靜態冰箱轉變為一個功能齊全的移動、射擊和進食機器。當您連線控制器時,遊戲會發生顯著變化(Hungry Fridge 變成 Super Turbo Hungry Fridge),您可以使用 Gamepad API 來控制裝甲冰箱。您必須射擊食物,但同樣,您也必須找到冰箱在每個時間點想要的食物型別,否則您會損失能量。
該遊戲封裝了兩種截然不同的“變化”型別——好食物與壞食物,以及移動與桌面。
演示
《Hungry Fridge》的完整版遊戲首先構建完成,然後為了展示 Gamepad API 的實際應用並顯示 JavaScript 原始碼,建立了一個簡單的演示。它是 GitHub 上提供的 Gamepad API 內容套件的一部分,您可以在其中深入研究程式碼並準確瞭解其工作原理。
下面解釋的程式碼來自《Hungry Fridge》的完整版遊戲,但與演示版幾乎相同——唯一的區別是完整版使用 turbo 變數來決定遊戲是否以 Super Turbo 模式啟動。它獨立工作,因此即使未連線手柄也可以開啟。
注意:彩蛋時間:有一個隱藏選項可以在沒有連線手柄的情況下在桌面上啟動 Super Turbo Hungry Fridge——點選螢幕右上角的控制器圖示。它將以 Super Turbo 模式啟動遊戲,您將能夠使用鍵盤控制冰箱:A 和 D 用於旋轉炮塔左右,W 用於射擊,箭頭鍵用於移動。
實現
與 Gamepad API 一起使用的兩個重要事件是 gamepadconnected 和 gamepaddisconnected。第一個事件在瀏覽器檢測到新遊戲手柄連線時觸發,第二個事件在遊戲手柄斷開連線時觸發(無論是使用者物理斷開還是由於不活動)。在演示中,gamepadAPI 物件用於儲存與 API 相關的所有內容。
const gamepadAPI = {
controller: {},
turbo: false,
connect() {},
disconnect() {},
update() {},
buttonPressed() {},
buttons: [],
buttonsCache: [],
buttonsStatus: [],
axesStatus: [],
};
buttons 陣列包含 Xbox 360 的按鈕佈局
const gamepadAPI = {
// …
buttons: [
"DPad-Up", "DPad-Down", "DPad-Left", "DPad-Right",
"Start", "Back", "Axis-Left", "Axis-Right",
"LB", "RB", "Power", "A", "B", "X", "Y",
],
// …
};
對於其他型別的遊戲手柄,例如 PS3 控制器(或雜牌通用手柄),這可能會有所不同,因此您必須小心,不要僅僅假設您期望的按鈕就是您實際得到的按鈕。接下來,我們設定兩個事件監聽器來獲取資料。
window.addEventListener("gamepadconnected", gamepadAPI.connect);
window.addEventListener("gamepaddisconnected", gamepadAPI.disconnect);
由於安全策略,您必須先與控制器進行互動,頁面可見時事件才會觸發。如果 API 在使用者沒有任何互動的情況下工作,它可能會在使用者不知情的情況下被用於對他們進行指紋識別。
這兩個函式都相當簡單。
const gamepadAPI = {
// …
connect(evt) {
gamepadAPI.controller = evt.gamepad;
gamepadAPI.turbo = true;
console.log("Gamepad connected.");
},
};
connect() 函式將事件作為引數,並將 gamepad 物件分配給 gamepadAPI.controller 變數。我們只使用一個遊戲手柄,所以它是一個單一物件而不是一個遊戲手柄陣列。然後我們將 turbo 屬性設定為 true。(我們可以為此目的使用 gamepad.connected 布林值,但我們想有一個單獨的變數來開啟 Turbo 模式,而無需連線遊戲手柄,原因如上所述。)
const gamepadAPI = {
// …
disconnect(evt) {
gamepadAPI.turbo = false;
delete gamepadAPI.controller;
console.log("Gamepad disconnected.");
},
};
disconnect 函式將 gamepad.turbo 屬性設定為 false,並刪除包含遊戲手柄物件的變數。
Gamepad 物件
gamepad 物件包含大量有用資訊,其中按鈕和軸的狀態是最重要的。
id:一個包含控制器資訊的字串。index:已連線裝置的唯一識別符號。connected:一個布林變數,如果裝置已連線則為true。mapping:按鈕的佈局型別;目前standard是唯一可用的選項。axes:每個軸的狀態,表示為浮點數值陣列。buttons:每個按鈕的狀態,表示為GamepadButton物件陣列,其中包含pressed和value屬性。
如果您連線了多個控制器並想識別它們以採取相應行動,例如在一個需要兩個裝置連線的雙人遊戲中,index 變數就很有用。
查詢 Gamepad 物件
除了 connect() 和 disconnect() 之外,gamepadAPI 物件還有另外兩個方法:update() 和 buttonPressed()。update() 在遊戲迴圈的每一幀中執行,以定期更新遊戲手柄物件的實際狀態。
const gamepadAPI = {
// …
update() {
// Clear the buttons cache
gamepadAPI.buttonsCache = [];
// Move the buttons status from the previous frame to the cache
for (let k = 0; k < gamepadAPI.buttonsStatus.length; k++) {
gamepadAPI.buttonsCache[k] = gamepadAPI.buttonsStatus[k];
}
// Clear the buttons status
gamepadAPI.buttonsStatus = [];
// Get the gamepad object
const c = gamepadAPI.controller || {};
// Loop through buttons and push the pressed ones to the array
const pressed = [];
if (c.buttons) {
for (let b = 0; b < c.buttons.length; b++) {
if (c.buttons[b].pressed) {
pressed.push(gamepadAPI.buttons[b]);
}
}
}
// Loop through axes and push their values to the array
const axes = [];
if (c.axes) {
for (const ax of c.axes) {
axes.push(ax.toFixed(2));
}
}
// Assign received values
gamepadAPI.axesStatus = axes;
gamepadAPI.buttonsStatus = pressed;
// Return buttons for debugging purposes
return pressed;
},
};
在每一幀中,update() 會將上一幀中按下的按鈕儲存到 buttonsCache 陣列,並從 gamepadAPI.controller 物件獲取新的按鈕狀態。然後它迴圈遍歷按鈕和軸以獲取它們的實際狀態和值。
檢測按鈕按下
buttonPressed() 方法也放置在主遊戲迴圈中以監聽按鈕按下。它接受兩個引數——要監聽的按鈕,以及(可選的)告知遊戲接受按住按鈕的方式。如果沒有這個,您將不得不釋放按鈕然後再次按下才能產生所需的效果。
const gamepadAPI = {
// …
buttonPressed(button, hold) {
let newPress = false;
if (GamepadAPI.buttons.status.includes(button)) {
newPress = true;
}
if (!hold && GamepadAPI.buttons.cache.includes(button)) {
newPress = false;
}
return newPress;
},
};
對於按鈕,有兩種型別的動作需要考慮:單擊和長按。newPress 布林變數將指示是否有新的按鈕按下。接下來,我們檢查按下按鈕的陣列——如果給定的按鈕在此陣列中,則 newPress 變數設定為 true。要檢查是否是新按下(即玩家沒有按住鍵),我們會檢查遊戲迴圈上一幀快取的按鈕狀態。如果我們在此處找到它,則意味著該按鈕正在被按住,因此不是新按下。最後返回 newPress 變數。buttonPressed 函式在遊戲的更新迴圈中按如下方式使用:
if (gamepadAPI.turbo) {
if (gamepadAPI.buttonPressed("A", "hold")) {
this.turbo_fire();
}
if (gamepadAPI.buttonPressed("B")) {
this.managePause();
}
}
如果 gamepadAPI.turbo 為 true 並且按下了(或按住了)給定的按鈕,我們將執行分配給它們的適當函式。在這種情況下,按下或按住 A 將發射子彈,按下 B 將暫停遊戲。
軸閾值
按鈕只有兩個狀態:0 或 1,但模擬搖桿可以有許多值——它們在 X 和 Y 軸上具有介於 -1 和 1 之間的浮點範圍。
遊戲手柄可能會因為長時間閒置而積灰,這意味著檢查精確的 -1 或 1 值可能會有問題。因此,為軸值設定一個閾值以使其生效可能是一個好主意。例如,冰箱坦克僅在 X 值大於 0.5 時才會向右轉。
if (gamepadAPI.axesStatus[0].x > 0.5) {
this.player.angle += 3;
this.turret.angle += 3;
}
即使我們不小心稍微移動了它,或者搖桿沒有回到原始位置,坦克也不會意外地轉動。
規範更新
在一年多的穩定後,W3C Gamepad API 規範於 2015 年 4 月進行了更新(檢視最新版本)。它變化不大,但瞭解最新動態很重要——更新如下。
獲取遊戲手柄
Navigator.getGamepads() 方法已更新,並附有更詳細的解釋和示例程式碼。現在,遊戲手柄陣列的長度必須為 n+1,其中 n 是已連線裝置的數量——當有一個裝置連線並且其索引為 1 時,陣列的長度為 2,其外觀如下:[null, [object Gamepad]]。如果裝置斷開連線或不可用,其值將設定為 null。
標準對映
對映型別現在是列舉物件而不是字串。
enum GamepadMappingType {
"",
"standard",
}
此列舉定義了一組已知的 Gamepad 對映。目前,只有 standard 佈局可用,但將來可能會出現新的佈局。如果佈局未知,則將其設定為空字串。
事件
規範中提供的事件比 gamepadconnected 和 gamepaddisconnected 要多,但它們已被移除,因為它們被認為用處不大。關於是否應該以某種形式將它們重新引入,討論仍在進行中。
總結
Gamepad API 非常易於開發。現在,在無需任何外掛的情況下,將類似遊戲主機的體驗交付到瀏覽器比以往任何時候都更容易。您可以直接在瀏覽器中玩《Hungry Fridge》的完整版遊戲。檢視 Gamepad API 內容套件上的其他資源。