桌面遊戲手柄控制元件

現在我們來看如何新增一些額外的東西——透過 Gamepad API 支援遊戲手柄控制元件。它為您的網頁遊戲帶來了類似主機的體驗。

Gamepad API 使您能夠將遊戲手柄連線到計算機,並透過 JavaScript 程式碼直接檢測按下的按鈕,這得益於瀏覽器實現了此功能。API 暴露了您將遊戲邏輯連線起來併成功控制使用者介面和遊戲玩法所需的所有資訊。

API 狀態、瀏覽器和硬體支援

Gamepad API 仍處於工作草案狀態,但瀏覽器支援已經相當好——根據 caniuse.com 的資料,全球覆蓋率約為 63%。支援的裝置列表也相當廣泛——大多數流行的遊戲手柄(例如 XBox 360 或 PS3)都應該適合 Web 實現。

純 JavaScript 方法

首先,讓我們考慮在我們的 小控制元件演示 中實現純 JavaScript 遊戲手柄控制元件,以瞭解其工作原理。首先,我們需要一個事件監聽器來監聽新裝置的連線

js
window.addEventListener("gamepadconnected", gamepadHandler);

這隻執行一次,所以我們可以建立一些稍後需要用來儲存控制器資訊和已按下按鈕的變數

js
let controller = {};
let buttonsPressed = [];
function gamepadHandler(e) {
  controller = e.gamepad;
  output.textContent = `Gamepad: ${controller.id}`;
}

gamepadHandler 函式中的第二行會在裝置連線時顯示在螢幕上

Gamepad connected message under the Captain Rogers game - wireless XBox 360 controller.

我們還可以顯示裝置的 id——在上面的例子中,我們使用的是 XBox 360 無線控制器。

要更新遊戲手柄當前按下按鈕的狀態,我們需要一個函式,它會在每一幀都執行此操作

js
function gamepadUpdateHandler() {
  buttonsPressed = [];
  if (controller.buttons) {
    for (const [i, button] of controller.buttons.entries()) {
      if (button.pressed) {
        buttonsPressed.push(i);
      }
    }
  }
}

我們首先重置 buttonsPressed 陣列,以便為儲存我們將在當前幀中寫入的最新資訊做好準備。然後,如果按鈕可用,我們迴圈遍歷它們;如果 pressed 屬性設定為 true,那麼我們將其新增到 buttonsPressed 陣列中以供後續處理。接下來,我們將考慮 gamepadButtonPressedHandler() 函式

js
function gamepadButtonPressedHandler(button) {
  return buttonsPressed.includes(button);
}

該函式以按鈕索引作為引數;它會檢查 buttonsPressed 是否包含我們正在查詢的按鈕,如果包含則返回 true。這會檢查按鈕是否被按下。

接下來,在 draw() 函式中,我們做了兩件事——執行 gamepadUpdateHandler() 函式以在每一幀獲取當前按下按鈕的狀態,並使用 gamepadButtonPressedHandler() 函式來檢查我們感興趣的按鈕是否被按下,如果被按下則執行某些操作

js
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // …

  gamepadUpdateHandler();
  if (gamepadButtonPressedHandler(0)) {
    playerY -= 5;
  } else if (gamepadButtonPressedHandler(1)) {
    playerY += 5;
  }
  if (gamepadButtonPressedHandler(2)) {
    playerX -= 5;
  } else if (gamepadButtonPressedHandler(3)) {
    playerX += 5;
  }
  if (gamepadButtonPressedHandler(11)) {
    alert("BOOM!");
  }

  // …

  ctx.drawImage(img, playerX, playerY);
  requestAnimationFrame(draw);
}

在這種情況下,我們正在檢查四個方向鍵按鈕(0-3)和 A 按鈕(11)。

注意:請記住,不同的裝置可能有不同的按鍵對映,即無線 XBox 360 的方向鍵右按鈕索引為 3,但在其他裝置上可能不同。

您還可以建立一個輔助函式來為列出的按鈕分配適當的名稱,因此,例如,而不是檢查 gamepadButtonPressedHandler(3) 是否被按下,您可以進行更具描述性的檢查:gamepadButtonPressedHandler('DPad-Right')

您可以看到一個 即時演示——嘗試連線您的遊戲手柄並按下按鈕。

Phaser 方法

讓我們繼續將 Gamepad API 的最終實現應用到我們使用 Phaser 建立的 Captain Rogers: Battle at Andromeda 遊戲中。不過,這也是純 JavaScript 程式碼,因此可以用於任何其他專案,無論使用什麼框架。

首先,我們將建立一個小庫來處理輸入。這是 GamepadAPI 物件,其中包含有用的變數和函式

js
const GamepadAPI = {
  active: false,
  controller: {},
  connect(event) {},
  disconnect(event) {},
  update() {},
  buttons: {
    layout: [],
    cache: [],
    status: [],
    pressed(button, state) {},
  },
  axes: {
    status: [],
  },
};

controller 變數儲存有關已連線遊戲手柄的資訊,還有一個 active 布林變數,我們可以用它來知道控制器是否已連線。connect()disconnect() 函式繫結到以下事件

js
window.addEventListener("gamepadconnected", GamepadAPI.connect);
window.addEventListener("gamepaddisconnected", GamepadAPI.disconnect);

它們分別在遊戲手柄連線和斷開時觸發。下一個函式是 update(),它會更新有關按下按鈕和搖桿的資訊。

buttons 變數包含給定控制器的 layout(例如,哪些按鈕在哪裡,因為 XBox 360 的佈局可能與通用控制器不同),cache 包含上一幀按鈕的資訊,status 包含當前幀的資訊。

pressed() 函式獲取輸入資料並將相關資訊設定在我們的物件中,而 axes 屬性儲存一個包含表示搖桿按下程度的浮點值的陣列,在 xy 方向上,用 (-1, 1) 範圍內的浮點數表示。

遊戲手柄連線後,控制器資訊會儲存在物件中

js
const GamepadAPI = {
  // …
  connect(event) {
    GamepadAPI.controller = event.gamepad;
    GamepadAPI.active = true;
  },
  // …
};

disconnect 函式會從物件中移除資訊

js
const GamepadAPI = {
  // …
  disconnect(event) {
    delete GamepadAPI.controller;
    GamepadAPI.active = false;
  },
};

update() 函式在遊戲的更新迴圈的每一幀執行,因此它包含有關按下按鈕的最新資訊

js
const GamepadAPI = {
  // …
  update() {
    GamepadAPI.buttons.cache = [];
    for (let k = 0; k < GamepadAPI.buttons.status.length; k++) {
      GamepadAPI.buttons.cache[k] = GamepadAPI.buttons.status[k];
    }
    GamepadAPI.buttons.status = [];
    const c = GamepadAPI.controller || {};
    const pressed = [];
    if (c.buttons) {
      for (let b = 0; b < c.buttons.length; b++) {
        if (c.buttons[b].pressed) {
          pressed.push(GamepadAPI.buttons.layout[b]);
        }
      }
    }
    const axes = [];
    if (c.axes) {
      for (const ax of c.axes) {
        axes.push(ax.toFixed(2));
      }
    }
    GamepadAPI.axes.status = axes;
    GamepadAPI.buttons.status = pressed;
    return pressed;
  },
  // …
};

上面的函式會清除按鈕快取,並將上一幀的按鈕狀態複製到快取中。接下來,清除按鈕狀態並新增新資訊。搖桿資訊也是如此——迴圈遍歷搖桿會將值新增到陣列中。接收到的值會分配給相應的物件,並返回按下資訊以供除錯。

button.pressed() 函式會檢測實際的按鈕按下情況

js
const GamepadAPI = {
  // …
  buttons: {
    // …
    pressed(button, hold) {
      let newPress = false;
      if (GamepadAPI.buttons.status.includes(button)) {
        newPress = true;
      }
      if (!hold && GamepadAPI.buttons.cache.includes(button)) {
        newPress = false;
      }
      return newPress;
    },
    // …
  },
  // …
};

它會檢查我們正在尋找的按鈕是否被按下,如果按下,則將相應的布林變數設定為 true。如果我們想檢查按鈕是否尚未被按住(也就是說,這是一次新的按下),那麼檢查上一幀的快取狀態就可以做到——如果按鈕已經被按下,那麼我們就忽略新的按下並將其設定為 false

實現

現在我們知道了 GamepadAPI 物件的樣子以及它包含哪些變數和函式,那麼讓我們來學習一下所有這些在遊戲中是如何實際使用的。為了指示遊戲手柄控制器已啟用,我們可以向用戶顯示一些自定義文字在遊戲的主選單螢幕上。

textGamepad 物件儲存著表示已連線遊戲手柄的文字,預設情況下是隱藏的。這是我們在 create() 函式中準備的程式碼,該函式在新狀態建立時執行一次

js
function create() {
  // …
  const message = "Gamepad connected! Press Y for controls";
  const textGamepad = this.add.text(0, 0, message);
  textGamepad.visible = false;
}

update() 函式中,該函式每幀執行一次,我們可以等到控制器真正連線後,然後顯示相應的文字。然後我們可以透過使用 Gamepad.update() 方法來跟蹤已按下按鈕的資訊,並根據給定資訊做出反應

js
function update() {
  // …
  if (GamepadAPI.active) {
    this.textGamepad.visible = true;

    GamepadAPI.update();
    if (GamepadAPI.buttons.pressed("Start")) {
      // start the game
    }
    if (GamepadAPI.buttons.pressed("X")) {
      // turn on/off the sounds
    }

    this.screenGamepadHelp.visible = GamepadAPI.buttons.pressed("Y", "hold");
  }
}

按下 Start 按鈕時,會呼叫相關函式來開始遊戲,同樣的方法也用於開啟和關閉音訊。有一個選項可以顯示 screenGamepadHelp,其中包含一張解釋所有按鈕控制元件的圖片——如果按下並按住 Y 按鈕,幫助資訊就會顯示出來;當釋放該按鈕時,幫助資訊就會消失。

Gamepad info with all the available keys described and explained.

螢幕上的說明

當遊戲啟動時,會顯示一些介紹性文字,顯示可用控制元件——我們已經檢測到遊戲是在桌面還是移動裝置上啟動,然後為裝置顯示相應的訊息,但我們可以做得更多,以允許使用遊戲手柄

js
function create() {
  // …
  if (this.game.device.desktop) {
    if (GamepadAPI.active) {
      moveText = "DPad or left Stick\nto move";
      shootText = "A to shoot,\nY for controls";
    } else {
      moveText = "Arrow keys\nor WASD to move";
      shootText = "X or Space\nto shoot";
    }
  } else {
    moveText = "Tap and hold to move";
    shootText = "Tap to shoot";
  }
}

在桌面模式下,我們可以檢查控制器是否處於活動狀態並顯示遊戲手柄控制元件——如果不是,則顯示鍵盤控制元件。

遊戲玩法控制

我們可以透過為主玩家提供主要和輔助遊戲手柄移動控制元件來提供更多靈活性

js
if (GamepadAPI.buttons.pressed("DPad-Up", "hold")) {
  // move player up
} else if (GamepadAPI.buttons.pressed("DPad-Down", "hold")) {
  // move player down
}

if (GamepadAPI.buttons.pressed("DPad-Left", "hold")) {
  // move player left
}

if (GamepadAPI.buttons.pressed("DPad-Right", "hold")) {
  // move player right
}

if (GamepadAPI.axes.status) {
  if (GamepadAPI.axes.status[0] > 0.5) {
    // move player up
  } else if (GamepadAPI.axes.status[0] < -0.5) {
    // move player down
  }

  if (GamepadAPI.axes.status[1] > 0.5) {
    // move player left
  } else if (GamepadAPI.axes.status[1] < -0.5) {
    // move player right
  }
}

他們現在可以使用 DPad 按鈕或左搖桿軸在螢幕上移動飛船。

您是否注意到軸的當前值是根據 0.5 進行評估的?這是因為軸具有浮點值,而按鈕是布林值。達到一定閾值後,我們可以假定輸入是由使用者有意進行的,並據此採取行動。

對於射擊控制元件,我們使用了 A 按鈕——當它被按住時,會生成一個新的子彈,其餘的都由遊戲處理

js
if (GamepadAPI.buttons.pressed("A", "hold")) {
  this.spawnBullet();
}

顯示所有控制元件的螢幕與主選單中的外觀完全相同

js
this.screenGamepadHelp.visible = GamepadAPI.buttons.pressed("Y", "hold");

如果按下 B 按鈕,遊戲將被暫停

js
if (gamepadAPI.buttonPressed("B")) {
  this.managePause();
}

暫停和遊戲結束狀態

我們已經學會了如何控制遊戲的整個生命週期:暫停遊戲、重新開始遊戲或返回主選單。它在移動裝置和桌面上都能流暢執行,並且新增遊戲手柄控制元件同樣簡單——在 update() 函式中,我們檢查當前狀態是否為“暫停”——如果是,則啟用相關操作

js
if (GamepadAPI.buttons.pressed("Start")) {
  this.managePause();
}

if (GamepadAPI.buttons.pressed("Back")) {
  this.stateBack();
}

類似地,當“遊戲結束”狀態處於活動狀態時,我們可以允許使用者重新開始遊戲而不是繼續遊戲

js
if (GamepadAPI.buttons.pressed("Start")) {
  this.stateRestart();
}
if (GamepadAPI.buttons.pressed("Back")) {
  this.stateBack();
}

當遊戲結束螢幕可見時,Start 按鈕會重新開始遊戲,而 Back 按鈕會幫助我們返回主選單。當遊戲暫停時也是如此:Start 按鈕會取消暫停遊戲,Back 按鈕會返回,就像之前一樣。

總結

就是這樣!我們已成功在遊戲中實現了遊戲手柄控制元件——嘗試連線任何流行的控制器,如 XBox 360 控制器,親身體驗用遊戲手柄躲避小行星並射擊外星人有多有趣。

現在,我們可以繼續探索新的、甚至更不尋常的 HTML 遊戲控制方式,例如對著筆記型電腦揮手或對著麥克風大喊。