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

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

Chrome 的離線頁面彩蛋是歷史上最容易洩漏的秘密之一 ([citation needed],但這只是為了營造戲劇效果而說的話)。如果您按下 空格鍵,或是在行動裝置上輕觸恐龍圖示,離線頁面就會變成可玩的街機遊戲。您可能知道,想玩遊戲時不一定要離線:在 Chrome 中,您可以前往 about://dino,或是前往 about://network-error/-106。不過,你知道每個月有 2 億人次玩 Chrome 恐龍遊戲嗎?

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

另一個您可能不知道,但相當實用的事實是,您可以在街機模式中使用遊戲控制器玩遊戲。在撰寫本文時,遊戲控制器支援功能大約在一年多前加入,當時是 Reilly Grantcommit。如您所見,這款遊戲與其他 Chromium 專案一樣,完全是開放原始碼。在本篇文章中,我想說明如何使用 Gamepad API。

使用 Gamepad API

功能偵測和瀏覽器支援

在電腦和行動裝置上,Gamepad API 都普遍支援瀏覽器。您可以使用以下程式碼片段,偵測是否支援 Gamepad API:

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

瀏覽器如何呈現遊戲控制器

瀏覽器會將遊戲控制器表示為 Gamepad 物件。Gamepad 包含下列屬性:

  • id:遊戲控制器的 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"}
  */
});

在遊戲控制器中斷連線時發出通知

接收 Gamepad 連線中斷通知的方式,與偵測連線的方式類似。這次應用程式會監聽 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 先前的努力),將其放置在獨立網站上,並透過新增靜音和震動效果來擴充現有的遊戲搖桿 API 實作,建立全螢幕模式,而 Mehul Satardekar 則提供了深色模式實作。祝您遊戲愉快!

特別銘謝

本文件由 François BeaufortJoe Medley 審查。Steve AgostonJames HollyerMatt Reynolds 編輯了 Gamepad API 規格。前規格編輯為 Brandon JonesScott GrahamTed MielczarekBrandon Jones 編輯了 Gamepad Extensions 規格。主頁橫幅圖片由 Laura Torrent Puig 提供。