使用遊戲手把玩 Chrome 恐龍遊戲

瞭解如何使用 Gamepad API,讓網路遊戲更上一層樓。

Chrome 的離線頁面復活節彩蛋是史上最糟的秘密之一 ([citation needed],但這是因戲劇效應而得的秘密)。如果按下空格鍵,或在行動裝置上輕觸恐龍,離線頁面就會變成可以遊戲的街機遊戲。您可能知道,想玩遊戲時不需要離線觀看,只要使用 Chrome,直接前往 about://dino 即可;當然,您也可以前往 about://network-error/-106但您知道嗎?每月都有 2.7 億個 Chrome 恐龍遊戲嗎?

Chrome 顯示 Chrome 恐龍遊戲的離線頁面。
按下空格鍵即可開始遊戲!

值得注意的另一個事實是,大家知道更實用,但您可能不知道在遊戲機模式下,可以使用遊戲手把玩遊戲。在約一年前,我們約在一年前就在 Reilly Grant 撰寫的修訂中新增遊戲控制器的支援。如您所見,遊戲和其他 Chromium 專案一樣,都是完整的開放原始碼。這篇文章將說明如何使用 Gamepad API。

使用 Gamepad API

功能偵測和瀏覽器支援

在電腦和行動裝置上,Gamepad API 提供廣泛的瀏覽器支援。您可以使用下列程式碼片段偵測是否支援 Gamepad API:

if ('getGamepads' in navigator) {
  // The API is supported!
}

瀏覽器如何呈現遊戲搖桿

瀏覽器將遊戲搖桿顯示為 Gamepad 物件。Gamepad 具備下列屬性:

  • id:遊戲手把的識別字串。此字串可用於識別已連線遊戲手把裝置的品牌或樣式。
  • displayId:關聯 VRDisplayVRDisplay.displayId (如適用)。
  • index:導覽器中的遊戲手把的索引。
  • connected:指出遊戲手把是否仍與系統連線。
  • hand:定義控制器會以何種手持或最可能持有的手部為列舉。
  • timestamp:上次更新這個遊戲手把資料的時間。
  • mapping:這部裝置使用的按鈕和軸對應,可以是 "standard""xr-standard"
  • poseGamepadPose 物件,代表與 WebVR 控制器相關聯的姿勢資訊。
  • axes:遊戲手把所有軸的值陣列,依線性正規化為 -1.01.0 的範圍。
  • buttons:遊戲手把所有按鈕的按鈕狀態陣列。

請注意,按鈕可以是數位 (按下或未按下) 或類比 (例如按下的 78%)。因此,系統會將按鈕回報為 GamepadButton 物件,其中包含下列屬性:

  • pressed:按鈕的按下狀態 (按下按鈕時為 true,未按下時則為 false)。
  • touched:按鈕的觸控狀態。如果按鈕能夠偵測觸控,此屬性為 true (如果輕觸按鈕),反之則為 false
  • value:對於含有類比感應器的按鈕,這個屬性代表按下按鈕的程度,在 0.01.0 的範圍之間進行線性正規化。
  • hapticActuators:包含 GamepadHapticActuator 物件的陣列,每個物件代表控制器上可用的觸覺回饋硬體。

另外,根據瀏覽器和遊戲手台而定,您可能還會遇到一個 vibrationActuator 屬性。它可呈現兩種饒舌效應:

  • 雙重爆發:由兩個古怪旋轉的質面致動器產生觸覺回饋效果,讓玩家在一隻手掌上做出旋轉動作。
  • 觸發機:觸覺回饋效果是由兩個獨立馬達產生的觸覺回饋效果,每個遊戲手把的觸發器各有一個馬達。

下方的結構定義總覽 (從規格中簡單說明) 顯示的是一般遊戲手板上的按鈕和軸排列方式,以及按鈕和軸的排列方式。

常用遊戲搖桿的按鈕和軸對應結構總覽。
標準遊戲手把版面配置的示意圖 (來源)。

連接遊戲手把時的通知

如要瞭解遊戲手把何時連線,請監聽 window 物件上觸發的 gamepadconnected 事件。當使用者透過 USB 或使用藍牙連接遊戲手把時,系統會觸發 GamepadEvent,其中具有適當名稱的 gamepad 屬性包含遊戲手把的詳細資料。下方範例是我坐在 Xbox 360 控制器的例子中 (沒錯,我愛著復古遊戲)。

window.addEventListener('gamepadconnected', (event) => {
  console.log('✅ 🎮 A gamepad was connected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: true
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: GamepadHapticActuator {type: "dual-rumble"}
  */
});

遊戲手把中斷連線時的通知

如果收到遊戲手把連線中斷的通知,與偵測連線的方式類似。應用程式這次會監聽 gamepaddisconnected 事件。請注意,在我拔除 Xbox 360 控制器時,以下範例中的 connected 現在是 false

window.addEventListener('gamepaddisconnected', (event) => {
  console.log('❌ 🎮 A gamepad was disconnected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: false
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: null
  */
});

遊戲迴圈中的遊戲搖桿

要取得遊戲手把的初始狀態,首先必須呼叫 navigator.getGamepads(),並傳回包含 Gamepad 項目的陣列。Chrome 中的陣列「一律」固定包含四個項目。如果連結零或少於 4 個遊戲搖桿,項目可能只有 null。請隨時檢查陣列的所有項目,並請注意,遊戲控制器會「記住」其版位,且不一定會出現在第一個可用的版位。

// When no gamepads are connected:
navigator.getGamepads();
// (4) [null, null, null, null]

如果已連接一或多個遊戲搖桿,但 navigator.getGamepads() 仍然回報 null 項目,您可能需要按下任何遊戲搖桿來「喚醒」每個遊戲手把。接著,您可以在遊戲迴圈中輪詢遊戲手把狀態,如以下程式碼所示。

const pollGamepads = () => {
  // Always call `navigator.getGamepads()` inside of
  // the game loop, not outside.
  const gamepads = navigator.getGamepads();
  for (const gamepad of gamepads) {
    // Disregard empty slots.
    if (!gamepad) {
      continue;
    }
    // Process the gamepad state.
    console.log(gamepad);
  }
  // Call yourself upon the next animation frame.
  // (Typically this happens every 60 times per second.)
  window.requestAnimationFrame(pollGamepads);
};
// Kick off the initial game loop iteration.
pollGamepads();

震動致動器

vibrationActuator 屬性會傳回 GamepadHapticActuator 物件,該物件會對應至馬達或其他致動器的設定,可套用觸覺回饋來回應觸覺回饋。如要播放觸覺回饋效果,請呼叫 Gamepad.vibrationActuator.playEffect()。唯一有效的效果類型為 'dual-rumble'。雙頻譜描述觸覺設定,在標準遊戲手把的每個控點內,都設有新奇的旋轉大規模振動馬。在這項設定中,任一馬達能夠震動整個遊戲手把。這兩個質量不相同,因此兩者的效果可以結合,產生更複雜的觸覺效果。雙重凸顯效果由四個參數定義:

  • duration:設定震動效果的時間長度 (以毫秒為單位)。
  • startDelay:設定震動開始之前的延遲時間。
  • strongMagnitudeweakMagnitude:設定更重且心位輻射的大馬馬的震動強度等級,經過正規化為 0.01.0 的範圍。

支援的亂碼效果

if (gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
  // Trigger rumble supported.
} else if (gamepad.vibrationActuator.effects.includes('dual-rumble')) {
  // Dual rumble supported.
} else {
  // Rumble effects aren't supported.
}

雙頻

// This assumes a `Gamepad` as the value of the `gamepad` variable.
const dualRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  gamepad.vibrationActuator.playEffect('dual-rumble', {
    // Start delay in ms.
    startDelay: delay,
    // Duration in ms.
    duration: duration,
    // The magnitude of the weak actuator (between 0 and 1).
    weakMagnitude: weak,
    // The magnitude of the strong actuator (between 0 and 1).
    strongMagnitude: strong,
  });
};

觸發爆炸動作

// This assumes a `Gamepad` as the value of the `gamepad` variable.
const triggerRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  // Feature detection.
  if (!('effects' in gamepad.vibrationActuator) || !gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
    return;
  }
  gamepad.vibrationActuator.playEffect('trigger-rumble', {
    // Duration in ms.
    duration: duration,
    // The left trigger (between 0 and 1).
    leftTrigger: leftTrigger,
    // The right trigger (between 0 and 1).
    rightTrigger: rightTrigger,
  });
};

權限整合政策

Gamepad API 規格定義了以 "gamepad" 字串識別的由政策控管功能。預設為 allowlist"self"。文件的權限政策可決定文件中的任何內容是否可存取 navigator.getGamepads()。如果在任何文件中停用,所有文件中的內容都將無法使用 navigator.getGamepads(),也不會觸發 gamepadconnectedgamepaddisconnected 事件。

<iframe src="index.html" allow="gamepad"></iframe>

操作示範

以下範例嵌入了遊戲控制器試用版。原始碼可在 Glitch 上取得。請使用 USB 或藍牙連接遊戲手把,並按下任一按鈕或移動遊戲軸的任何按鈕,藉此試用這項示範。

獎勵:在 web.dev 暢玩 Chrome 恐龍遊戲

您可以在這個網站上使用遊戲手把玩 Chrome 恐龍遊戲。您可以在 GitHub 找到原始碼。請查看 trex-runner.js 中的遊戲手把輪詢實作方式,並留意此做法模擬按鍵操作的方式。

為順利執行 Chrome 恐龍遊戲手把示範,我從 Chromium 核心專案提供了 Chrome 恐龍遊戲 (由 Arnelle Ballane 完成更早的努力)、將遊戲架上的 API 實作整合至獨立網站、加入專為遊戲控制器提供的 API 實作項目,以及建立全螢幕模式,以及 Mehul Sadetar 模式。盡情享受遊戲樂趣!

特別銘謝

這份文件由 François BeaufortJoe Medley 審查。Gamepad API 規格是由 Steve AgostonJames HollyerMatt Reynolds 編輯。先前的規格編輯器為 Brandon JonesScott GrahamTed Mielczarek。Gamepad Extensions 規格是由 Brandon Jones 編輯。主頁橫幅:Laura Torrent Puig。