マガジン

キーボードとタッチの両方で遊べるゲームにする方法

遊びをつくる

執筆にClaudeを使用しています。

キーボードとタッチの二つの入力が、同じ駒のジャンプにつながる様子を描いたイラスト。
記事のテーマをもとに制作したAI生成イラスト。

マウスで動かすことしか想定していないゲームを作ったあと、「スマートフォンでも遊びたい」「キーボードだけで操作したい」という要望に気づくことは珍しくありません。こうした要望に個別に対応しようとすると、コードが分岐だらけになり、バグが混入しやすくなります。この記事では、入力の種類を「ゲームが理解できる操作」に変換する層(アクション層)をひとつ設ける考え方と、実際にキーボードとタッチを扱うAPIの使い方を説明します。

この記事の目次
  1. 入力とゲームロジックを分ける理由
  2. キーボード入力を受け取る
  3. Pointer Eventsでタッチとマウスを統一する
  4. スマートフォン向けの仮想ボタンを作る
  5. 入力方式を検知してUIを切り替える
  6. まとめ:アクション層を中心に組み立てる

入力とゲームロジックを分ける理由

ゲームのロジックは「プレイヤーが上に動く」「決定ボタンが押された」という抽象的な操作を知っていれば十分です。その操作がキーボードのArrowUpキーから来たのか、画面上の仮想ボタンのタップから来たのか、ゲームロジックが気にする必要はありません。入力の種類とゲーム操作を切り離すことで、新しい入力方法を追加するときも、ゲームロジックに手を加えずに済みます。

具体的には、ゲームが参照する「現在押されている操作」を表すオブジェクトをひとつ用意します。キーボードもタッチも、そのオブジェクトを更新する役割だけを持ちます。以下はその構造の例です(説明用のコードであり、動作を保証するものではありません)。

javascript
// ゲームが参照するアクションの状態
const actions = {
  up: false,
  down: false,
  left: false,
  right: false,
  confirm: false,
};

// ゲームループではactionだけを見る
function update() {
  if (actions.up)   player.y -= speed;
  if (actions.down) player.y += speed;
  if (actions.left) player.x -= speed;
  if (actions.right)player.x += speed;
}

キーボードとタッチはそれぞれ別のコードで `actions` を書き換えますが、ゲームループは `actions` だけを見て動きます。入力の追加・変更はアクション層の修正で完結します。

キーボード入力を受け取る

キーボード入力は `keydown` と `keyup` イベントで扱います。MDNの仕様に基づくと、キーの識別には `event.key` プロパティを使うのが現在の標準的な方法です。矢印キーは `"ArrowUp"` `"ArrowDown"` `"ArrowLeft"` `"ArrowRight"`、スペースは `" "`(半角スペース1文字)、エンターは `"Enter"` という値になります。WASDの場合は `"w"` `"a"` `"s"` `"d"` ですが、Shiftキーと同時押しのときは `"W"` のように大文字になるため、両方を処理するか `event.key.toLowerCase()` で統一するかを決めておきます。

javascript
function keyToAction(key) {
  switch (key.toLowerCase()) {
    case 'arrowup':  case 'w': return 'up';
    case 'arrowdown':case 's': return 'down';
    case 'arrowleft':case 'a': return 'left';
    case 'arrowright':case 'd': return 'right';
    case ' ':        case 'enter': return 'confirm';
    default: return null;
  }
}

window.addEventListener('keydown', (e) => {
  const action = keyToAction(e.key);
  if (action) {
    actions[action] = true;
    e.preventDefault(); // スクロールなどブラウザ既定動作を止める
  }
});

window.addEventListener('keyup', (e) => {
  const action = keyToAction(e.key);
  if (action) actions[action] = false;
});

`e.preventDefault()` を呼ぶと矢印キーによるページスクロールを止められます。ただし、ゲームキャンバスにフォーカスがある場合に限るなど、適用範囲を絞りたいときは `e.target` を確認してから呼ぶようにすると、ページ全体の操作性への影響を抑えられます。

なお、`keydown` イベントはキーを押し続けると繰り返し発火します。ゲームループ内で `actions` の状態を毎フレーム読む設計であれば問題ありませんが、「1回押したら1回だけ反応する」ものが必要なときは `keydown` のハンドラ内でフラグを立て、次の `keyup` まで無視する処理が必要です。

Pointer Eventsでタッチとマウスを統一する

スマートフォンのタッチを扱う方法はいくつかありますが、W3C勧告の Pointer Events Level 3 仕様では、マウス・タッチ・スタイラスを `pointerdown` `pointermove` `pointerup` という共通のイベントで扱えます。`event.pointerType` プロパティが `"mouse"` `"touch"` `"pen"` のいずれかを返すため、必要なら入力種別ごとに処理を分岐することもできます。

タッチが複数の指で同時に発生する場合は、`event.pointerId` でそれぞれの接触を区別します。各指に対して一意のIDが割り振られるため、`Map` を使って追跡するのが定石です。

javascript
// CSS側でブラウザのスクロール・ズームをゲーム領域に対して止める
// canvas { touch-action: none; }

const activePointers = new Map();

canvas.addEventListener('pointerdown', (e) => {
  activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
  canvas.setPointerCapture(e.pointerId); // 要素外に出てもイベントを受け取り続ける
  onInputStart(e.clientX, e.clientY);
});

canvas.addEventListener('pointermove', (e) => {
  if (!activePointers.has(e.pointerId)) return;
  activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
  onInputMove(e.clientX, e.clientY);
});

canvas.addEventListener('pointerup', (e) => {
  activePointers.delete(e.pointerId);
  onInputEnd();
});

canvas.addEventListener('pointercancel', (e) => {
  activePointers.delete(e.pointerId);
  onInputEnd();
});

CSSの `touch-action: none` をゲーム領域の要素に指定すると、ブラウザがタッチによるスクロールやピンチズームを処理する前にPointer Eventsが発火します。指定しないと、ブラウザが操作を横取りしてゲームへのイベントが届かないことがあります。

`setPointerCapture` を呼ぶと、ポインターが要素の外に出てもその要素がイベントを受け取り続けます。ゲームキャラクターをドラッグするときや、キャンバス外まで指が動くアクションゲームで役立ちます。

スマートフォン向けの仮想ボタンを作る

キーボードのないタッチデバイスでは、ゲーム画面上に仮想ボタンを配置することで操作を提供できます。仮想ボタンはHTML要素(`<button>` や `<div>`)として作り、Pointer Eventsで押下・離しを検知して `actions` を更新します。

javascript
// 仮想ボタン要素にdata属性でアクション名を持たせる例
// <button class="vbtn" data-action="up">↑</button>

document.querySelectorAll('.vbtn').forEach((btn) => {
  btn.addEventListener('pointerdown', (e) => {
    e.preventDefault();
    const action = btn.dataset.action;
    if (action in actions) actions[action] = true;
  });
  btn.addEventListener('pointerup', () => {
    const action = btn.dataset.action;
    if (action in actions) actions[action] = false;
  });
  btn.addEventListener('pointercancel', () => {
    const action = btn.dataset.action;
    if (action in actions) actions[action] = false;
  });
});

仮想ボタンのデザインについていくつか実装上の注意があります。

  • タップ領域を十分に大きくする:モバイルでの操作を考えると、最低でも44×44ピクセル程度の領域を確保するとミスタップが減ります(Apple Human Interface Guidelinesが参考になりますが、絶対的な規則ではありません)。
  • ゲーム画面を隠さない配置:ゲーム描画領域と仮想ボタン領域を分けるか、透過レイヤーとして重ねる設計にします。
  • PCでは非表示にする:CSSのメディアクエリや、`pointerType` の検知でタッチ入力がない環境では仮想ボタンを隠すと画面がすっきりします。ただし、最初の入力が来るまでポインタータイプを知る方法がないため、初期状態を決めておく必要があります。
  • 複数ボタンの同時押し:斜め移動に対応するには、複数の `pointerdown` が同時に存在できるよう、前述の `pointerId` ベースの管理を仮想ボタンにも適用します。

入力方式を検知してUIを切り替える

デバイスによってどの入力方式が主になるかは、ページを開いた瞬間にはわかりません。キーボードショートカットの案内を出すべきかどうか、仮想ボタンを表示すべきかどうかは、実際に入力が来てから判断するのが現実的です。

javascript
let inputMode = 'unknown'; // 'keyboard' | 'touch' | 'mouse'

window.addEventListener('keydown', () => {
  if (inputMode !== 'keyboard') {
    inputMode = 'keyboard';
    updateInputUI();
  }
}, { passive: true });

window.addEventListener('pointerdown', (e) => {
  const mode = e.pointerType === 'touch' ? 'touch' : 'mouse';
  if (inputMode !== mode) {
    inputMode = mode;
    updateInputUI();
  }
}, { passive: true });

function updateInputUI() {
  const showVirtualButtons = inputMode === 'touch';
  document.getElementById('virtual-controls').hidden = !showVirtualButtons;
  document.getElementById('keyboard-hint').hidden = inputMode !== 'keyboard';
}

最初のキー入力が来た時点でキーボードUIを、タッチが来た時点でタッチUIを表示し、その後も切り替えを受け入れる設計です。ゲームパッドを扱う場合は Gamepad API が別途存在しますが、ここでは範囲外としています。

まとめ:アクション層を中心に組み立てる

この記事で紹介した設計の核心は、入力の種類をゲームロジックに持ち込まないという一点です。キーボード・マウス・タッチのすべてが `actions` という共通オブジェクトを書き換え、ゲームループはそのオブジェクトだけを読む。この分離があれば、新しい入力方法を追加するときにゲームロジックを触らずに済みます。

実装の手順を整理すると、次のようになります。

  1. ゲームに必要な操作の種類(up/down/left/right/confirm など)を列挙し、`actions` オブジェクトとして定義する。
  2. `keydown` / `keyup` で `event.key` を読み、対応するアクションに変換して `actions` を更新する。
  3. ゲーム要素(canvasなど)に `touch-action: none` を指定してから、`pointerdown` / `pointermove` / `pointerup` / `pointercancel` を登録する。
  4. タッチ用の仮想ボタンを作り、Pointer Eventsで同じ `actions` を更新する。
  5. 最初の入力イベントでUIを切り替え、仮想ボタンやヒント表示を動的に出し分ける。

Pointer Events Level 3 はW3Cの勧告段階の仕様であり、主要なモバイル・デスクトップブラウザで利用できます。KeyboardEvent の `key` プロパティもHTML Living Standard準拠のブラウザで動作します。ただし、古いブラウザや一部の組み込みブラウザでは対応状況が異なる場合があるため、対象とする環境を確認してから実装する習慣をつけておくとよいでしょう。

アソバースに投稿するゲームはHTMLファイルやZIPファイルとして提出できます。キーボードとタッチの両方に対応したゲームを作ったら、実際にスマートフォンとキーボード環境で動作を確認してから投稿してみてください。入力の幅が広がると、遊べる人の幅も広がります。

出典・参考資料