マガジン

ゲームのキー入力がブラウザのショートカットに奪われる問題

遊びをつくる

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

大きな赤いボタンと指先、ばね、曲線のレールで押す・離す・待つを表す立体イメージ
記事のテーマをもとに制作したAI生成イラスト。

ブラウザゲームを作っていると、スペースキーでジャンプしようとしたらページがスクロールした、F5キーでリロードが走ってゲームが止まった、という経験をする方は少なくありません。これはゲームのバグではなく、ブラウザがキーイベントを先に処理してしまう仕組みによるものです。 原因と対処はシンプルですが、「どのキーが奪われるか」「どこで止めるか」を正確に把握しておかないと、修正が不完全になったり、逆にブラウザの必要な動作まで壊したりします。この記事では、奪われやすいキーの一覧、preventDefault() の正しい使い方、フォーカス管理という三つの切り口から整理します。

この記事の目次
  1. なぜブラウザがキーを先取りするのか
  2. 奪われやすいキーと発動条件の一覧
  3. `preventDefault()` をどこに書くか
  4. フォーカス管理という根本対策
  5. スクロール禁止の追加設定
  6. コンテキストを考慮した制御
  7. 次の行動
  8. 参考資料

なぜブラウザがキーを先取りするのか

ブラウザはキーイベントを「ドキュメント全体」で受け取り、ゲームのcanvasやdivよりも先にデフォルト動作を実行することがあります。たとえばスペースキーはフォーカスがボタンにあれば「クリック」、そうでなければ「スクロール」として処理されます。矢印キーはページのスクロール、Tabキーはフォーカス移動、F1〜F12はブラウザ機能、Backspaceは「前のページに戻る」として機能する環境もあります。

JavaScriptのキーイベントは keydown → keypress → keyup の順に発火しますが、デフォルト動作の多くは keydown の時点で決まります。そのため keydown で preventDefault() を呼ばないと、スクロールは止められません。

奪われやすいキーと発動条件の一覧

すべてのキーが常に奪われるわけではなく、条件によって変わります。判断の基準として次の傾向を押さえておくと便利です。

  • Space、典型的なブラウザの動作:ページ下スクロール、発動しやすい条件:フォーカスがcanvas・div以外のとき
  • ↑↓←→、典型的なブラウザの動作:ページスクロール、発動しやすい条件:スクロール可能な要素が存在するとき
  • Tab、典型的なブラウザの動作:フォーカス移動、発動しやすい条件:常に
  • Backspace、典型的なブラウザの動作:前ページに戻る、発動しやすい条件:フォームの外にフォーカスがあるとき(ブラウザ依存)
  • F5 / Ctrl+R、典型的なブラウザの動作:リロード、発動しやすい条件:常に(keydownでは止められないケースあり)
  • Ctrl+S、典型的なブラウザの動作:ページ保存、発動しやすい条件:常に
  • Ctrl+P、典型的なブラウザの動作:印刷ダイアログ、発動しやすい条件:常に
  • Alt+←→、典型的なブラウザの動作:ページ前後移動、発動しやすい条件:常に(ブラウザ依存)

F5やCtrl+Rのようなブラウザ固有の操作は、preventDefault()で止められない場合があります。これらはゲームのキーとして使わないのが現実的な設計判断です。

`preventDefault()` をどこに書くか

最も基本的な対処は、ゲームが使うキーに対して keydown イベントで preventDefault() を呼ぶことです。

js
window.addEventListener('keydown', (e) => {
  const gameKeys = [
    'Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'
  ];
  if (gameKeys.includes(e.code)) {
    e.preventDefault();
  }
  // ゲームの処理
});

ここで重要なのは e.key ではなく e.code を使う点です。e.key はキーボードレイアウトや修飾キーの影響を受けますが、e.code は物理的なキーの位置を表すため、スペースは常に 'Space'、矢印は 'ArrowUp' などと一致します。英字キーを使う場合も 'KeyW' 'KeyA' のように書けるので、レイアウト非依存になります。

やりがちな失敗: keypress だけで止めようとするケース。keypress はすでに非推奨で、スクロールなどのデフォルト動作は keydown の段階で始まるため、keypress で preventDefault() しても手遅れになります。

別の失敗: window ではなく canvas 要素に対してリスナーを登録したのに止まらないケース。canvas はデフォルトでキーイベントを受け取れないため、tabIndex="0" を設定してフォーカスを与える必要があります。それでも window に登録した方がゲーム全体で確実に動作します。

フォーカス管理という根本対策

preventDefault() はあくまで対症療法です。もう一つの切り口は「フォーカスをゲーム要素に確実に当てること」で、これが不安定だと問題が再発します。

ページを開いた直後、フォーカスはURLバーや最初のインタラクティブ要素にあることが多いです。この状態でキーを押すとブラウザが処理してしまいます。canvas や ゲームコンテナの div に tabIndex="0" を設定し、ゲーム開始時に .focus() を呼ぶことで、フォーカスをゲーム領域に誘導できます。

js
const canvas = document.getElementById('game');
canvas.tabIndex = 0;
canvas.focus();

ただしこれだけでは、ユーザーがブラウザの別の場所をクリックしたあとにフォーカスが外れます。解決策は二択です。

  1. クリックで再フォーカス: ゲーム画面内のクリックイベントで canvas.focus() を再実行する。ユーザーが画面を触った直後は自然にフォーカスが戻る。
  2. window にリスナーを置く: フォーカスの有無に関わらずキーを取得したいなら、リスナーを window に置き、preventDefault() で必要なキーを止める。こちらの方が実装はシンプルですが、ゲーム画面が非表示や非アクティブな状態でもキーを拾ってしまうので、ゲームの状態(起動中か否か)でフラグを持って制御するのが丁寧です。

スクロール禁止の追加設定

keydown の preventDefault() でもスクロールが止まらない場合があります。その原因の多くは、passive: true オプションです。パフォーマンス向上のためブラウザが一部のイベントリスナーを「パッシブ」として扱い、preventDefault() を無視することがあります。

明示的に passive: false を指定することで解決します。

js
window.addEventListener('keydown', handler, { passive: false });

さらに、スペースや矢印によるスクロール自体をCSSで止めたい場合は、ゲームコンテナに overflow: hidden を指定し、body や html のスクロールを無効化する方法もあります。ただしこの方法はページ全体のスクロールを封じるため、ゲームが画面全体を占有する構成のときに限って使うのが安全です。

コンテキストを考慮した制御

すべてのキーに一律で preventDefault() をかけることは避けるべきです。Tabキーを止めると、支援技術(スクリーンリーダーなど)を使うユーザーがゲームの外に出られなくなります。少なくとも「ゲームが終了した・一時停止した状態」ではTabによるフォーカス移動を許可するようにしておくことが、アクセシビリティの観点から望ましい考え方です。

同様に、Ctrl+C・Ctrl+V・Ctrl+Aなどのテキスト操作系ショートカットは、ゲーム内にテキスト入力フォームがある場合は止めないようにします。e.ctrlKey や e.metaKey(Mac)と e.code を組み合わせて、修飾キーありの組み合わせだけ除外するとよいでしょう。

js
if (gameKeys.includes(e.code) && !e.ctrlKey && !e.metaKey) {
  e.preventDefault();
}

次の行動

まずブラウザのコンソールでキーイベントをログに出し、自分のゲームで使うキーがどのデフォルト動作と競合しているか確認してください。その上で keydown に { passive: false } 付きのリスナーを追加し、ゲームキーのみ preventDefault() する実装を試してみてください。作ったゲームはアソバースに投稿して、実際のブラウザ環境で動作を確認するのが最も確実な検証方法です。

参考資料

  • MDN Web Docs — KeyboardEvent: code プロパティ
  • MDN Web Docs — EventTarget.addEventListener() の passive オプション
  • MDN Web Docs — Event.preventDefault()
  • WHATWG HTML Living Standard — フォーカス管理