Logic High & Low マッチング実装(Singleton Matchmaker DO 方式)【CFW P2P心理戦ゲーム開発記】 #12

前回までの数回 Protcol-99のゲームハブの主要部分についての実装を行い、前回でログインまでの仕組みの大枠が完成しました。

今回からは、ハイアンドローゲーム( Logic High & Lowという名前になりそうです)の本実装に入っていこうと思います。

まず手始めに、レート肝となるマッチング方式を、高速かつ安定的に接続する方法として、 Singleton Matchmaker DO 方式を採用し実装していきます。

初めに

オンライン対戦ゲームにおいて、「いかにプレイヤー同士を速く、かつ正確に引き合わせるか」はユーザー体験(UX)の根幹を成します。 ここまでのLogic High and Lowのマッチング機構は、Cloudflare D1(SQLite)を「掲示板」として利用するポーリング方式で仮実装していました。

しかし、より実践的でスケーラブルなマッチングを追求した結果、Durable Objects(DO)を「ステートフルな受付係(Matchmaker)」として起用するアーキテクチャへと舵を切ることとした。

本稿では、なぜD1からDOへ移行したのか、そして「Matchmaker DO」とは一体どのような仕組みなのか、実装の全体フローとともに解説します。

1. これまでの課題:D1「掲示板」方式

これまでの仮実装では、D1データベース内に match_queue テーブルを作成し、待機中のプレイヤーをレコードとして保存する方式で動かしていました。 プレイヤーは数秒おきに GET /api/match/status を叩いて(ポーリング)、自分の部屋に誰かが入ってきたかを確認するという方法です。これは、WEBマッチの王道手法の1つですが、今回のゲームのマッチングのような比較的短いマッチを高頻度で行う場合、以下のような課題があります。

D1の課題

  • データベースへの異常な負荷(コスト) 100人が1分間、2秒おきにポーリングを行うだけで、D1に対して「3,000回」ものReadリクエストが発生する。ステートレスなサーバーレス環境において、無駄なDBアクセスはシステム全体のボトルネックとなる。
  • レースコンディション(競合)の処理 「全く同時に2人のプレイヤーが1つの待機部屋を見つけた」場合、楽観的ロックなどを用いてDB側で厳密に弾く必要があり、ロジックが複雑化する。
  • 切断検知の遅延 プレイヤーがブラウザを閉じて離脱しても、DB上のレコードはタイムアウト処理が走るまで「待機中」として残り続けてしまう。

2. 「Matchmaker DO(受付係)」とは?

これらの課題を一掃するモダンな解決策が、「Matchmaker DO(マッチング専用 Durable Object)」です。
端的に言うと、まず受付係に行きその後、受付側でマッチができたらゲーム(DO)に移行する方式です。

Lobby Service (ユーザはとりあえずロビーに入る)
→ Match Server (ロビーでマッチできたらゲーム開始)

この方法は20年前から使われており、身近な例だと(詳細は非公開にされていますが動作的に)スプラトゥーンのマッチ方法、ロビーに入り、8人集まるまで待つ(ここまではlobby)、その後、準備できたらゲームへ移行する(Match Server)というようなものがあります。

Durable Objectsは、Cloudflareのエッジネットワーク上に「状態(State)を保持し続けることができる単一のインスタンス(Singleton)」を立ち上げる機能です。 D1(ディスク)に待機列を書き込むのをやめ、このMatchmaker DOの「メモリ上の配列(Array)」に待機プレイヤーを格納し、WebSocketで繋ぎっぱなしにします。一般的な大規模なLobbyサーバでは、ここを並列化や細分化しているようですが、そこまで大規模ではないのでシングルトンで行こうと思います。

アーキテクチャのメリット

  1. DB負荷が完全ゼロ: マッチングの計算はすべてDOのメモリ(RAM)上で行われるため、超高速かつD1の課金・制限枠を一切消費しない。
  2. プッシュ通知による「ゼロ秒」マッチング: ポーリングを廃止。相手が見つかった瞬間に、サーバー(DO)側からクライアントへ room_id がプッシュ送信されるため、ラグが存在しない。
  3. 離脱の即時検知: クライアントのWebSocketが切断された瞬間、DOの webSocketClose イベントが発火するため、待機列の配列から即座に削除できる。ゾンビ部屋が一切発生しない。

3. 実装の全体フロー(受付から試合会場へ)

実際のシステムでは、この「受付係(Matchmaker DO)」と、実際にゲームをプレイする「試合会場(GameLobby DO)」の2つのDOが連携して動作します。

フェーズ1:受付係への接続(エントリー)

プレイヤーが「マッチング開始」ボタンを押すと、クライアントは LHL_Matchmaker DOに対してWebSocket接続を確立し、希望するモードと自身のステータスを送信する。

  • 送信データ例:{ "action": "join", "mode": "rate", "rating": 1500 }
  • 受付係はこれをメモリ内の waitingPlayers 配列にプッシュする。

フェーズ2:マッチングの評価(Tick処理)

受付係(DO)は、新しく人が列に並んだ瞬間、または1秒おきの定期処理で、配列内のプレイヤー同士の条件を評価する。

  • 過疎対策アルゴリズム: 「待機時間が長いプレイヤーほど、許容するレート差(検索範囲)を広げる」という計算を、重いSQLではなくTypeScriptのシンプルな関数として実行する。

フェーズ3:試合会場の割り当て(ディスパッチ)

条件に合致する2名(Player A と Player B)が見つかった瞬間、受付係はランダムなユニークID(試合会場のルームID)を生成する。 そして、AとBの両方のWebSocketに対して、以下のメッセージをプッシュ送信する。

  • プッシュデータ例:{ "action": "matched", "roomId": "lhl_room_xyz999" }
  • 送信後、受付係は2人を待機列(配列)から削除する。

フェーズ4:試合会場(GameLobby)への遷移

メッセージを受け取ったクライアント(ブラウザ)は、直ちに受付係とのWebSocket通信を切断する。 そのまま流れるように、指示された roomId を持つ LHL_GameLobby DOに対して新しいWebSocket接続を行い、ゲーム本編のシグナリング(WebRTC接続)を開始する。

4. 実装結果

マッチ作成からゲームまでを試してみました。

ロビーへの接続とマッチング成功

ロビー接続からマッチまでの動画(gif)

まとめ

DBへのポーリングから「インメモリキュー + イベント駆動(WebSocket)」へ移行することで、商業レベルの対戦ゲームと遜色ない、極めて効率的でスケーラブルなマッチング基盤を構築できました。

関連記事
CloudFlare Workersセットアップ
Cloudflare Workersの始め方:Wranglerによるローカル環境構築と世界公開の手順
【第4回】ユーザデータベース実装
D1データベース実装:ユーザデータ管理 【CFW P2P心理戦ゲーム開発記】 #4
【第9回】ゲームデザインとアイテム実装
ゲームルールを考える:ハイアンドローモックアップ【CFW P2P心理戦ゲーム開発記】 #9

次回予告

次回は、第9回モックアップで考えたゲームルールに加えて、セット先取性などを導入しつつ、遊べるゲームに仕上げていきたいと思います。
次回:関連記事は、2026年6月23日に公開予定 (あと30分)

ここまで読んでいただきありがとうございます。

では、次の記事で。 lumenHero