マガジン

マッチング機能の作り方:部屋・招待・ランダム

遊びをつくる

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

一台のタブレットを囲む二人と、離れた机同士をつなぐ糸を描いた紙の模型。
記事のテーマをもとに制作したAI生成イラスト。

マルチプレイゲームを作るとき、「どうやって対戦相手と繋がるか」という仕組みがマッチング機能です。対戦・協力を問わず、複数のプレイヤーを同じゲームセッションに集めるまでの流れ全体を指します。WebSocketでリアルタイム通信を実装できても、マッチング部分を後回しにすると「部屋を作っても誰も来られない」「招待リンクが機能しない」といった問題が起きやすくなります。 マッチングの方式は大きく三つに分かれます。部屋方式(ホストが部屋を立て、参加者がIDで入る)、招待方式(URLやコードを共有して特定の相手を呼ぶ)、ランダムマッチ方式(サーバーが空き待ちのプレイヤー同士を自動で組み合わせる)です。どれを選ぶかで必要なサーバー処理と画面設計が変わります。この記事では三つの方式を実装視点で比較し、それぞれの具体的な構成と失敗しやすい点を整理します。

この記事の目次
  1. 三つの方式をどう選ぶか
  2. 部屋方式の実装構成
  3. 招待方式の追加設計
  4. ランダムマッチの仕組みと注意点
  5. 部屋状態の同期とよくあるバグ
  6. 次の行動
  7. 参考資料

三つの方式をどう選ぶか

方式の選択は「誰と遊ぶか事前にわかっているか」で決まります。

友人と決まった時間に遊ぶなら、部屋方式か招待方式が適しています。部屋方式はホストが部屋を作りIDを口頭やチャットで共有する流れで、実装がシンプルです。招待方式はURLに部屋IDを含めてリンクを踏むだけで入れる形で、SNSで共有しやすい反面、リンクの有効期限や再利用の制御が必要になります。

見知らぬ相手と対戦させたいならランダムマッチが必要です。ただし同時接続者数が少ないゲームでランダムマッチを実装しても「待ち続けて誰も来ない」体験を生みます。リリース直後や小規模なゲームでは、部屋方式をベースにしてランダムマッチは後から追加するか、省略するほうが現実的です。

三方式を同時に提供することも可能ですが、サーバー側の部屋管理ロジックを共通化しないと保守が大変になります。まず部屋方式を基盤として作り、招待はそこにURLパラメータを加える拡張、ランダムマッチはキュー管理を加える拡張、と段階的に構築するのが安全な設計です。

部屋方式の実装構成

部屋方式の最小構成は「部屋の作成・一覧・入室・退室・解散」の五操作です。

サーバー(Node.js + Socketio などを想定)はメモリ上に部屋オブジェクトを持ちます。部屋ごとにroomId(ランダムなID文字列)、hostSocketId、players(配列)、state(waiting / playing / finished)を持たせます。

部屋オブジェクトの例
{
  roomId: "ab12cd",
  hostSocketId: "xxxx",
  players: ["xxxx", "yyyy"],
  maxPlayers: 2,
  state: "waiting"
}

クライアントからの操作はイベント名で区別します。createRoom・joinRoom・leaveRoomの三つが最低限です。createRoomを受け取ったサーバーは新しい部屋を生成してホストをそのSocket.ioルームに追加し、roomCreatedイベントでroomIdを返します。クライアントはこのIDを画面に表示して他のプレイヤーに伝えます。

失敗しやすい点が二つあります。一つ目はホストが切断したときの処理です。ホストのSocketが切断されたとき、部屋を解散するか参加者の誰かにホスト権を移譲するかを決めておかないと、他のプレイヤーが部屋に閉じ込められます。disconnectイベントで必ず部屋の状態をチェックする処理を入れてください。二つ目は満員チェックのタイミングです。joinRoomリクエストが同時に届いたとき、players.length < maxPlayersの確認とpushを非同期で処理するとオーバーフローします。Node.jsはシングルスレッドなので通常は問題になりませんが、外部DBを使う構成ではトランザクションまたはアトミック操作が必要です。

招待方式の追加設計

招待方式は部屋方式の上に「URLで入室できる」機能を乗せたものです。実装の核心はURLパラメータからroomIdを読み取り、自動的にjoinRoomを発行する処理です。

https://example.com/game?room=ab12cd

クライアントはページ読み込み時にURLSearchParamsでroomパラメータを確認し、値があればSocket接続後すぐにjoinRoomを発行します。

javascript
const params = new URLSearchParams(location.search);
const roomId = params.get('room');
if (roomId) {
  socket.emit('joinRoom', { roomId });
}

設計上の判断が必要なのは以下の点です。

  • リンクの有効期限:部屋がplaying状態になった後に招待リンクを踏んだ人をどう扱うか。「入室拒否してエラーを表示する」か「観戦者として参加させる」かを決めます。
  • 部屋IDの推測しにくさ:連番のIDは第三者に推測されやすいです。nanoidなどで8文字以上のランダム文字列を使うのが無難です。
  • 招待コードの二段階:URLを共有しにくい環境(コンソール、音声通話中)向けに、6桁の数字コードでも入室できる仕組みを追加すると利便性が上がります。この場合、サーバー側でroomIdとショートコードの対応テーブルを持ちます。

ランダムマッチの仕組みと注意点

ランダムマッチの基本はマッチングキューです。プレイヤーが「対戦を探す」ボタンを押したらサーバーのキューに追加し、キューに必要人数が揃ったら部屋を自動生成して全員を移動させます。

キューの流れ
1. クライアント → サーバー: enqueueMatch
2. サーバー: queue に追加 → queue.length >= 2 なら
   → 新しい roomId を生成
   → queue から 2 名取り出し joinRoom を発行
   → それぞれに matchFound イベントを送信
3. クライアント: matchFound を受け取り、ゲーム画面へ遷移

キャンセル処理を必ず実装してください。待ち状態でブラウザを閉じたプレイヤーがキューに残り続けると、そのプレイヤーとマッチングした相手が部屋に入れないまま止まります。disconnectイベントでキューから除外する処理と、「キャンセルする」ボタンからのdequeueMatchイベント処理の両方が必要です。

同時接続が少ない状況への対策としてタイムアウト付きマッチングが有効です。一定時間(例:30秒)待っても相手が見つからない場合は「しばらく後に再試行してください」と通知する、あるいはCPU対戦モードに自動的に切り替えるなど、待ちっぱなしにならない設計が重要です。

レーティングマッチ(近いスコアの相手を優先する)を実装する場合は、キューの要素にスコアを持たせ、マッチング時にスコア差が閾値以内の組み合わせを探します。待機時間が長くなるほど閾値を広げるエラスティックマッチングという手法が一般的ですが、小規模なゲームでは単純なFIFOキューで十分です。

部屋状態の同期とよくあるバグ

マッチングに関連するバグの多くは「部屋の状態がサーバーとクライアントでズレる」ことから起きます。よくあるパターンを整理します。

  • ゲーム開始直後に別のプレイヤーが入室できてしまう:stateをplayingに変更するタイミングが遅いか、入室チェックでstateを確認していない。startGameイベントを送る前にstateを更新する。
  • 部屋一覧に空の部屋が残り続ける:全員が退室しても部屋オブジェクトを削除していない。playersが空になったタイミングで部屋を削除するか、定期的にGCする処理を入れる。
  • 再接続時に部屋に戻れない:ブラウザリロードするとSocketIDが変わるため、旧IDで管理していた部屋には戻れません。ユーザーIDと部屋IDの対応を保持し、再接続時に旧IDをキーに部屋へ再参加させる処理が必要です。

次の行動

まず部屋方式の最小実装(作成・入室・退室)を動かし、ホスト切断時の処理を確認してください。それが安定したら、URLパラメータによる招待を追加します。ランダムマッチはキューとキャンセル処理をセットで実装し、タイムアウト時の導線を決めてから組み込むと後戻りが少なくなります。アソバースにZIPで投稿する場合も、サーバー部分を外部ホスティング(RenderやRailwayなど)に置き、クライアントのHTMLから接続する構成が一般的です。

参考資料

  • Socket.io 公式ドキュメント(Rooms セクション)
  • MDN Web Docs — URLSearchParams
  • nanoid ライブラリ公式リポジトリ