使用遊戲手把玩 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"
  • pose:代表與 WebVR 控制器相關聯的姿勢資訊的 GamepadPose 物件。
  • 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 中的陣列一律有四個項目的固定長度。如果連線的遊戲手把少於四個,項目可能只會顯示 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''trigger-rumble'

支援的震動效果

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.
}

雙震動

雙震動是指觸覺設定,在標準遊戲控制器的每個把手都設有偏心旋轉質量振動馬達。在此設定中,任一馬達都能震動整個遊戲手把。這兩個質量不相等,因此可以結合各自的效果,產生更複雜的觸覺效果。雙震動效果由四個參數定義:

  • duration:設定震動效果的持續時間 (以毫秒為單位)。
  • startDelay:設定震動開始前的延遲時間長度。
  • strongMagnitudeweakMagnitude:設定較重和較輕的偏心旋轉質量馬達的震動強度等級,並將其標準化至 0.01.0 的範圍。
// 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早期成果),並將其放在獨立網站上,透過新增閃避和震動效果擴充現有的 Gamepad API 實作項目,建立全螢幕模式,Mehul Satardekar 則貢獻了深色模式實作項目。祝你玩得開心!

特別銘謝

本文由 François BeaufortJoe Medley 審查。Gamepad API 規格由 Steve AgostonJames HollyerMatt Reynolds 編輯。前規格編輯者為 Brandon JonesScott GrahamTed Mielczarek。Gamepad 擴充功能規格由 Brandon Jones 編輯。主頁橫幅圖片由 Laura Torrent Puig 提供。