同じ画面で2人が遊ぶゲームの作り方:ローカル対戦
遊びをつくる執筆にClaudeを使用しています。

ローカル対戦とは、1台の端末・1つの画面を2人で共有してプレイする形式のことです。サーバーもWebSocketも必要なく、キーボードの左半分と右半分を1人ずつ担当するだけで成立します。オンライン対戦と比べると技術的なハードルが大幅に低く、「作って、すぐ横の人と遊ぶ」という体験を最短距離で実現できます。 この記事では、ブラウザゲームとしてローカル対戦を実装するときの設計の考え方、入力の割り当て方、画面レイアウトの判断基準、そして詰まりやすいポイントを順に説明します。ネットワーク通信を一切使わず、HTMLとJavaScriptだけで完結する実装を前提にしています。
ローカル対戦が向くゲームと向かないゲーム
すべてのゲームジャンルがローカル対戦に向くわけではありません。実装を始める前に、自分のゲームがこの形式に合っているかを判断してください。
向くのは、2人のキャラクターや操作対象が同時に動き続けるゲームです。対戦格闘、横スクロールアクション、ブロック崩しの2人版、テーブルゲーム(チェスや将棋など)がこれにあたります。操作が交互になるゲームは特にシンプルで、「どちらのターンか」を変数一つで管理できます。
向きにくいのは、プレイヤーごとに秘匿情報が必要なゲームです。カードゲームで手札を隠す場面や、戦略ゲームで霧の戦争(fog of war)を使う場面は、1つの画面では実現が難しくなります。その場合はスプリットスクリーン(画面分割)かオンライン対戦に切り替えを検討してください。
入力を2人分に割り当てる
キーボード入力の分割が、ローカル対戦実装の核心です。基本的な考え方は「どのキーが押されたかをイベントで受け取り、プレイヤーIDと対応付ける」だけです。
よく使われる割り当ての例を示します。
- P1、移動:W / A / S / D、アクション:Shift(左)
- P2、移動:↑ / ← / ↓ / →、アクション:Enter
JavaScriptでは keydown と keyup イベントを使い、event.key の値で分岐します。「押しっぱなし」の状態を扱うには、キーが押されたときに true、離れたときに false をセットするオブジェクト(キーマップ)を用意し、ゲームループの中でそのオブジェクトを毎フレーム参照するのが定石です。
**1つ目は `event.preventDefault()` の呼び忘れです。** スペースキーやカーソルキーはブラウザのスクロール動作と競合します。ゲームで使うキーに対しては `preventDefault()` を呼んで、ブラウザのデフォルト動作を止めてください。特にカーソルキーはページがスクロールして操作不能になりやすいため、見落としがちです。
**2つ目はキー同時押しの上限です。** 安価なキーボードの多くは、複数キーの同時押しを一定数以上認識しません(いわゆるキーロールオーバーの制限)。6キー以上を同時に押す設計は避け、2人合計でも4〜6キー程度に収めると安全です。
---
## 画面レイアウトを決める3つのパターン
2人が1つの画面を共有するとき、何をどこに表示するかが体験の質を左右します。代表的なパターンは3つです。
### 共有フィールド型
2人のキャラクターが同じフィールド上に存在し、カメラも共通です。対戦格闘や2人協力アクションに向きます。シンプルに作れる反面、一方が端に追い詰められると見切れる問題が起きます。対策として「2人の中間にカメラを置き、距離が離れるにつれてズームアウトする」手法がよく使われます。
### スプリットスクリーン型
画面を上下または左右に半分に分割し、各プレイヤーの視点を独立させます。レースゲームや探索ゲームで有効です。CanvasをCSSで2つ並べるか、1つのCanvasを半分ずつ描画領域として使い分ける方法があります。後者の場合、`ctx.save()` と `ctx.restore()` でクリッピング領域を切り替えながら描画します。
### ターン制型
対局系(チェス・オセロなど)のように、どちらかが操作しているあいだ相手は待つ形式です。画面を分割する必要がなく、「誰のターンか」を示すUIを目立たせるだけで成立します。実装がもっとも素直で、AI(コンピュータ対戦)への拡張もしやすいです。
---
## ゲームループと状態管理の設計
ローカル対戦でよくある失敗は、プレイヤー1とプレイヤー2のロジックを別々の関数に書きすぎて、フレームのタイミングがずれることです。原則として、**2人分の状態を1つのゲームループで同時に更新する**設計にしてください。
ゲームの状態をオブジェクト配列で持つと管理しやすくなります。たとえば `players[0]` と `players[1]` に座標・速度・HP・操作キーマップへの参照をまとめて持たせ、ループの中で `players.forEach(p => update(p))` のように統一して処理します。衝突判定も同じループの中で行い、どちらが先に処理されたかによって結果が変わらないよう順序を固定してください。
ゲームの進行状態(「待機中」「プレイ中」「どちらかが勝利」「引き分け」)も一つの変数で管理し、状態に応じて描画と入力受付を切り替えます。これを曖昧にしたまま実装を進めると、勝利判定後も操作を受け付け続けるバグが頻発します。
---
## モバイルとキーボードが使えない環境への対応
スマートフォンのブラウザではキーボードが前提にできません。ローカル対戦をモバイルで動かしたい場合は、画面上のバーチャルボタン(タッチ操作用UI)を左右に配置する方法が現実的です。ただし画面の半分近くをボタンが占めるため、フィールドが狭くなります。ゲームの設計段階でプレイ環境を決め、PCキーボード前提か、タッチ前提か、両対応かを選んでください。
両対応にする場合は、入力の受け皿を「仮想コントローラーオブジェクト」として抽象化し、キーボードイベントもタッチイベントも最終的には同じオブジェクトを更新するよう設計すると、後から入力方式を増やしやすくなります。
---
## 次の行動
まずターン制のシンプルなゲーム(〇×ゲームや簡易オセロなど)を1つ完成させ、「2人の状態を1ループで管理する」感覚をつかんでください。その後、同時操作型に挑むとキーマップや当たり判定の設計が自然と身につきます。完成したゲームはHTMLファイルやZIPとしてアソバースに投稿し、実際に隣の人と遊んでフィードバックを得るのが、次の改善への一番の近道です。



