この記事では、WebのJavaScriptプログラムからゲームパッドの操作を受け付ける方法について説明します。
モチベーション
ロードランナー風アクションパズルゲームをブログを書きながら作り直して、「Treasure & Monster」としていい感じになったので、キーボードではなくゲームパッドで操作したくなりました。
Javascriptでゲームパッドが使えるのは以前から知っていましたが、今回初めて本格的に自作ゲームに対応しようと思ったわけです。
実際にやってみると、そのままだと動作がイマイチなところがあり、ゲームに組み込むのにちょっとしたロジックを組む必要がありました。そこら辺は、この記事で解説していきます。
ゲームパッドを購入
なにはともあれ、ゲームパッドを手に入れないと始まりません。
筆者はMacユーザなので、それに対応しているものをAmazonで物色しました。多種多様ものが発売されていますが、対応OSがMac OSと明確に書いてあるものとなると案外少なく、最終的にはデザインも好みの下記を購入しました。
スーファミ風のデザインに見えるし、初代プレイステーションのやつから取っ手を無くした版にも見えます。奇をてらってなくて変に目立つこともない良いデザインだと思います。
心配だったのが日本語マニュアルがなさそうなよく知らないブランドの製品であること。でしたが、購入した後、Steamのゲームに使ってみたら、(当たり前ですが)普通に問題なく使えました。
余談ですが、ゲームパッドを探すのに苦労している点からしても、アップルがゲームにまったく力を入れていないのが不思議で仕方がありません。Macも自社のチップで固めているので、SPECに統一感があってゲーム機と一緒です。Mac miniはコスパ最強だし、個人的には、本気でやれば今のコンソール機を駆逐して覇権を取れると思っているのですが。
まずは、入力イベントを拾ってみる
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>
ゲームパッドが対応していない場合は、真っ黒な画面が出てるだけですが、うまくいった場合は下記のような感じでテキストが表示されます。
8Bitdo SN30 Proのボタンの番号
今回購入したゲームパッド8BitDo Proは、マニュアルのpdfがネットに公開されていました。
ボタンだけでも結構な数です。L3, R3はアナログ入力スティックで、ボタンとは扱い方が違うので、別の機会に試したいと思います。
それ以外のボタン類では、Bluetoothのペアリングを行う pairと謎のstar ボタン以外は、全部入力イベントが拾えました。
各ボタンと対応する番号は下記でした。
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) {
・・・
}
違いを図にすると以下のようなになります。
プログラムの中では入力イベントの扱い方は統一しておきたいところです。
好きな時に入力状態をクリアできない
例えば、下記の図のように、ゲームクリア画面でstartボタンが押されたらタイトル画面に映り、タイトル画面でも start ボタンが押されたらゲーム開始画面に移るとします。
本当は、ユーザには「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なので、そのままでは使い勝手が悪く、ひと工夫必要でした。キーボードと同じように、押された/離されたをトリガーにイベントを発信クラスを実装したことで、入力イベントの扱い方も統一できて使いやすくなったと思います。
今回はボタンのイベントのみを拾うようにしましたが、別の機会でアナログスティックの情報を拾ってゲームに使ってみたいと思います。
コメント