12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Web] JavaScriptからゲームパッドの入力を受け付ける方法

12
Last updated at Posted at 2025-07-02

モチベーション

自分は趣味でJavascriptを使ってレトロゲームを作っていました。Webブラウザでゲームパッドが使えるのは以前から知っていましたが、今回、本格的に自作ゲームに対応しようと思ったわけです。

実際にやってみると、そのままだと動作がイマイチなところがあり、ちょっとしたロジックを入れる必要がありました。そこら辺をこの記事で書きます。

ゲームパッドを購入

筆者はMacユーザなので、それに対応しているものをAmazonで物色しました。多種多様ものが発売されていますが、対応OSがMac OSと明記してあるものは案外少なく、最終的にはデザインも好みの下記を購入しました。

スクリーンショット-2024-09-23-0.14.40.png

スーファミ風のデザインに見えるし、初代プレイステーションのやつから取っ手を無くした版にも見えます。奇をてらってなくて変に目立つこともない良いデザインだと思います。

心配だったのが日本語マニュアルがなさそうで、よく知らないブランドの製品であること。でしたが、購入した後、Steamのゲームに使ってみたら、(当たり前ですが)普通に問題なく使えました。

まずは、入力イベントを拾ってみる

Webブラウザ(JavaScript)でゲームパッドの入力イベントを拾えるかを確認するために、簡単なプログラムを作成しました。

<html>
    <center>
        <canvas id="canvas" width="512" height="256" style="border:1px solid #000000; background-color: #000;"></canvas>
<script>
    const canvas = document.getElementById('canvas');
    const context = canvas.getContext('2d');

    function draw_text_center(text) {
        context.fillStyle = "#fff";
        context.font = '24px Consolas';
        context.textAlign = 'left';
        let text_w = context.measureText(text).width;
        context.fillText(text, canvas.width/2-text_w/2, canvas.height/2);
    }

    function updateGamepadStatus() {
        const gamepads = navigator.getGamepads();
        const gamepad = gamepads[0];

        if (gamepad) {
            gamepad.buttons.forEach((button, index) => {
                if (button.pressed) {
                    context.clearRect(0,0,canvas.width, canvas.height);
                    draw_text_center(`button ${index} pressed`);
                }
            });
        }

        requestAnimationFrame(updateGamepadStatus);
    }

    requestAnimationFrame(updateGamepadStatus);
</script>
</center>
</html>

ゲームパッドが対応していない場合は、真っ黒な画面が出てるだけですが、うまくいった場合は下記のような感じでテキストが表示されます。

スクリーンショット-2024-09-21-6.47.50.png

8Bitdo SN30 Proのボタンの番号

今回購入したゲームパッド8BitDo Proは、マニュアルのpdfがネットに公開されていました。

ボタンだけでも結構な数です。L3, R3はアナログ入力スティックで、ボタンとは扱い方が違うので、別の機会に試したいと思います。

それ以外のボタン類では、Bluetoothのペアリングを行う pairと謎のstar ボタン以外は、全部入力イベントが拾えました。

各ボタンと対応する番号は下記でした。

スクリーンショット-2024-09-21-7.34.26.png

L3の10, R3の11はアナログスティックですが、実は押すことができました。なかなか本格的なゲームパッドです。

この番号の配置は業界標準の仕様があるかは分かりませんが、メーカーごとに別々だとゲーム開発者は対応するのが大変そうです。

ボタン入力イベントの拾い方

Webブラウザでゲームパッドの入力を扱うには、ゲームパッドAPIを使用します。詳細はドキュメントがあるので、そちらを参照してください。

ゲームパッドAPIは、ボタンが押された時にイベントを発信してくれるタイプのものわけではなく、一定周期で状態を取得しに行くものです。

お作法としては以下のように、Webブラウザの描画フレーム単位に入力状態をチェックするようにします。

// 状態チェック用の関数を次のフレームで読んでもらう(最初のリクエスト)
requestAnimationFrame(updateGamepadStatus);

function updateGamepadStatus() {
  // ゲームパッドの状態をチェックする
  ・・・
  // サイド、状態チェック用の関数を次のフレームで読んでもらう(繰り返すのでループになる)
   requestAnimationFrame(updateGamepadStatus);
}

updateGamepadStatus() 内のゲームパッドの処理は下記です。

const gamepads = navigator.getGamepads(); // 接続されているすべてのゲームパッドが取得できる
const gamepad = gamepads[0]; // ここでは最初のゲームパッドだけ見る

if (gamepad) { // 接続されているゲームパッドがあれば
    gamepad.buttons.forEach((button, index) => {
        if (button.pressed) {
           // indexは押されたボタンの番号で、これでどのボタンが押されたか判断する
        }
    });
}

実際にゲームに組み込む際の問題点

ゲームパッドのボタンの入力状態を拾えるようになったのですが、実際のゲームに組み込むには、ちょっと使い勝手がよろしくありません。2つほど問題点があります。

  • キーボードの入力イベントの拾い方と違う
  • 好きな時に入力状態をクリアできない

キーボードの入力イベントの拾い方と違う

キーボードの入力イベントは取得は、下記の通り、押された/離されたがトリガーになって、イベントとして通知されます。

// キーが押された時のハンドラーの登録
document.addEventListener('keydown', keyDownHandler, false);
// キーが離された時のハンドラーの登録
document.addEventListener('keyup', keyUpHandler, false);

function keyDownHandler(event) {
    switch(event.key) {
    case 'j': world.player.move_left(); break;
    ・・・
    }
}

function keyUpHandler(event) {
  ・・・
}

違いを図にすると以下のようになります。

スクリーンショット-2024-09-21-16.23.22-1024x367.png

プログラムの中では入力イベントの扱い方は統一しておきたいところです。

好きな時に入力状態をクリアできない

例えば、下記の図のように、ゲームクリア画面でstartボタンが押されたらタイトル画面に映り、タイトル画面でも start ボタンが押されたらゲーム開始画面に移るとします。

スクリーンショット-2024-09-21-16.51.59.png

本当は、ユーザには「startが押された①」と「startが押された②」で2回押すことを期待しています。

が、プログラムの中では、ゲームパッドに対してチェックしに行く作りになっているため、ユーザが一度ボタンを離したのか押し続けているのか判断できません。そして、実際には画面遷移の処理時間が早く、ユーザがボタンを離す前に、状態をチェックしてしまいます。

なので、動作としては、ゲームクリア画面でstartを一度押すと、タイトル画面をすっ飛ばして、ゲーム開始してしまいます。

ここで、「startが押された①」の後に、ゲームパッドに対して入力状態をクリアしたいところです。しかし、ゲームパッドAPIにはそのような機能はありません。

残る方法としては、ゲームプログラム側のゲームパッドの入力の検出の仕方を変えることです。キーボードと同様に、ボタンが押された/離されたをトリガーにイベントを通知してもらう方式にすれば、この問題は解決します。

問題を解決するには

すでに述べた2つの問題は解決方法は一緒です。ゲームパッドAPIをそのまま使用するのではなく、キーボードのように、押された/離されたをトリガーにイベントを発行する仕組みを導入します。

といっても、複雑な機構は不要で下記のclass GamePadを用意するだけです。

class GamePad {
    constructor(no) {
        this.no = no;
        this.prev_pressed = [];
        this.listener_list = { 'pressed': [], 'released': [] }

        this.updateGamepadStatus = this.updateGamepadStatus.bind(this);
        requestAnimationFrame(this.updateGamepadStatus);
    }

    addEventListener(type, listener) {
        this.listener_list[type].push(listener);
    }

    notify_event(type, e) {
        let listeners = this.listener_list[type];
        for (let func of listeners) {
            func(e);
        }
    }

    updateGamepadStatus() {
        const gamepads = navigator.getGamepads();
        const gamepad = gamepads[this.no];

        if (gamepad) {
            // ボタンの状態をチェック
            gamepad.buttons.forEach((button, index) => {
                if (button.pressed) {
                    if(!this.prev_pressed[index]) {
               // 押されていない状態から押された状態になった場合にpressedイベントを発信する 
                        this.notify_event("pressed", { index: index });
                        this.prev_pressed[index] = true;
                    }
                } else {
                    if(this.prev_pressed[index]) {
               // 押されていた状態から押されていない状態になった場合にreleasedイベントを発信する 
                        this.notify_event("released", { index: index });
                        this.prev_pressed[index] = false;
                    }
                }
            });
        }

        // 次のフレームでまたポーリングを実行
        requestAnimationFrame(this.updateGamepadStatus);
    }
}

以下のように使用します。

let gamepad = new GamePad(0) // 0番目のゲームパッドを対象とするオブジェクトを生成する

// ボタンが押された時のイベントハンドラーを登録する
gamepad.addEventListener("pressed", btnDownHandler);

// ボタンが離された時のイベントハンドラーを登録する
gamepad.addEventListener("released", btnUpHandler);

function btnDownHandler(event) {
    swith(event.index) {
    0: Bボタンが押された処理を書く
    ・・・
    }
}

function btnUpHandler(event) {
   ・・・
}

まとめ

WebブラウザのJavaScriptからゲームパッドの入力を受け付ける方法を調べました。ゲームパッドAPIを使用することで、割と簡単にゲームパッドが使えます。

ただし、ゲームパッドの状態を一定周期で監視するスタイルのAPIなので、そのままでは使い勝手が悪く、ひと工夫必要でした。キーボードと同じように、押された/離されたをトリガーにイベントを発信クラスを実装したことで、入力イベントの扱い方を統一しました。

12
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?