Gamepad API でハードルを飛ばす

はじめに

初心者にも、アドベンチャー ゲームではキーボードを、フルーツ カットには大切な指先を触ってもらい、マイケル ジャクソンのように踊れるふりをする斬新なモーション センサーを使いましょう。(Newsflash: できません)。でもあなたは違います。あなたは上手です。プロなみです。ゲームは、ゲームパッドを手にした状態で始まり、終わります。

待って。ウェブアプリでゲームパッドをサポートすることは不可能ではありませんか?今はそうではありません。そこで役立つのが新しい Gamepad API です。この API を使用すると、パソコンに接続されたゲームパッド コントローラの状態を JavaScript で読み取ることができます。リリースされたばかりで、先週 Chrome 21 でしか公開されていません。また、Firefox でのサポートも間近に迫っています(現在は特別なビルドで利用可能です)。

最近 Hurdles 2012 Google Doodle で取り上げる機会があったので、とても良い機会でした。この記事では、Doodle に Gamepad API を追加した方法と、その過程で学んだことを簡単に説明します。

ハードル 2012 Google Doodle
2012 年のハードルに関する Google Doodle

ゲームパッド テスター

インタラクティブな Doodle は一時的なものですが、内部ではかなり複雑になる傾向があります。デモをわかりやすくするため、Doodle からゲームパッドのコードを選び、シンプルなゲームパッド テスターを作成しました。このツールを使用して、USB ゲームパッドが正しく動作するかどうかを調べたり、内部で動作を調べたりすることもできます。

現在、どのブラウザでサポートされていますか?

対応ブラウザ

  • 21
  • 12
  • 29
  • 10.1

ソース

どのゲームパッドを使用できますか?

通常は、システムがネイティブにサポートしている最新のゲームパッドであれば正常に動作します。テストには、他社製の PC の USB コントローラから、ドングルで Mac に接続された PlayStation 2 のゲームパッドから、ChromeOS のノートブックとペアリングした Bluetooth コントローラに至るまで、さまざまなゲームパッドをテストしました。

ゲームパッド
ゲームパッド

これは、Google が Doodle のテストに使用したコントローラの写真です。「ええ、お母さん、私の仕事でもそうしています」コントローラが動作しない場合や、コントロールが正しくマッピングされていない場合は、Chrome または Firefox に対してバグを報告してください。(まだ修正されていないことを確認するため、各ブラウザの最新のバージョンでテストしてください)。

" id="feature_detecting_the_gamepad_api" tabindex="-1">Gamepad API の機能を検出する

Chrome で簡単:

var gamepadSupportAvailable = !!navigator.webkitGetGamepads || !!navigator.webkitGamepads;

Firefox では、今のところこれを検出できないようです。すべてがイベントベースであり、すべてのイベント ハンドラをウィンドウにアタッチする必要があるため、イベント ハンドラを検出する一般的な手法が機能しません。

これは一時的なものです。Gamepad API については、非常に優れた Modernizr ですでに説明済みですので、現在および将来の検出のニーズに応えるのがおすすめです。

var gamepadSupportAvailable = Modernizr.gamepads;

接続されているゲームパッドの確認

ゲームパッドを接続しても、ユーザーが先にいずれかのボタンを押さない限り、ゲームパッド自体は現れません。これはフィンガープリントを防ぐためのものですが、ユーザー エクスペリエンスにとって少々困難です。ユーザーがコントローラを接続したかどうかがわからないため、ユーザーにボタンを押すよう求めたり、ゲームパッド固有の手順を提供したりすることはできません。

しかし、そのハードルをクリアした後は、さらなるチャンスが待っています。

ポーリング

Chrome の API の実装では、navigator.webkitGetGamepads() という関数が公開されます。この関数を使用すると、現在システムに接続されているすべてのゲームパッドのリストと、現在の状態(ボタンとスティック)のリストを取得できます。最初に接続されたゲームパッドは、配列の最初のエントリとして返されます。

(この関数呼び出しは最近、直接アクセスできる配列 navigator.webkitGamepads[] に置き換えられました。2012 年 8 月初旬の時点では、Chrome 21 では引き続きこの配列にアクセスする必要がありますが、Chrome 22 以降では関数呼び出しが機能します。今後は、関数呼び出しが API の使用に推奨される方法で、インストールされているすべての Chrome ブラウザに徐々に適用されるようになります)。

これまで実装されていたこの仕様では、状況が変更されたときにイベントを発生させるのではなく、接続されているゲームパッドの状態を継続的にチェックし、必要に応じて以前のゲームパッドと比較する必要があります。requestAnimationFrame() を使用して、最も効率的かつバッテリーに優しい方法でポーリングをセットアップしました。この Doodle では、アニメーションをサポートする requestAnimationFrame() ループをすでに作成していますが、まったく別のループも作成しました。これはコーディングが簡単で、パフォーマンスに影響が及ぶことはありません。

テスターからのコードは次のとおりです。

/**
 * Starts a polling loop to check for gamepad state.
 */
startPolling: function() {
    // Don't accidentally start a second loop, man.
    if (!gamepadSupport.ticking) {
    gamepadSupport.ticking = true;
    gamepadSupport.tick();
    }
},

/**
 * Stops a polling loop by setting a flag which will prevent the next
 * requestAnimationFrame() from being scheduled.
 */
stopPolling: function() {
    gamepadSupport.ticking = false;
},

/**
 * A function called with each requestAnimationFrame(). Polls the gamepad
 * status and schedules another poll.
 */
tick: function() {
    gamepadSupport.pollStatus();
    gamepadSupport.scheduleNextTick();
},

scheduleNextTick: function() {
    // Only schedule the next frame if we haven't decided to stop via
    // stopPolling() before.
    if (gamepadSupport.ticking) {
    if (window.requestAnimationFrame) {
        window.requestAnimationFrame(gamepadSupport.tick);
    } else if (window.mozRequestAnimationFrame) {
        window.mozRequestAnimationFrame(gamepadSupport.tick);
    } else if (window.webkitRequestAnimationFrame) {
        window.webkitRequestAnimationFrame(gamepadSupport.tick);
    }
    // Note lack of setTimeout since all the browsers that support
    // Gamepad API are already supporting requestAnimationFrame().
    }
},

/**
 * Checks for the gamepad status. Monitors the necessary data and notices
 * the differences from previous state (buttons for Chrome/Firefox,
 * new connects/disconnects for Chrome). If differences are noticed, asks
 * to update the display accordingly. Should run as close to 60 frames per
 * second as possible.
 */
pollStatus: function() {
    // (Code goes here.)
},

必要なゲームパッドが 1 つのみの場合は、次のようにしてデータを簡単に取得できます。

var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];

もう少し巧みにしたい場合や、同時に複数のプレーヤーをサポートしたい場合は、より複雑なシナリオ(2 つ以上のゲームパッドが接続されている、一部が途中で切断されるなど)に対応するために数行のコードを追加する必要があります。これを解決する方法の一つは、テスターである関数 pollGamepads()ソースコードで確認できます。

イベント

Firefox は、Gamepad API の仕様に記載されている別の適切な方法を使用します。ポーリングを要求するのではなく、MozGamepadConnectedMozGamepadDisconnected という 2 つのイベントを公開します。これらのイベントは、ゲームパッドが接続されたとき(より正確には、ゲームパッドが接続されたとき、または電源から外すことで接続され、「通知」される)たびに発生します。将来の状態を継続的に反映するゲームパッド オブジェクトは、イベント オブジェクトの .gamepad パラメータとして渡されます。

テスターのソースコードから:

/**
 * React to the gamepad being connected. Today, this will only be executed
 * on Firefox.
 */
onGamepadConnect: function(event) {
    // Add the new gamepad on the list of gamepads to look after.
    gamepadSupport.gamepads.push(event.gamepad);

    // Start the polling loop to monitor button changes.
    gamepadSupport.startPolling();

    // Ask the tester to update the screen to show more gamepads.
    tester.updateGamepads(gamepadSupport.gamepads);
},

まとめ

最終的に、両方のアプローチをサポートするテスターの初期化関数は次のようになります。

/**
 * Initialize support for Gamepad API.
 */
init: function() {
    // As of writing, it seems impossible to detect Gamepad API support
    // in Firefox, hence we need to hardcode it in the third clause.
    // (The preceding two clauses are for Chrome.)
    var gamepadSupportAvailable = !!navigator.webkitGetGamepads ||
        !!navigator.webkitGamepads ||
        (navigator.userAgent.indexOf('Firefox/') != -1);

    if (!gamepadSupportAvailable) {
    // It doesn't seem Gamepad API is available – show a message telling
    // the visitor about it.
    tester.showNotSupported();
    } else {
    // Firefox supports the connect/disconnect event, so we attach event
    // handlers to those.
    window.addEventListener('MozGamepadConnected',
                            gamepadSupport.onGamepadConnect, false);
    window.addEventListener('MozGamepadDisconnected',
                            gamepadSupport.onGamepadDisconnect, false);

    // Since Chrome only supports polling, we initiate polling loop straight
    // away. For Firefox, we will only do it if we get a connect event.
    if (!!navigator.webkitGamepads || !!navigator.webkitGetGamepads) {
        gamepadSupport.startPolling();
    }
    }
},

ゲームパッド情報

システムに接続されているゲームパッドは、それぞれ次のようなオブジェクトで表されます。

id: "PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)"
index: 1
timestamp: 18395424738498
buttons: Array[8]
    0: 0
    1: 0
    2: 1
    3: 0
    4: 0
    5: 0
    6: 0.03291
    7: 0
axes: Array[4]
    0: -0.01176
    1: 0.01961
    2: -0.00392
    3: -0.01176

基本情報

上部のいくつかのフィールドは単純なメタデータです。

  • id: ゲームパッドのテキスト形式の説明
  • index: 1 台のコンピュータに接続されているゲームパッドを区別するのに役立つ整数
  • timestamp: ボタンまたは軸の状態が最後に更新されたタイムスタンプ(現時点では Chrome でのみサポートされています)

ボタンとスティック

今日のゲームパッドは、おじいちゃんが間違った城にプリンセスを救うために使用したかもしれないものとは異なります。通常、2 本のアナログ スティックに加えて、少なくとも 16 個の個別のボタン(個別のボタン、アナログボタン)があります。Gamepad API は、オペレーティング システムから報告されたすべてのボタンとアナログ スティックについて通知します。

ゲームパッド オブジェクトで現在の状態を取得したら、.buttons[] でボタンにアクセスし、.axes[] 配列で固定できます。それぞれが何に対応するのかを図でまとめます。

ゲームパッドの図
ゲームパッドの図

この仕様では、ブラウザに対して、最初の 16 個のボタンと 4 つの軸を以下にマッピングするよう指示しています。

gamepad.BUTTONS = {
    FACE_1: 0, // Face (main) buttons
    FACE_2: 1,
    FACE_3: 2,
    FACE_4: 3,
    LEFT_SHOULDER: 4, // Top shoulder buttons
    RIGHT_SHOULDER: 5,
    LEFT_SHOULDER_BOTTOM: 6, // Bottom shoulder buttons
    RIGHT_SHOULDER_BOTTOM: 7,
    SELECT: 8,
    START: 9,
    LEFT_ANALOGUE_STICK: 10, // Analogue sticks (if depressible)
    RIGHT_ANALOGUE_STICK: 11,
    PAD_TOP: 12, // Directional (discrete) pad
    PAD_BOTTOM: 13,
    PAD_LEFT: 14,
    PAD_RIGHT: 15
};

gamepad.AXES = {
    LEFT_ANALOGUE_HOR: 0,
    LEFT_ANALOGUE_VERT: 1,
    RIGHT_ANALOGUE_HOR: 2,
    RIGHT_ANALOGUE_VERT: 3
};

その他のボタンと軸は、上記のボタンと軸に追加されます。ただし、16 個のボタンと 4 つの軸は保証されません。一部は単に未定義のままです。

ボタンは 0.0(押していない)~ 1.0(完全に押す)の範囲で指定できます。軸は、-1.0(完全に左または上)から 0.0(中央)、1.0(完全に右または下)の範囲になります。

アナログですか、それともディスクリートですか?

表面的には、すべてのボタンをアナログのボタンにすることもできますが、これはショルダー ボタンなどでやや一般的です。そのため、単純に 1.00 と比較するのではなく、しきい値を設定することをおすすめします(アナログのボタンがたまたま少し汚れている場合はどうでしょうか?1.00 に届かないこともあります)。Doodle では次のようにしています。

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

gamepad.buttonPressed_ = function(pad, buttonId) {
    return pad.buttons[buttonId] &&
            (pad.buttons[buttonId] > gamepad.ANALOGUE_BUTTON_THRESHOLD);
};

同様に、アナログ スティックをデジタル ジョイスティックに変えることもできます。デジタルパッド(D-pad)は常にありますが、お使いのゲームパッドにない場合があります。これを処理するためのコードは次のようになります。

gamepad.AXIS_THRESHOLD = .75;

gamepad.stickMoved_ = function(pad, axisId, negativeDirection) {
    if (typeof pad.axes[axisId] == 'undefined') {
    return false;
    } else if (negativeDirection) {
    return pad.axes[axisId] < -gamepad.AXIS_THRESHOLD;
    } else {
    return pad.axes[axisId] > gamepad.AXIS_THRESHOLD;
    }
};

ボタンの押下とスティックの動作

イベント

フライト シミュレータ ゲームのように、スティックの位置やボタンの押下を継続的にチェックして反応するほうが理にかなっている場合もありますが、ハードル 2012 の Doodle などでは、なぜ、フレームごとにボタンを確認する必要があるのか、疑問に思われるかもしれません。キーボードやマウスの上下移動のようなイベントを取得できないのはなぜですか?

ご安心ください。残念なことに。仕様には含まれていますが、どのブラウザにも実装されていません。

ポーリング

それまでは、現在と以前の状態を比較し、違いがあれば関数を呼び出すことをおすすめします。次に例を示します。

if (buttonPressed(pad, 0) != buttonPressed(oldPad, 0)) {
    buttonEvent(0, buttonPressed(pad, 0) ? 'down' : 'up');
}
for (var i in gamepadSupport.gamepads) {
    var gamepad = gamepadSupport.gamepads[i];

    // Don't do anything if the current timestamp is the same as previous
    // one, which means that the state of the gamepad hasn't changed.
    // This is only supported by Chrome right now, so the first check
    // makes sure we're not doing anything if the timestamps are empty
    // or undefined.
    if (gamepadSupport.prevTimestamps[i] &&
        (gamepad.timestamp == gamepadSupport.prevTimestamps[i])) {
    continue;
    }
    gamepadSupport.prevTimestamps[i] = gamepad.timestamp;

    gamepadSupport.updateDisplay(i);
}

ハードル 2012 Doodle でキーボードを使うアプローチ

ゲームパッドがなく、今日の Doodle で好まれる入力方法はキーボードであるため、ゲームパッドでも厳密にエミュレートすることにしました。具体的には、次の 3 つの決定を行いました。

  1. Doodle に必要なボタンは 3 つ(ランニング用とジャンプ用)のみですが、ゲームパッドにはさらに多くのボタンがあります。そこで、既知の 16 個のボタンと既知の 2 本のスティックを、最も理にかなった方法でこれら 3 つの論理機能にマッピングしました。これにより、A/B ボタンを交互にしたり、ショルダーボタンを交互にしたり、D-pad を左右に押したり、左右に激しく揺れる(もちろん、一部は他のものより効率的です)。次に例を示します。

    newState[gamepad.STATES.LEFT] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.PAD_LEFT) ||
        gamepad.stickMoved_(pad, gamepad.AXES.LEFT_ANALOGUE_HOR, true) ||
        gamepad.stickMoved_(pad, gamepad.AXES.RIGHT_ANALOGUE_HOR, true),
    
    newState[gamepad.STATES.PRIMARY_BUTTON] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.FACE_1) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER_BOTTOM) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.SELECT) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.START) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_ANALOGUE_STICK),
    
  2. 前述のしきい値関数を使用して、各アナログ入力を離散入力として扱いました。

  3. ゲームパッドの入力は、Doodle に組み込むのではなく、Doodle に付け加えています。ポーリング ループは、実際に必要な keydown イベントと keyup イベントを(適切な keyCode を使用して)合成し、それを DOM に送り返します。

    // Create and dispatch a corresponding key event.
    var event = document.createEvent('Event');
    var eventName = down ? 'keydown' : 'keyup';
    event.initEvent(eventName, true, true);
    event.keyCode = gamepad.stateToKeyCodeMap_[state];
    gamepad.containerElement_.dispatchEvent(event);

これですべての設定が完了し、

ヒントとコツ

  • ゲームパッドは、ボタンが押されるまでブラウザにはまったく表示されません。
  • 異なるブラウザで同時にゲームパッドをテストする場合、そのうちの 1 つのみがコントローラを感知します。イベントを受信しない場合は、そのイベントを使用している可能性のある他のページを閉じてください。さらに、これまでの経験から、タブを閉じたりブラウザ自体を終了したりしても、ブラウザがゲームパッドを「長押し」することがあります。問題を解決する唯一の方法は、システムを再起動することです。
  • Chrome Canary など他のブラウザ用の同等のバージョンを使用して、最適なサポートが受けられることをご確認ください。また、古いバージョンで動作が異なる場合は適切な対応を行ってください。

今後の計画

この情報が、この新しい API の理解に役立つことを願っています。この API はまだ不安定ですが、楽しさが増しています。

API の欠けている部分(イベントなど)や幅広いブラウザ サポートに加え、将来的にはランブル コントロール、内蔵ジャイロスコープへのアクセスなども実現したいと考えています。また、さまざまなタイプのゲームパッドのサポートも追加する予定です。正しく機能しない、またはまったく機能しないバグを見つけた場合は、Chrome にバグを報告したり、Firefox にバグを報告したりしてください。

その前に、Hurdles 2012 Doodle でゲームパッドの楽しさをチェックしてみましょう。10.7 秒よりうまくできるって言ったの?持ってきて。

関連情報