WebブラウザのGamepadAPIをつかってみる

前々から気になっていたのですが、最近あんまりネットで効かなくなったGamepadAPIを試してみます。

【公式情報】 w3c.github.io

公式のW3Cな情報でもいいのですが、Mozillaのサイトのほうが情報がわかりやすいので以下を参照しながらコーディングをしてみようと思います。

【参考】

developer.mozilla.org

参考をみるとこの様に書かれています。

Gamepad API は開発者とデザイナーに Gamepad やコントローラーへのアクセスを提供するものです。Gamepad API は Window オブジェクトにGamepadとコントローラー(以下、Gamepad)の状態を読み取る新しいイベントをいくつか追加します。

ではこれを見ながら最小限のコーディングをしていきます。

Webに公開できるようにGithubにUPしてみました。ここでもソースコードおいてみますのでご利用ください。

github.com

ソースコード

【index.html】

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">

<style>
.buttons, .axes {
  padding: 1em;
}

.axis {
  min-width: 400px;
  margin: 1em;
}

.button {
  padding: 1em;
  border: 2px solid black;
  background-color: rgb(255, 255, 128);
}

.pressed {
  border: 2px solid black;
  background-color: red;
}
</style>
</head>
<body>
<h2 id="start">Gamepadを接続してなにかボタンを押すとスタートします。</h2>
<script type="text/javascript" src="gamepad.js"></script>
</body>
</html>

【gamepad.js】

var haveEvents = 'GamepadEvent' in window;
var controllers = {};
var rAF = window.requestAnimationFrame;

function connectHandler(e) {
    addGamepad(e.gamepad);
}
function addGamepad(gamepad) {
    // gamepadのArrayを作成
    controllers[gamepad.index] = gamepad;
    // HTMLへ接続されたGamepad毎の要素を追加(複数のgamepadにも対応)
    var d = document.createElement("div");
    d.setAttribute("id", "controller" + gamepad.index);//idはpadの番号がついた形式
    var t = document.createElement("h2");
    t.appendChild(document.createTextNode("接続Gamepad情報: "));
    d.appendChild(t);
    var info = document.createElement("h1");
    info.appendChild(document.createTextNode(gamepad.id));
    d.appendChild(info);

    //Gamepadコントロール要素(ボタンなど)表示部分
    var b = document.createElement("div");
    b.className = "buttons";
    var t = document.createElement("h2");
    t.appendChild(document.createTextNode("ボタンコントロール情報: "));
    b.appendChild(t);
    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);

    //Gamepadコントロール要素(アナログジョイなど)表示部分
    var a = document.createElement("div");
    a.className = "axes";
    var t = document.createElement("h2");
    t.appendChild(document.createTextNode("アナログコントロール情報: "));
    a.appendChild(t);
    for (i = 0; i < gamepad.axes.length; i++) {
        c = document.createElement("h3");
        c.appendChild(document.createTextNode("axis" + i));
        a.appendChild(c);
        e = document.createElement("meter");
        e.className = "axis";
        //e.id = "a" + i;
        e.setAttribute("min", "-1");
        e.setAttribute("max", "1");
        e.setAttribute("value", "0");
        e.innerHTML = i;
        a.appendChild(e);
    }
    d.appendChild(a);
    document.getElementById("start").style.display = "none";
    document.body.appendChild(d);
    rAF(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() {
    scanGamepads();
    for (j in controllers) {
        var controller = controllers[j];
        var d = document.getElementById("controller" + j);
        var buttons = d.getElementsByClassName("button");

        //ボタン情報の状態取得
        for (var 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 (var 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]);
        }
    }
    rAF(updateStatus);
}

function scanGamepads() {
    var gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
    for (var i = 0; i < gamepads.length; i++) {
        if (gamepads[i]) {
            if (!(gamepads[i].index in controllers)) {
                addGamepad(gamepads[i]);
                console.log("a");
            } else {
                controllers[gamepads[i].index] = gamepads[i];
                //console.log("b");
            }
        }
    }
}

if (haveEvents) {
    window.addEventListener("gamepadconnected", connectHandler);
    window.addEventListener("gamepaddisconnected", disconnectHandler);
} else {
    setInterval(scanGamepads, 500);
}

実行させてみる

GithubリポジトリをWeb公開状態に以下においてありますのでそのまま動作させてみてください。

https://ueponx.github.io/gamepadapi/

2つのファイル(index.htmlgamepad.js)をローカルPCの同じディレクトリにおいても同じ様に使用できます。

index.htmlの開くと以下のように表示されますので、

f:id:ueponx:20190330221428p:plain

Gamepadを接続するか、Gamepadのいずれかのボタンを押すと表示が変更されます。

f:id:ueponx:20190330223116p:plain

今回接続したのは以下のUSB接続をゲームパッドを接続していますが、ちゃんと認識してくれているようです。

buffalo.jp

ボタンは物理的には10あるのですが、認識されているのは連射機能とそのOFFを除く8つのボタンでした。 また、方向ボタン(十字ボタン)はアナログスティックとして認識されているようです。ただ動かすとわかるのですが、デジタル的な動きをしているようです。

また複数のゲームパッドを接続しても認識してくれます。

f:id:ueponx:20190330222810p:plain

2つ目に接続したのはXBOX Oneの無線コントローラになります。

詳細は以下の通りです。

f:id:ueponx:20190330222636p:plain

おわりに

Gamepad APIを使ってみました。まだDraft状態のAPIではありますが、現行のモダンブラウザであれば比較的対応しているのでわりといい感じです。 ネットの情報ではブラウザ毎の挙動が微妙に違っているという話もありますが…。そのうち正式な対応されるでしょう。

各ブラウザの対応状況

対応状況をみると、IEは兎も角としてSafari

あと、パッドのおすすめはXBOXなコントローラーをデフォルト扱いとして開発されているようなので、今後コントローラーを購入するのであればXBOX用のものを おすすめします。

できれば、現在あるWebアプリなどにJoy2Keyみたいな感じでこの機能を簡単でアタッチする方法とかがあるといいなあとか思いました。 なにかつかえますかねえ。IotデバイスでWebインターフェースを作るようなパターンであれば行けそうな気もしますが。

今回やってて痛感しましたが、Javascriptスクリプト書くのは問題ないんですけど、HTMLを書くのがもう面倒で厳しいかも。JQueryとかBootstrapにシフトして行きたいです…その他のフレームワークに行ってもいいのかも。