Gamepad APIの利用

これは実験段階の機能です。
この機能は複数のブラウザーで開発中の状態にあります。互換性テーブルをチェックしてください。また、実験段階の機能の構文と挙動は、仕様変更に伴い各ブラウザーの将来のバージョンで変更になる可能性があることに注意してください。

HTML5 はリッチでインタラクティブなゲームを開発するために必要なコンポーネントを多く搭載しています。 <canvas>や WebGL、<audio> に <video> などの技術は、今まで Native コードを書く必要のあった機能をサポートできるほどに成長しました。Gamepad API は開発者とデザイナーに Gamepad やコントローラーへのアクセスを提供するものです。

Gamepad APIWindow オブジェクトにGamepadとコントローラー(以下、Gamepad)の状態を読み取る新しいイベントをいくつか追加します。さらに、 Gamepad というGamepadの接続状態が得られるオブジェクトと navigator.getGamepads というGamepadの一覧を取得できるメソッドが追加されます。 

Gamepad の接続

新しい Gamepad が接続された時、アクティブなページは gamepadconnected イベントを受け取ります。ページ読み込み時にGamepadがすでに接続されている場合、Gamepad のボタンを押すなどの操作をした時に gamepadconnected イベントがアクティブなページに対して発生します。

Firefox では、 ページが見える状態でかつユーザーによる Gamepad の操作を受け付けたときにのみ、Gamepad が利用可能になります。これによって、ユーザーを特定する Fingerprinting に利用されることを防止しています。一度一つのコントローラーが操作されれば、他のコントローラーも自動で接続され利用可能になります。

以下のようにして gamepadconnected を使用します:

window.addEventListener("gamepadconnected", function(e) {
  console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.",
    e.gamepad.index, e.gamepad.id,
    e.gamepad.buttons.length, e.gamepad.axes.length);
});

Gamepad はそれぞれ固有の ID を gamepad プロパティの中に持っています。

Gamepad の切断

Gamepad が切断されると、Gamepad が以前に受信したデータ(例: gamepadconnected )があると、2番目のイベント(例: gamepadconnected )がフォーカスされたウィンドウにディスパッチします:

window.addEventListener("gamepaddisconnected", function(e) {
  console.log("Gamepad disconnected from index %d: %s",
    e.gamepad.index, e.gamepad.id);
});

Gamepadの index というプロパティは同じタイプの複数のコントローラーが使用されている場合であっても、システムに接続されたデバイスごとにユニークになります。 Index プロパティもまた Navigator.getGamepads() として戻される Array の index として機能します。

var gamepads = {};

function gamepadHandler(event, connecting) {
  var gamepad = event.gamepad;
  // Note:
  // gamepad === navigator.getGamepads()[gamepad.index]

  if (connecting) {
    gamepads[gamepad.index] = gamepad;
  } else {
    delete gamepads[gamepad.index];
  }
}

window.addEventListener("gamepadconnected", function(e) { gamepadHandler(e, true); }, false);
window.addEventListener("gamepaddisconnected", function(e) { gamepadHandler(e, false); }, false);

この前の例ではイベントが完了した後に gamepad プロパティがどのように保持できるかを示しています - 後でデバイスの状態照会のために使用する技術となります。

Gamepad オブジェクトの問い合わせ

ご覧のように、上述の Gamepad イベントは  Gamepad オブジェクトを返すイベントオブジェクト、上の  gamepad  のプロパティが含まれています。複数の Gamepad (すなわち、そのID ) を一度に接続される可能性があるため、イベントを発生させたのはどのゲームパッドを決定するためにこれらを使用することができます。それへの参照を保持し、それがボタンや軸のいずれかの時点で押されているかを知るために照会するなど、Gamepad オブジェクトから様々なことを行うことができます。そうすることで、多くの場合、今回と次回のイベント発生とゲームパッドの状態を知っておく必要があり、ゲームやその他のインタラクティブな Web ページであることが望ましいです。

このようなチェックを実行すると、開発者はゲームパッドゲームパッドの状態に基づいて、現在のフレームのための意思決定を行うために必要なアニメーションループ (例 : requestAnimationFrame) 一緒に Gamepad オブジェクト使用して関与する傾向があります

Navigator.getGamepads() メソッドは現在 Web ページから見える Gamepad オブジェクト ( Gamepad が繋がっていない時は毎回 null が返される ) のような、すべてのデバイスを配列として戻します。これは、同じ情報を得るために使用することができます例えば、 以下に示すように上記の最初のコード例書き換えます

window.addEventListener("gamepadconnected", function(e) {
  var gp = navigator.getGamepads()[e.gamepad.index];
  console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.",
    gp.index, gp.id,
    gp.buttons.length, gp.axes.length);
});

Gamepad オブジェクトの機能は以下です :

  • id: コントローラーに関する情報を含んだ文字列です。これは厳密には指定されていなく、Firefox では、コントローラのUSBベンダと製品IDを含んでいる2つの4桁16進数字列、およびドライバーから提供されるようなコントローラの名前といった3つの情報が、ダッシュ(-)により分離され含まれています。この情報はユーザーへの有益なフィードバックを表示するとともに、デバイスのコントロールのマッピングを見つけることができるようにします。
  • index: 現在システムに接続されているゲームパッドごとに固有の整数。この値は複数のコントローラを区別するために使用できます。デバイスを切断してから新しいデバイスを接続すると、以前のインデックスが再利用されることに注意してください。
  • mapping: ブラウザがデバイス上のコントロールを既知のレイアウトに再マップしたかどうかを示す文字列。現在、サポートされている既知のレイアウト - 標準のゲームパッドは1つしかありません。ブラウザがデバイス上のコントロールをそのレイアウトにマッピングできる場合、mappingプロパティは文字列standardに設定されます。
  • connected: ゲームパッドがシステムに接続されているかどうかを示すブール値。もし接続されている場合True接続されていない場合はFalseが設定されます。
  • buttons: デバイス上に存在するボタンを表すGamepadButtonオブジェクトの配列。GamepadButtonには、pressed のプロパティvalue のプロパティがあり9ます:
    • pressed プロパティは、ボタンが現在押されている( true )か押されていない( false )かを示すブール値です。
    • value プロパティは、現代の多くのゲームパッドのトリガなど、アナログボタンの表示を有効にするために使用される浮動小数点値です。値は0.0..1.0の範囲に正規化され、0.0は押されていないボタンを表し、1.0は完全に押されたボタンを表します。
  • axes: デバイス上に軸があるコントロールを表す配列 (例:アナログサムスティック)。 配列の各エントリは-1.0〜1.0の範囲の浮動小数点値で、最小値 (-1.0) から最大値 (1.0) までの軸の位置を表します。
  • timestamp: このゲームパッドのデータが最後に更新された時刻を表すDOMHighResTimeStampを返します。この値により、開発者は axes と button のデータがハードウェアから更新されたかどうかを判断できます。 値は、PerformanceTimingインターフェースの navigationStart 属性との相対値でなければなりません。 値は単調に増加します。つまり、更新の順序を決定するために比較することができます。新しい値は常に古い値よりも大きいか等しいためです。このプロパティは現在 Firefox ではサポートされていません。

注記: Gamepadオブジェクトは、セキュリティ上の理由からWindowオブジェクトではなくgamepadconnectedイベントで使用できます。一度リファレンスを取得すると、そのプロパティでゲームパッドの現在の状態に関する情報を照会できます。 このオブジェクトは、ゲームパッドの状態が変わるたびに更新されます。

ボタン情報の使用

1つのゲームパッドの接続情報を表示する簡単な例を見てみましょう (後続のゲームパッド接続を無視します)。ゲームパッドの右側にある4つのゲームパッドボタンを使用してボールを画面の周りに移動できます。デモをライブで見ることができ、Githubでソースコードを見つけることができます

まず、いくつかの変数を宣言します: 接続情報が書き込まれる gamepadInfo のパラグラフ、移動する ballrequestAnimation Frame の ID として機能する start 変数、ボールを移動するための位置変更子として機能する a および b 変数、および短縮形変数 これは、requestAnimationFrame() and cancelAnimationFrame()およびcancelAnimationFrame()クロスブラウザフォークで使用されます。

var gamepadInfo = document.getElementById("gamepad-info");
var ball = document.getElementById("ball");
var start;
var a = 0;
var b = 0;

次にgamepadconnectedイベントを使用して、接続されているゲームパッドを確認します。接続されるとNavigator.getGamepads()[0]を使用してゲームパッドを取得し、ゲームパッドに関する情報をゲームパッドの情報 div に出力し、全体のボールの動きが始まる gameLoop() 関数が呼び出します

window.addEventListener("gamepadconnected", function(e) {
  var gp = navigator.getGamepads()[e.gamepad.index];
  gamepadInfo.innerHTML = "Gamepad connected at index " + gp.index + ": " + gp.id + ". It has " + gp.buttons.length + " buttons and " + gp.axes.length + " axes.";

  gameLoop();
});

これでgamepaddisconnectedイベントを使用して、ゲームパッドが再び切断されたかどうかを確認します。 もしそうならば、requestAnimationFrame()ループ (下記参照) を停止し、ゲームパッドの情報を元の状態に戻します。

window.addEventListener("gamepaddisconnected", function(e) {
  gamepadInfo.innerHTML = "Waiting for gamepad.";

  cancelRequestAnimationFrame(start);
});

Chromeでは異なる挙動になります。変数にゲームパッドの最新の状態を常に保存するのではなく、スナップショットを保存するだけなので、Chrome で同じことを行うにはポーリングしてからGamepadオブジェクトをコードで使用する必要があり、それは利用可能です。私たちはこれをWindow.setInterval()オブジェクトが利用可能になると、ゲームパッド情報が出力され、ゲームループが開始され、Window.clearInterval()を使用して間隔がクリアされます。 Chrome Navigator.getGamepads()の古いバージョンでは、Webkit プレフィックスを使用して実装されています。下位互換性のために、接頭辞付きのバージョンと関数の標準バージョンの両方を検出して処理しようとします。

var interval;

if (!('ongamepadconnected' in window)) {
  // No gamepad events available, poll instead.
  interval = setInterval(pollGamepads, 500);
}

function pollGamepads() {
  var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : []);
  for (var i = 0; i < gamepads.length; i++) {
    var gp = gamepads[i];
    if (gp) {
      gamepadInfo.innerHTML = "Gamepad connected at index " + gp.index + ": " + gp.id +
        ". It has " + gp.buttons.length + " buttons and " + gp.axes.length + " axes.";
      gameLoop();
      clearInterval(interval);
    }
  }
}

今度はメインのゲームループです。ループが実行されるたびに、4つのボタンの1つが押されているかどうかがチェックされます。そうするとab の移動変数の値を適切に更新し、lefttopのプロパティを更新し、その値を a および b とする。これはボールを画面の周りに動かす効果があります。 現在のバージョンの Chrome (この記事の執筆時点ではバージョン34) では、ボタンの値はGamepadButtonオブジェクトではなく、double値の配列として保存されます。

この作業がすべて完了したら、requestAnimationFrame() を使用して gameLoop() を再び実行して次のアニメーションフレームを要求します。

function buttonPressed(b) {
  if (typeof(b) == "object") {
    return b.pressed;
  }
  return b == 1.0;
}

function gameLoop() {
  var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : []);
  if (!gamepads) {
    return;
  }

  var gp = gamepads[0];
  if (buttonPressed(gp.buttons[0])) {
    b--;
  } else if (buttonPressed(gp.buttons[2])) {
    b++;
  }
  if (buttonPressed(gp.buttons[1])) {
    a++;
  } else if (buttonPressed(gp.buttons[3])) {
    a--;
  }

  ball.style.left = a * 2 + "px";
  ball.style.top = b * 2 + "px";

  start = requestAnimationFrame(gameLoop);
}

軸情報の使用

TBD (FirefoxとChromeの両方で button[i].value ではなく axes[i] を使う以外は基本的に同じです。)

完全な例: ゲームパッドの状態を表示する

この例では、Gamepadオブジェクト、gamepadconnectedイベント、gamepaddisconnectedイベントを使用してシステムに接続されているすべてのゲームパッドの状態を表示します。デモを見て、Githubの完全なソースコードを見ることができます。

var haveEvents = 'ongamepadconnected' in window;
var controllers = {};

function connecthandler(e) {
  addgamepad(e.gamepad);
}

function addgamepad(gamepad) {
  controllers[gamepad.index] = gamepad;

  var d = document.createElement("div");
  d.setAttribute("id", "controller" + gamepad.index);

  var t = document.createElement("h1");
  t.appendChild(document.createTextNode("gamepad: " + gamepad.id));
  d.appendChild(t);

  var b = document.createElement("div");
  b.className = "buttons";
  for (var i = 0; i < gamepad.buttons.length; i++) {
    var e = document.createElement("span");
    e.className = "button";
    //e.id = "b" + i;
    e.innerHTML = i;
    b.appendChild(e);
  }

  d.appendChild(b);

  var a = document.createElement("div");
  a.className = "axes";

  for (var i = 0; i < gamepad.axes.length; i++) {
    var p = document.createElement("progress");
    p.className = "axis";
    //p.id = "a" + i;
    p.setAttribute("max", "2");
    p.setAttribute("value", "1");
    p.innerHTML = i;
    a.appendChild(p);
  }

  d.appendChild(a);

  // See https://github.com/luser/gamepadtest/blob/master/index.html
  var start = document.getElementById("start");
  if (start) {
    start.style.display = "none";
  }

  document.body.appendChild(d);
  requestAnimationFrame(updateStatus);
}

function disconnecthandler(e) {
  removegamepad(e.gamepad);
}

function removegamepad(gamepad) {
  var d = document.getElementById("controller" + gamepad.index);
  document.body.removeChild(d);
  delete controllers[gamepad.index];
}

function updateStatus() {
  if (!haveEvents) {
    scangamepads();
  }

  var i = 0;
  var j;

  for (j in controllers) {
    var controller = controllers[j];
    var d = document.getElementById("controller" + j);
    var buttons = d.getElementsByClassName("button");

    for (i = 0; i < controller.buttons.length; i++) {
      var b = buttons[i];
      var val = controller.buttons[i];
      var pressed = val == 1.0;
      if (typeof(val) == "object") {
        pressed = val.pressed;
        val = val.value;
      }

      var pct = Math.round(val * 100) + "%";
      b.style.backgroundSize = pct + " " + pct;

      if (pressed) {
        b.className = "button pressed";
      } else {
        b.className = "button";
      }
    }

    var axes = d.getElementsByClassName("axis");
    for (i = 0; i < controller.axes.length; i++) {
      var a = axes[i];
      a.innerHTML = i + ": " + controller.axes[i].toFixed(4);
      a.setAttribute("value", controller.axes[i] + 1);
    }
  }

  requestAnimationFrame(updateStatus);
}

function scangamepads() {
  var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);
  for (var i = 0; i < gamepads.length; i++) {
    if (gamepads[i]) {
      if (gamepads[i].index in controllers) {
        controllers[gamepads[i].index] = gamepads[i];
      } else {
        addgamepad(gamepads[i]);
      }
    }
  }
}


window.addEventListener("gamepadconnected", connecthandler);
window.addEventListener("gamepaddisconnected", disconnecthandler);

if (!haveEvents) {
  setInterval(scangamepads, 500);
}

仕様

仕様 状態 コメント
Gamepad
Gamepad の定義
草案 Initial defintion

ブラウザの互換性

Feature Chrome Firefox (Gecko) Edge Internet Explorer Opera Safari (WebKit)
General support 21.0 webkit
35.0
29.0 (29.0) [1] (Yes) 未サポート 15.0 webkit
22.0
未サポート
Feature Android Firefox Mobile (Gecko) IE Phone Opera Mobile Safari Mobile Samsung Internet
General support 未サポート 32.0 (32.0) 未サポート 未サポート 未サポート (Yes)

[1] Firefox 24 以降、Gamepad API は preference を通して使用可能でした。これらのバージョンでは about:config を読み込み、dom.gamepad.enabled の設定を true に設定します。

ドキュメントのタグと貢献者

 このページの貢献者: T.Ukegawa, mzyy94
 最終更新者: T.Ukegawa,