「入力の切り替え」を作り込む:キーボード・タッチ・マウスの統合
遊びをつくる執筆にClaudeを使用しています。

キーボードの左右キーで動くゲームを作り、スマートフォンで開く場面を考えてみます。画面には何も押せるものがなく、キャラクターは1ミリも動きません。そこで画面に矢印ボタンを足し、マウス用のクリック処理も足していくと、ゲームの本体に「キーが押されたら」「ボタンが押されたら」「ドラッグされたら」という分岐が散らばっていきます。ここから先が厄介で、新しい入力を足すたびに、ゲームの動きを変えるコードを何か所も直すことになります。 この記事の問いは、「入力の種類を一つの仕組みにまとめるには、何を最初に決め、どこで合流させればよいか」です。答えの中心は、ゲーム本体に「キーボードかタッチか」を知らせないことです。本体が見るのは「左」「右」「決定」といったアクションだけにします。キーボード、マウス、タッチは、入口でアクションに翻訳してから渡します。 以下では、単独で動く43行ほどのHTMLを使い、手順1から順に作って確かめます。外部ライブラリもサーバーも使いません。最後に、アソバースに投稿するときに気にしたい点にも触れます。
この記事の目次
手順1:入力ではなく「アクション」を先に決める
最初に、ゲームが必要とする操作を、入力機器と切り離して書き出します。紙一枚で足ります。
- 左へ動く、キーボード:← / A、マウス:画面の ◀ を押す、または盤面を左へドラッグ、タッチ:同左
- 右へ動く、キーボード:→ / D、マウス:画面の ▶ を押す、または盤面を右へドラッグ、タッチ:同左
- 決定、キーボード:Enter / Space、マウス:「決定」ボタンをクリック、タッチ:「決定」ボタンをタップ
この表の列がそのまま実装の単位になります。マウスとタッチを別の列にしてもかまいませんが、後述のPointer Events(マウス・タッチ・ペンを同じ形で扱えるブラウザの仕組み)を使えば、実装上は同じ処理に合流できます。
公式作品にも、同じ考え方が見て取れます。たとえば「紙ひこうき郵便」は、左右キー、ドラッグ、矢印ボタンの3通りで操縦できます。「潮目のオセロ」は矢印キーとEnterでも操作できます。ここから読み取れるのは、一つの目的の操作に複数の入口が用意されている、ということです。ただし、それぞれの入口が内部でどうつながっているかは、作品を開いて自分の手で確かめてください。
アクションを決めるときの判断基準は次の三つです。
- アクション名は「何をするか」で付けます。「スペースキー」ではなく「決定」にします。
- 1回押すと1回だけ起きる操作(決定、一時停止)と、押している間続く操作(移動)を区別します。
- 数は少なく保ちます。ボタンを置けない小さな画面でも、全アクションが成り立つかを考えます。
手順2:最小の統合入力を動かす
次のコードを index.html として保存し、ブラウザで開いてください。青い四角が下に表示されます。
<!doctype html>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{margin:0;font:16px sans-serif;touch-action:none;user-select:none}
canvas{display:block;width:100%;max-width:480px;background:#123;touch-action:none}
button{font-size:24px;width:30%;height:64px;touch-action:none}
</style>
<canvas id="c" width="480" height="240"></canvas>
<button data-act="left">◀</button><button data-act="ok">決定</button><button data-act="right">▶</button>
<p id="log"></p>
<script>
const map={ArrowLeft:'left',KeyA:'left',ArrowRight:'right',KeyD:'right',Enter:'ok',Space:'ok'};
const held=new Set(), pressed=new Set();
let device='-', x=240, target=null, flash=0;
function down(a,d){ if(!held.has(a)) pressed.add(a); held.add(a); device=d; }
addEventListener('keydown',e=>{const a=map[e.code]; if(a){e.preventDefault(); down(a,'keyboard');}});
addEventListener('keyup',e=>held.delete(map[e.code]));
addEventListener('blur',()=>held.clear());
document.querySelectorAll('button').forEach(b=>{
const a=b.dataset.act, off=()=>held.delete(a);
b.addEventListener('pointerdown',e=>{b.setPointerCapture(e.pointerId); down(a,e.pointerType);});
b.addEventListener('pointerup',off);
b.addEventListener('pointercancel',off);
});
const c=document.getElementById('c'), g=c.getContext('2d');
const px=e=>{const r=c.getBoundingClientRect(); return (e.clientX-r.left)*c.width/r.width;};
c.addEventListener('pointerdown',e=>{c.setPointerCapture(e.pointerId); target=px(e); device=e.pointerType;});
c.addEventListener('pointermove',e=>{if(target!==null) target=px(e);});
c.addEventListener('pointerup',()=>target=null);
c.addEventListener('pointercancel',()=>target=null);
function frame(){
let dx=(held.has('right')?1:0)-(held.has('left')?1:0);
if(target!==null && Math.abs(target-x)>4) dx=Math.sign(target-x);
x=Math.max(20,Math.min(460,x+dx*4));
if(pressed.has('ok')) flash=10;
pressed.clear(); flash=Math.max(0,flash-1);
g.fillStyle='#123'; g.fillRect(0,0,480,240);
g.fillStyle=flash?'#fd5':'#6cf'; g.fillRect(x-20,180,40,40);
document.getElementById('log').textContent='最後の入力: '+device;
requestAnimationFrame(frame);
}
frame();
</script>このコードの要点は、held と pressed の二つの集合にあります。
- held は「いま押されているアクション」です。移動のように押している間続く操作に使います。
- pressed は「このフレームで新しく押されたアクション」です。決定のように1回だけ起きる操作に使い、frame() の中で読んだあとに空にします。
入口は3種類ありますが、どれも最終的に down('left', …) のような同じ関数を呼びます。frame() の中には keydown も pointerdown も出てきません。ここが、入力を一つの仕組みにまとめるという意味の核です。盤面のドラッグだけは位置という連続値なので target に入れ、frame() の冒頭で「左右の方向」に変換して、同じ dx に合流させています。
キーの判定に e.key ではなく e.code を使っているのは、CapsLockや日本語入力の状態で文字が変わらないようにするためです。ただし e.code はキーの物理的な位置を指します。キー配列が違う環境でWASDの並びが崩れる可能性もあるので、必ず矢印キーも併用してください。
手順3:入口ごとに動作を確かめる
コードが動いたら、次の順で確認します。各項目で、画面下の「最後の入力」の表示も見てください。
- キーボード:←→とA/Dで四角が動くか。Enter/Spaceで一瞬黄色くなるか。キーを押したままにしても、黄色の点滅が連続しないか(pressed は押した瞬間だけ立つ設計です)。
- マウス:◀▶を押し続けている間だけ動くか。ボタンの外へマウスをずらして離しても、動きが止まるか。盤面を押してドラッグすると、四角がポインタへ寄っていくか。
- タッチ:スマートフォンの実機が手元にあれば、実機で同じことを行います。ない場合は、ブラウザの開発者ツールのデバイスエミュレーションでも近い確認ができます。ただし、実機の指の太さやスクロールの挙動までは再現されません。
- 切り替え:キーボードで動かした直後にタッチ、その直後にマウスへと、機器を行き来します。表示が毎回切り替わり、動きが残ったり固まったりしなければ合格です。
- フォーカス喪失:右キーを押したままほかのタブへ切り替え、戻ってください。四角が勝手に右へ動き続けていれば、blur の処理が効いていません。
記録の取り方は単純です。上の5項目を表にして、入口(キーボード・マウス・タッチ)ごとに「動く/動かない/止まらない」を、それぞれ最低3回は書き留めてください。1回では偶然の成功と区別できません。
手順4:入力が「固まる」「二重になる」を潰す
統合したあとに起きやすい不具合は、だいたい次の型に収まります。
押しっぱなしのまま離された
キーを押している最中にウィンドウが非アクティブになると、keyup が届かず、アクションが押されたままになります。手順2の blur での全消去はその対策です。タッチでも、指が画面の外へ出たり、システムの操作に割り込まれたりすると pointerup の代わりに pointercancel が来ることがあります。pointerup と pointercancel をセットで処理してください。
ボタンの外へ指が滑った
setPointerCapture を使わないと、ボタンの上で押した指が外へ出た時点で、離したことが検知できなくなります。コードでは押した時点でボタンにポインタを固定し、確実に pointerup を受け取れるようにしています。
ブラウザの既定動作とぶつかる
タッチ操作では、ブラウザが画面のスクロールやピンチ拡大を始めて、ドラッグが途中で途切れることがあります。CSSの touch-action: none は、その要素の上でブラウザ標準のスクロールなどを止める指定です。ゲーム画面だけに適用し、ページ全体の説明文など読ませたい部分には付けないのが基本です。キーボードでも、Spaceが画面のスクロールを起こすことがあるため、preventDefault() で止めています。
同時に複数の指
2本指で左右のボタンを同時に押すと、left と right が両方立ちます。コードでは dx が0になり、止まります。この挙動が正しいかどうかはゲームによります。「後から押したほうを優先する」仕様にするなら、held を集合ではなく押した順の配列にします。まず、どちらにしたいかを決めてください。
押しにくい大きさ
ボタンは見た目が合っていても、指で押すには小さいことがあります。一般に指で押す対象は縦横それぞれ数ミリ以上は欲しいと言われます。実機で、片手で持った状態で押し外さないかを確かめてください。
手順5:アソバースに載せる前の確認
ここまでのコードをゲームに組み込んだら、アソバースでの動作に関わる点を整理します。アップロードする作品は、サンドボックス付きのiframeの中で実行されます。サイト側の仕様として確認できるのは、この点までです。
- 投稿するHTMLまたはZIPは、直下の index.html が入口です。このコードのように外部通信なしで動く単独のHTMLなら、そのまま入口にできます。
- iframeの中では、キーボード入力を受け取るには、まずゲーム画面をクリックまたはタップしてフォーカスを与えることが必要になる場合があります。はじめの画面に「画面をタップして開始」のような一枚を挟むと、キーボードの人にも自然に伝わります。実際の挙動は、投稿前の表示確認の段階で、ご自分のブラウザで確かめてください。
- 「どの入口を使っているか」の記憶(最後に使った機器など)を、アップロード作品内のlocalStorageに頼って残す設計は避けてください。アップロード作品内のlocalStorage/sessionStorageは、一時メモリーとしての互換処理です。残したい設定が必要なら、クラウドセーブ(作品ごとにJSONで128KiBまで)の利用を検討します。
- 操作説明は、入口ごとに書き分けます。投稿画面の「遊び方」は、作者が自分で書く任意の項目です。キーボードなら「←→で移動、Enterで決定」、タッチなら「画面の矢印で移動」といった書き方で、アクション表がそのまま使えます。
「動きを抑える」設定に当たる処理も、入力の統合と相性がよい領域です。ゲームの動きを frame() の一か所に集めておけば、アニメーションを弱める分岐も一か所で済みます。
次の行動
まず自作のゲームから、キー・マウス・タッチの処理を書いている場所をすべて探し、手順1の表に書き出してください。表が埋まったら、ゲーム本体が呼ぶ関数を「アクション名」だけに置き換え、入口側のコードを一つの関数にまとめます。最後に、手順3の5項目を3回ずつ確認すれば、入力まわりの完成度はひとまず見通せます。
参考資料
- MDN Web Docs「Pointer events」
- MDN Web Docs「KeyboardEvent: code プロパティ」
- MDN Web Docs「touch-action」

