Gamepad API でハードルを飛ばす

はじめに

初心者は、アドベンチャー ゲーム用のキーボード、フルーツのカット用のマルチタッチ指先、マイケル ジャクソンのように踊れるふりをする用の最新のモーション センサーを使いましょう。(残念ながら、できません)。お客様の場合は異なります。ありがとうございます。プロなみです。ゲームの開始と終了は、ゲームパッドを手に持つところから始まります。

でも、ウェブアプリでゲームパッドをサポートしたい場合は、どうすればよいですか?今はそうではありません。新しい Gamepad API を使用すると、JavaScript を使用して、コンピュータに接続されているゲームパッド コントローラの状態を読み取ることができます。この機能は先週 Chrome 21 に導入されたばかりで、Firefox でもまもなくサポートされる予定です(現在は特別ビルドで利用できます)。

タイミングが非常に良かったようで、先日公開された Hurdles 2012 Google doodle で使用できました。この記事では、Google が Gamepad API を Doodle に追加した方法と、その過程で得た知見について簡単に説明します。

2012 年 10 月 17 日の Google Doodle
Hurdles 2012 Google doodle

ゲームパッド テスター

エフェメラルであるにもかかわらず、インタラクティブな Doodle は内部的には非常に複雑な傾向があります。説明をわかりやすくするために、Google は、このドゥードルのゲームパッド コードを使用して、簡単なゲームパッド テスターを作成しました。このツールを使用すると、USB ゲームパッドが正しく動作しているかどうかを確認できます。また、内部を調べて動作を確認することもできます。

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

対応ブラウザ

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

ソース

使用できるゲームパッドはどれですか?

一般的に、システムでネイティブにサポートされている最新のゲームパッドはすべて動作します。テストでは、PC で使用するブランド外の USB コントローラから、ドングルを介して Mac に接続する PlayStation 2 ゲームパッド、ChromeOS ノートパソコンとペア設定した Bluetooth コントローラまで、さまざまなゲームパッドをテストしました。

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

以下は、この Doodle のテストに使用したコントローラの写真です。「ママ、仕事で本当にこんなことをしているんだよ」コントローラが機能しない、またはコントロールが正しくマッピングされていない場合は、Chrome または Firefox のバグを報告してください。(各ブラウザの最新バージョンでテストし、まだ修正されていないことを確認してください)。

" id="feature_detecting_the_gamepad_api" tabindex="-1">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() を使用して、最も効率的でバッテリーに優しい方法でポーリングを設定しました。ドゥードルでは、アニメーションをサポートする 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 つ以上のゲームパッドが接続されている、途中で一部のゲームパッドが切断されるなど)に対応するコードをさらに数行追加する必要があります。この問題を解決する方法の一つとして、テスターの関数 pollGamepads()ソースコードをご覧ください。

イベント

Firefox では、Gamepad API の仕様で説明されている、より優れた代替方法が使用されています。ポーリングを要求する代わりに、ゲームパッドが接続されたとき(正確には、接続されてボタンを押すことで「通知」されたとき)と接続が解除されたときに発生する 2 つのイベント(MozGamepadConnectedMozGamepadDisconnected)が公開されます。今後の状態を反映し続けるゲームパッド オブジェクトが、イベント オブジェクトの .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 に達しないこともあります)。ドゥードゥルでは、次のように行います。

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

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

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. ドゥードゥルには 3 つのボタン(走る用が 2 つ、ジャンプ用が 1 つ)しか必要ありませんが、ゲームパッドはもっと多くのボタンを備えている可能性があります。そのため、A/B ボタン、ショルダー ボタン、十字キーの左右、いずれかのスティックを左右に激しく動かすなど、ユーザーがさまざまな方法で走行できるように、16 個のボタンと 2 つのスティックを 3 つの論理機能にマッピングしました(もちろん、これらのうちいくつかは他の方法よりも効率的です)。次に例を示します。

    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. ゲームパッド入力を組み込むのではなく、ドゥードルにボルト止めするまでやりました。ポーリング ループは、必要な 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 に関するバグを報告してください。

ゲームパッドを使って、Hurdles 2012 の Doodle をプレイして、その楽しさをぜひお試しください。10.7 秒より短い時間でできるとおっしゃいましたか?挑戦を待っています!

関連情報