マガジン

オンライン対戦の遅延対策:予測と補間の基礎

遊びをつくる

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

チェスと白黒の石の盤面を青いリボンでつなぎ、招待対戦を表した写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

オンライン対戦ゲームを作るとき、最初に壁として現れるのが「遅延」です。プレイヤーの操作がサーバーを経由して相手の画面に届くまでに、数十ミリ秒から数百ミリ秒のズレが生じます。この時間差をそのまま放置すると、キャラクターがガクガク動いたり、当たり判定がおかしくなったりして、ゲームとして成立しなくなります。 遅延そのものをゼロにすることはできません。光の速さで通信しても、東京とニューヨーク間の往復には物理的に70ミリ秒以上かかります。そのため、現代のオンラインゲームは「遅延を減らす」より「遅延を隠す」技術に重点を置いています。その中核にあるのがクライアント予測(Client-side Prediction)と補間(Interpolation)の二つです。 この記事では、この二つの仕組みがどのように遅延の影響を目立たなくするのか、考え方と実装上の判断基準を順に説明します。ゲームエンジンや言語に依存しない概念として整理しているので、JavaScriptでブラウザゲームを作る場合にも、そのまま応用できます。

この記事の目次
  1. なぜ遅延は見た目の問題になるのか
  2. クライアント予測:サーバーの返答を待たずに動かす
  3. 補間:相手キャラクターの動きをなめらかにする
  4. 外挿(エクストラポレーション)との使い分け
  5. 実装で最初につまずきやすい点
  6. 次の行動
  7. 参考資料

なぜ遅延は見た目の問題になるのか

ネットワーク遅延を何も対策しないまま実装すると、次のことが起きます。プレイヤーが「右キーを押した」という入力をサーバーに送り、サーバーが「右に動いた」という結果を返すまでの間、自分のキャラクターは画面の上で止まったままです。往復遅延(RTT)が200ミリ秒あれば、操作してから結果が見えるまでに0.2秒待つことになります。

人間の知覚として、100ミリ秒を超えると「もっさりしている」と感じ始め、200ミリ秒を超えると操作への反応に明らかな違和感が出ると一般に言われています。アクション系やリアルタイム対戦ゲームでは、この遅延感が致命的な体験の悪化につながります。

問題は二方向に分かれます。一つは「自分の操作が画面に反映されるまでの遅れ」で、もう一つは「相手キャラクターの動きが自分の画面でカクつく」という問題です。前者にはクライアント予測が、後者には補間が有効です。

クライアント予測:サーバーの返答を待たずに動かす

クライアント予測の考え方はシンプルです。「サーバーの許可が届く前に、自分の画面では先に動かしてしまう」ということです。

手順を整理すると次のようになります。

  1. プレイヤーが入力する
  2. 入力をサーバーに送信する(非同期)
  3. 同時に、クライアント側でも入力の結果をローカルに計算して画面を更新する
  4. サーバーから「確定した状態」が届いたら、ローカルの状態と比較する
  5. ずれていた場合は補正する

3番のステップで自分のキャラクターがすぐ動くため、プレイヤーには遅延を感じさせません。ポイントは4番と5番の「サーバー権威モデル」との整合です。クライアントが自由に位置を決められるとチートが成立してしまうため、最終的な状態はサーバーの計算結果が正しいというルールを守ります。

実装で判断が必要になるのは「補正の方法」です。サーバーの確定値とクライアントの予測値に差があったとき、どう扱うかは二択になります。

  • 即時スナップ(瞬間的に正しい位置へ移動)、見た目:キャラクターが瞬間移動して見える、実装の複雑さ:簡単
  • スムーズ補正(差分を数フレームで縮める)、見た目:自然だが少しズレが残る期間がある、実装の複雑さ:やや複雑

軽量なアクションゲームや練習用のプロトタイプなら即時スナップで問題ないことが多いです。プレイヤーが気になるほど差が出るケースは、サーバーロジックとクライアントロジックが食い違っているサインなので、まずそちらを揃えることを優先します。

入力履歴の保持が不可欠な理由

クライアント予測を正しく機能させるには、送信した入力を番号つきで保持しておく必要があります。サーバーは「入力番号N番まで処理した時点の状態」を返してきます。クライアントはその番号より後の入力をローカルで再計算することで、予測値を正確に作り直せます。この仕組みを入力の再シミュレーション(Re-simulation)と呼びます。

入力を捨てずに蓄積しておく量は、想定するRTTに見合った分だけあれば十分です。RTTが最大300ミリ秒で、入力が1秒間に60回なら、最大18フレーム分の入力を保持しておけばカバーできます。

補間:相手キャラクターの動きをなめらかにする

自分の操作感はクライアント予測で解決できますが、相手のキャラクターにはその手法が使えません。相手の次の行動は予測できないからです。

ここで使うのが補間です。考え方は「少し古い状態」を使って表示することです。

サーバーから相手の位置情報が届くタイミングは不規則です。ネットワークのゆらぎ(ジッター)によって、100ミリ秒間隔で来るはずのパケットが、80ミリ秒・120ミリ秒・90ミリ秒・130ミリ秒…と不規則に届きます。これをそのまま画面に反映するとカクカクした動きになります。

補間では、受信した状態をバッファ(一時的なキュー)に蓄えておき、少し前の状態と少し後の状態の間を時間に合わせてなめらかに計算します。表示タイムラインを意図的に100〜200ミリ秒ほど遅らせることで、常に「前後の二点を持った状態」で補間できるようにします。

バッファ量の判断基準

どのくらい遅らせるかはゲームジャンルによって変わります。

  • 格闘ゲームやシューター:遅れが少ないほうが有利なので、バッファは最小限(50〜100ms程度)にして、代わりに予測外れ時のスナップを許容することが多いです。
  • RTS・パズル・ターン制:多少の遅れよりなめらかさを優先できるので、バッファを200ms前後とって補間を丁寧にするほうが自然に見えます。
  • プロトタイプ・カジュアルゲーム:まず100ms固定で試して、実際にプレイしながら調整するのが現実的です。

バッファが短すぎると、パケットが届かないときにバッファが空になり、補間ができずにキャラクターが止まります。長すぎると、相手の動きに対して自分の画面が遅れすぎて対戦にならなくなります。自分のゲームのRTT分布を計測して、ジッターの幅より少し大きいバッファ量を選ぶのが基本です。

外挿(エクストラポレーション)との使い分け

補間と似た言葉に外挿(Extrapolation)があります。補間が「過去の二点の間を埋める」のに対して、外挿は「最新の状態から先を推測して前に出す」手法です。

外挿は遅延をほぼゼロに見せられる利点がある反面、予測が外れたときに大きなズレが発生します。直線運動の多いレースゲームや、動きが予測しやすい物体(弾丸など)には有効ですが、人間の操作するキャラクターに使うと「幽霊のようにすり抜ける」現象が出やすくなります。

実装の判断としては、最初は補間だけで作り、予測可能な要素(物理演算で飛ぶオブジェクトなど)にだけ外挿を部分的に適用するのがリスクの少い進め方です。

実装で最初につまずきやすい点

タイムスタンプの基準をそろえることが最初の落とし穴です。クライアントとサーバーのシステム時刻は微妙にずれていますし、複数のクライアント間での時刻同期も必要です。NTPや独自の時刻同期プロトコルを使って、全員が同じ「ゲーム内時刻」を共有することが前提になります。

状態の定義を一本化することも重要です。「位置」「速度」「向き」「アニメーション状態」など、何を同期する情報とするかをあらかじめ決めておかないと、補間する対象がバラバラになります。同期が必要な状態変数を構造体や型として最初に定義し、そこ以外はローカルにとどめる設計が安全です。

デバッグ用の遅延シミュレーターを開発中に組み込んでおくと、後の調整が格段に楽になります。送受信に人工的な遅延やパケットロスを加えられるようにしておくと、本番環境に近い条件でローカル開発ができます。

次の行動

まずは「補間だけ」のシンプルな実装から始め、相手キャラクターの動きがなめらかになることを確認してください。次のステップとして自分のキャラクターにクライアント予測を加え、入力の即時反映と補正の両方を体験するのが理解を深める近道です。ブラウザゲームであれば、WebSocketとrequestAnimationFrameの組み合わせでこれらの仕組みを試せます。作ったゲームが動くようになったら、アソバースにZIPファイルやHTMLファイルとして投稿して実際のプレイヤーに触れてもらい、体感遅延のフィードバックを集めることをお勧めします。

参考資料

  • Gabriel Gambetta「Fast-Paced Multiplayer」シリーズ(個人ブログ)
  • Valve Developer Community「Source Multiplayer Networking」
  • Glenn Fiedler「Networked Physics」シリーズ(個人ブログ)
  • MDN Web Docs「WebSocket API」