Gamepad API を使用してウェブゲームを次のレベルに引き上げる方法を学びます。
Chrome のオフライン ページのイースター エッグは、歴史上最もよく知られた秘密の一つです([citation needed]、ただし、ドラマチックな効果を狙った主張です)。スペースキーを押すか、モバイル デバイスで恐竜をタップすると、オフライン ページがプレイ可能なアーケード ゲームになります。ご存じかもしれませんが、プレイしたいときに実際にオフラインにする必要はありません。Chrome で about://dino に移動するか、about://network-error/-106 を参照するだけです。しかし、Chrome の恐竜ゲームが毎月 2 億 7,000 万回プレイされていることをご存じでしょうか。
もう一つ、知っておくと便利な事実があります。それは、アーケード モードではゲームパッドを使ってゲームをプレイできるということです。ゲームパッドのサポートは、この記事の執筆時点から約 1 年前に、Reilly Grant 氏によるcommitで追加されました。ご覧のとおり、このゲームは、Chromium プロジェクトの他の部分と同様に、完全にオープンソースです。この投稿では、Gamepad API の使用方法について説明します。
Gamepad API を使用する
機能の検出とブラウザのサポート
Gamepad API は、パソコンとモバイルの両方でブラウザ サポートが優れています。Gamepad API がサポートされているかどうかは、次のスニペットを使用して検出できます。
if ('getGamepads' in navigator) {
// The API is supported!
}
ブラウザがゲームパッドを表現する方法
ブラウザは、ゲームパッドを Gamepad オブジェクトとして表します。Gamepad には次の特性があります。
id: ゲームパッドの識別文字列。この文字列は、接続されているゲームパッド デバイスのブランドまたはスタイルを識別します。displayId: 関連付けられたVRDisplayのVRDisplay.displayId(該当する場合)。index: ナビゲーター内のゲームパッドのインデックス。connected: ゲームパッドがシステムに接続されているかどうかを示します。hand: コントローラが保持されている手、または保持されている可能性が最も高い手を定義する列挙型。timestamp: : このゲームパッドのデータが最後に更新された時刻。mapping: このデバイスで使用されているボタンと軸のマッピング("standard"または"xr-standard")。pose: WebVR コントローラに関連付けられたポーズ情報を表すGamepadPoseオブジェクト。axes: ゲームパッドのすべての軸の値の配列。-1.0~1.0の範囲に線形正規化されます。buttons: ゲームパッドのすべてのボタンのボタン状態の配列。
ボタンはデジタル(押されているか押されていないか)またはアナログ(例: 78% 押されている)にすることができます。このため、ボタンは次の属性を持つ GamepadButton オブジェクトとして報告されます。
pressed: ボタンの押下状態(ボタンが押されている場合はtrue、押されていない場合はfalse)。touched: ボタンのタップ状態。ボタンがタッチを検出できる場合、このプロパティは、ボタンがタッチされている場合はtrue、それ以外の場合はfalseです。value: アナログ センサーを搭載したボタンの場合、このプロパティはボタンが押された量を表し、0.0~1.0の範囲に線形正規化されます。hapticActuators:GamepadHapticActuatorオブジェクトを含む配列。各オブジェクトは、コントローラで使用可能な触覚フィードバック ハードウェアを表します。
ブラウザとゲームパッドによっては、vibrationActuator プロパティが追加されることがあります。次の 2 種類の振動効果が可能です。
- デュアル ランブル: ゲームパッドの各グリップに 1 つずつ、計 2 つの偏心回転質量アクチュエータによって生成される触覚フィードバック効果。
- トリガー振動: 2 つの独立したモーターによって生成される触覚フィードバック効果。1 つのモーターはゲームパッドの各トリガーに配置されています。
次の概略図は、仕様から直接引用したもので、一般的なゲームパッドのボタンと軸のマッピングと配置を示しています。
ゲームパッドが接続されたことを知らせる通知
ゲームパッドが接続されたことを検出するには、window オブジェクトでトリガーされる gamepadconnected イベントをリッスンします。ユーザーがゲームパッドを接続すると(USB または Bluetooth を使用)、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 つの項目で固定長です。接続されているゲームパッドが 0 個または 4 個未満の場合、アイテムは null になることがあります。常に配列のすべての項目を確認し、ゲームパッドがスロットを「記憶」していること、および最初に使用可能なスロットに常に存在するとは限らないことに注意してください。
// When no gamepads are connected:
navigator.getGamepads();
// (4) [null, null, null, null]
1 つ以上のゲームパッドが接続されているにもかかわらず、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.
}
デュアル振動
デュアル ランブルは、標準的なゲームパッドの各ハンドルに偏心回転質量振動モーターを搭載したハプティクス構成です。この構成では、どちらのモーターもゲームパッド全体を振動させることができます。2 つの質量は不均等であるため、それぞれの効果を組み合わせて、より複雑な触覚効果を生み出すことができます。デュアル振動効果は、次の 4 つのパラメータで定義されます。
duration: バイブレーション効果の持続時間をミリ秒単位で設定します。startDelay: 振動が開始されるまでの遅延時間を設定します。strongMagnitudeとweakMagnitude: 重い偏心回転質量モーターと軽い偏心回転質量モーターのバイブレーション強度レベルを、0.0~1.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,
});
};
振動をトリガー
トリガー振動は、2 つの独立したモーターによって生成される触覚フィードバック効果です。1 つのモーターはゲームパッドの各トリガーにあります。
// 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() を使用できず、gamepadconnected イベントと gamepaddisconnected イベントも発生しません。
<iframe src="index.html" allow="gamepad"></iframe>
デモ
次の例には、ゲームパッド テスターのデモが埋め込まれています。ソースコードは GitHub で入手できます。デモを試すには、USB または Bluetooth を使用してゲームパッドを接続し、いずれかのボタンを押すか、いずれかの軸を動かします。
おまけ: web.dev で Chrome Dino をプレイする
このサイトでは、ゲームパッドを使って Chrome Dino をプレイできます。ソースコードは GitHub にあります。trex-runner.js のゲームパッド ポーリングの実装を確認し、キーの押下がどのようにエミュレートされているかに注目してください。
Chrome Dino ゲームパッドのデモを動作させるために、私はコア Chromium プロジェクトから Chrome Dino ゲームを切り離し(Arnelle Ballane 氏による以前の取り組みを更新)、スタンドアロン サイトに配置しました。また、既存の Gamepad API の実装を拡張して、ダッキングとバイブレーションの効果を追加し、フルスクリーン モードを作成しました。さらに、Mehul Satardekar 氏がダークモードの実装に貢献してくれました。ゲームをお楽しみください。
役に立つリンク
謝辞
このドキュメントは、François Beaufort と Joe Medley によってレビューされました。Gamepad API 仕様は、Steve Agoston、James Hollyer、Matt Reynolds によって編集されています。以前の仕様編集者は、Brandon Jones、Scott Graham、Ted Mielczarek です。Gamepad Extensions の仕様は、Brandon Jones 氏によって編集されています。