Gamepad API でハードルを飛ばす

はじめに

初心者は、アドベンチャー ゲームにはキーボードを、フルーツをカットするには大切なマルチタッチの指先を、マイケル ジャクソンのように踊れるふりをするには、おしゃれな最新のモーション センサーを使いましょう。(ニュース速報: できません)。でも、あなたは違います。君の方が上手だ。プロなみです。ゲームパッドを手に取ってゲームを始め、ゲームを終える。

ですが、ちょっと待って。ウェブアプリでゲームパッドをサポートしたい場合は、運が悪いのでしょうか?アプリを開いても、新しい Gamepad API を使用すると、JavaScript を使用して、パソコンに接続されているゲームパッド コントローラの状態を読み取ることができます。この機能は、先週 Chrome 21 に搭載されたばかりの最新機能です。Firefox でもまもなくサポートされる予定です(現在は特別ビルドで利用可能)。

これは非常に良いタイミングでした。最近、2012 年のハードル走の Google Doodle でこの機能を使用する機会があったからです。この記事では、Gamepad API を Doodle に追加した方法と、そのプロセスで学んだことについて簡単に説明します。

ハードル 2012 Google Doodle
2012 年のハードル走の Google Doodle

ゲームパッド テスター

インタラクティブな Doodle は、一時的なものであるにもかかわらず、内部的にはかなり複雑なものになる傾向があります。説明をわかりやすくするために、Doodle からゲームパッドのコードを抽出し、シンプルなゲームパッド テスターを作成しました。このアプリを使用して、USB ゲームパッドが正しく動作するかどうかを確認できます。また、内部でどのように動作しているかも確認できます。

現在、どのブラウザが対応していますか?

Browser Support

  • Chrome: 21.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 10.1.

Source

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

一般的に、システムでネイティブにサポートされている最新のゲームパッドであれば、どれでも動作します。PC に接続したノーブランドの USB コントローラ、ドングル経由で Mac に接続した PlayStation 2 のゲームパッド、Chrome OS ノートパソコンとペア設定した Bluetooth コントローラなど、さまざまなゲームパッドをテストしました。

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

これは、Doodle のテストに使用したコントローラの写真です。コントローラが動作しない場合や、コントロールが正しくマッピングされていない場合は、Chrome または Firefox のバグを報告してください。(各ブラウザの最新バージョンでテストして、問題がまだ修正されていないことを確認してください)。

Gamepad API の機能検出<

Chrome の場合:

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

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

しかし、これは一時的なものだと確信しています。非常に優れた Modernizr は、すでに Gamepad API についての情報を提供しているため、現在および将来の検出ニーズすべてにこれを使用することをおすすめします。

var gamepadSupportAvailable = Modernizr.gamepads;

接続されているゲームパッドに関する情報を取得する

ゲームパッドを接続しても、ユーザーがボタンを押すまでは、ゲームパッドは認識されません。これはフィンガープリントを防止するためですが、ユーザー エクスペリエンスの観点からは少し課題があります。ユーザーがコントローラを接続したかどうかがわからないため、ユーザーにボタンを押すよう求めたり、ゲームパッド固有の手順を提供したりすることはできません。

しかし、そのハードルをクリアすると(すみません…)、さらに多くのハードルが待ち受けています。

ポーリング

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

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

これまでに実装された仕様の一部では、接続されたゲームパッドの状態を継続的に確認し(必要に応じて以前の状態と比較する)、状態が変化したときにイベントを発生させる必要があります。requestAnimationFrame() を使用して、最も効率的でバッテリーに優しい方法でポーリングを設定しました。この Doodle では、アニメーションをサポートする requestAnimationFrame() ループがすでにありましたが、完全に独立した 2 つ目のループを作成しました。この方がコーディングが簡単で、パフォーマンスに影響を与えることもありません。

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

/**
 * 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 つ以上のゲームパッドが接続されている、一部のゲームパッドが途中で切断されるなど)に対応するために、コードを数行追加する必要があります。この問題を解決する方法の 1 つとして、テスターのソースコード(関数 pollGamepads())をご覧ください。

イベント

Firefox では、Gamepad API 仕様に記載されている、より優れた代替方法を使用しています。ポーリングを求める代わりに、MozGamepadConnected と MozGamepadDisconnected という 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 つ(走るためのボタン 2 つとジャンプするためのボタン 1 つ)だけですが、ゲームパッドにはそれ以上のボタンがあるはずです。そこで、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 つのブラウザのみです。イベントを受信していない場合は、イベントを使用している可能性のある他のページを閉じてください。また、Google の経験上、タブを閉じたりブラウザ自体を終了したりしても、ブラウザがゲームパッドを「保持」していることがあります。システムを再起動することが、問題を解決する唯一の方法である場合があります。
  • これまでと同様に、Chrome Canary や他のブラウザの同等バージョンを使用して、最適なサポートが提供されていることを確認してください。古いバージョンで動作が異なる場合は、適切に対処してください。

今後について

このブログ投稿が、この新しい API(まだ少し不安定ですが、すでに非常に興味深いものとなっています)を理解するうえで少しでもお役に立てば幸いです。

API の欠落部分(イベントなど)や、より広範なブラウザ サポートに加えて、最終的にはランブル コントロールや内蔵ジャイロスコープへのアクセスなども実現したいと考えています。また、さまざまなタイプのゲームパッドのサポートも強化したいと考えています。正しく動作しない、またはまったく動作しないゲームパッドを見つけた場合は、Chrome に対してバグを報告するか、Firefox に対してバグを報告してください。

その前に、2012 年のハードル走の Doodle をゲームパッドでプレイして、どれほど楽しくなるか試してみてください。10.7 秒より速く走れるって言った?挑戦を待っています!

関連情報