WebSocketでリアルタイム対戦を作る最小構成
遊びをつくる執筆にClaudeを使用しています。

「WebSocketを使えばリアルタイム対戦ができる」とわかっていても、何をどこまで用意すればゲームとして動くのか、迷う人は多いです。サーバーのコードは何行必要か、クライアント側はどう書くか、マッチメイキングはどう実装するか——調べると情報が多すぎて、最小限の構成が見えなくなりがちです。 この記事では「2人が同じ画面で状態を共有しながら交互に操作する」という対戦ゲームの最小形を目標に置きます。オセロや○×ゲームのような「ターン制・盤面共有」を例として使い、サーバー・クライアントそれぞれに何が必要かを一つずつ説明します。派手な機能は後回しにして、まず動く状態を作るための判断基準を示します。
WebSocketが必要な場面と不要な場面
最初に確認しておきたいのは、WebSocketが本当に必要かどうかです。ターン制の対戦なら、HTTPのポーリング(一定間隔でサーバーに問い合わせる方式)でも動くことがあります。ただし、応答の遅延が200〜500ミリ秒単位になるため、操作の感触が悪くなります。WebSocketは一度接続を確立したあとはサーバーとクライアントが双方向に即座にメッセージを送り合えるため、「相手の操作結果をすぐ反映したい」場面に向いています。
判断基準をまとめると次のとおりです。
- ポーリングで十分なケース:数秒に一度の状態確認でよい非同期型、または同期が不要なランキング更新など
- WebSocketが必要なケース:相手の操作を100ミリ秒以内に反映したいリアルタイム対戦、チャット、協力プレイ
ターン制対戦でも「早押しや制限時間のあるゲーム」はWebSocketが有利です。純粋な非同期ターン制(将棋の通信対局のように数分単位で指す)であれば、HTTPだけで実装するほうが構成が単純になる場合もあります。
最小構成に必要な3つの要素
リアルタイム対戦の最小構成は、以下の3要素で成り立ちます。
サーバー:接続管理と状態の配布
サーバーの役割は「誰が接続しているかを管理し、一方から届いたメッセージをもう一方へ転送する」ことです。Node.jsとwsライブラリを使った最小実装は、おおよそ次の流れです。
- サーバー起動時にWebSocketServerを立ち上げる
- 接続してきたクライアントをルーム(部屋)に割り当てる
- ルームに2人揃ったらゲーム開始を双方に通知する
- 一方から受け取ったメッセージ(操作内容)を検証し、もう一方へ転送する
- 切断時はルームを破棄してもう一方に通知する
サーバー側でゲームの状態(盤面データ)を持つかどうかは設計の岐路です。最小構成では「サーバーは状態を保持せず、クライアントの操作をそのまま中継するだけ」にすることもできます。ただし、この場合はチートへの耐性がなくなります。「誰が勝ったかをサーバーが判定する」必要がある対戦ゲームなら、盤面状態はサーバー側で管理する設計にします。最初の実装ではまず中継だけに絞り、動いたあとで状態管理をサーバーへ移す順番が現実的です。
クライアント:接続・送信・受信の3動作
ブラウザのWebSocket APIはシンプルです。new WebSocket(url)で接続し、ws.send()で送り、ws.onmessageで受け取ります。対戦ゲームのクライアント側に最低限必要な処理は次のとおりです。
- 接続確立:ページ読み込み時またはボタン押下時に接続し、自分のプレイヤーIDを受け取る
- 操作の送信:盤面をクリックしたときに{ type: "move", row: 2, col: 3 }のようなJSONを送る
- 受信と反映:onmessageで受け取ったデータをパースして盤面を再描画する
- 切断の検知:oncloseでサーバーまたは相手の切断を検知してUIに表示する
送受信するデータ形式はJSON.stringify()とJSON.parse()で扱うJSONが最も素直です。バイナリ形式(MessagePackなど)にすると帯域は減りますが、デバッグが難しくなるため最小構成では避けます。
マッチメイキング:最小形は「先着2名をペアにする」だけ
マッチメイキングは複雑にしようとすると際限がなくなります。最小構成で十分な実装は「接続してきた順に2人をペアにする」だけです。サーバー側で待機中のクライアントを配列で管理し、2人目が来た瞬間にルームを作って両者に通知します。
レーティングや地域によるマッチング、観戦機能は後から追加できる要素です。まず「2人が同じ部屋に入って操作できる」状態を作ることを優先します。
陥りやすい問題と対処
JSON送信のタイミング問題:接続が確立する前にsend()を呼ぶとエラーになります。ws.readyState === WebSocket.OPENを確認してから送るか、onopen内で初期送信を行うことで回避できます。
相手の切断への対処:ブラウザを閉じた場合、oncloseイベントが発火するまでにタイムラグがあります。サーバー側でping/pongを定期送信してクライアントの生存確認を行い、応答がなければ強制切断扱いにするとゲームが止まりにくくなります。wsライブラリにはclientTrackingとping送信のオプションがあります。
状態の不整合:クライアント側で盤面状態を持っている場合、ネットワーク遅延で操作順が前後することがあります。最も単純な対処は「自分のターン以外の操作を無視する」処理をサーバー側に持たせることです。どちらのターンかをサーバーが管理し、違反操作はそのまま捨てることでクライアント間の状態を揃えられます。
ローカル開発とデプロイの差:ローカルではws://localhost:3000で動いても、HTTPSで配信するサーバーにデプロイするとwss://(TLS付き)が必要になります。ブラウザのMixed Content制限により、HTTPSのページからWSへの接続はブロックされます。デプロイ先のサービス(RailwayやRenderなど)がWSSに対応しているかを事前に確認します。
デプロイと公開の判断基準
WebSocketサーバーは「常時起動しているプロセス」が必要なため、静的ファイルホスティングだけでは動きません。Node.jsプロセスを動かせるサービスが必要です。無料枠で試せるサービスはいくつかありますが、スリープ機能(一定時間アクセスがないとプロセスを停止する)がある場合、最初の接続に数秒かかることがあります。常時起動が必要かどうかはゲームの用途に合わせて判断し、料金や制限は各サービスの公式サイトで最新の条件を確認します。
ゲームをHTMLファイルとしてアソバースに投稿する場合、WebSocketサーバーは別途外部に用意する必要があります。HTMLファイル側にサーバーのURLを書き込み、外部サーバーと通信する形になります。サーバーの維持・管理は作者自身の責任範囲です。
次の行動
wsライブラリの公式ドキュメントを参照しながら、まず2人のクライアントがメッセージを送り合えるだけの最小サーバーを手元で動かしてみてください。盤面描画よりも先に「接続・送信・受信の3動作が確認できる状態」を作ることで、問題の切り分けが格段に楽になります。動いたゲームはZIPにまとめてアソバースへ投稿し、URLを共有して実際の対戦で遅延や切断の挙動を確かめてみてください。
参考資料
- MDN Web Docs「WebSocket API」
- ws(npm)公式ドキュメント
- WHATWG「The WebSocket Protocol」仕様



