決定的ロックステップ(RTS)
数百ユニットを低帯域で同期したいRTS開発者へ。入力だけを送り全員が同じ計算を回す決定的ロックステップなら、通信量を劇的に下げつつ完全一致の対戦を実現できます。落とし穴の浮動小数点も押さえます。
- 決定的ロックステップは状態ではなく入力(コマンド)だけを全員に配り、各クライアントが同一の初期状態から同一の決定的シミュレーションを回して同じ結果に到達する。ユニット数に依存しない一定・低帯域が最大の利点。
- 全端末でビット単位に一致させる必要があるため、浮動小数点の非決定性・イテレーション順序・乱数シードが致命的。固定小数点や厳密なコード規律で決定性を担保し、遅延ステップ(入力遅延)で回線遅延を吸収する。
- 各ターンの状態からチェックサムを取り相互比較して発散(desync)を即検出する。1端末でもズレたら全体が破綻するため、検出・再現・防止の設計が実装の核心。
状態を送らず「入力」だけを送るという発想
横にスクロール
『StarCraft』『Age of Empires』のような RTS(リアルタイムストラテジー)は、画面上に数百〜数千のユニットが存在します。もし各ユニットの位置・体力・状態を毎フレーム全プレイヤーへ送信すれば、帯域はユニット数に比例して膨れ上がり、大規模な戦闘ほど破綻します。決定的ロックステップ(deterministic lockstep)は、この問題を発想の転換で回避します。同期するのはゲーム状態ではなく、プレイヤーの入力(コマンド)だけです。
前提は単純かつ強力です。全クライアントがまったく同じ初期状態から出発し、まったく同じ入力列を、まったく同じ順序で、まったく同じ決定的なシミュレーションに食わせれば、途中経過も最終結果も完全に一致する——という決定論の性質を使います。送るべきは「プレイヤー1が座標(x,y)へ移動を命じた」といった数十バイトのコマンドだけで、その結果ユニットが実際にどう動くかは各端末が自分で計算します。帯域はユニット数に依存せず、プレイヤーが発行するコマンド数だけに比例するため、8人対戦で数千ユニットが乱戦しても通信量はごくわずかで済みます。
状態同期(state synchronization、FPS などで主流)は、権威サーバーが真の状態を計算し、その結果(位置・向き・HP など)をクライアントへ配信します。帯域はエンティティ数に比例して増える代わりに、各クライアントは計算をほぼしません。ロックステップは逆で、入力だけを配り計算は全員が各自で行う。帯域はエンティティ数から独立する一方、全端末のシミュレーションがビット単位で一致していなければ成立しません。RTS が前者でなく後者を選ぶのは、まさに「大量ユニット × 低帯域」という要求に決定的に向いているからです。ネットワーク同期モデルの選択についてはネットワークのトップも参照。
決定性という厳しい制約:ビット単位で一致させる
ロックステップの利点はすべて「全クライアントのシミュレーションが完全に一致する」という前提に乗っています。逆に言えば、どこか1台でも計算結果が1ビットでもズレた瞬間、以後の予測は雪だるま式に発散し、対戦は崩壊します。この状態を desync(同期ズレ)と呼びます。したがってロックステップ実装の本質は、ネットワークコードそのものよりも「シミュレーションを完全に決定的に保つ」ことにあります。決定性を壊す主な要因は次のとおりです。
- 浮動小数点演算: 同じ式でも、CPU アーキテクチャ・コンパイラ・最適化レベル・x87 と SSE の違い・FMA(積和融合)の有無・命令の並べ替えによって、丸め結果の最下位ビットが変わりうる。これがロックステップ最大の敵。
- イテレーション順序: ハッシュマップやセットを走査する順序が端末間で異なると、ユニットの処理順が変わり結果がズレる。順序が保証されたコンテナ(挿入順・ID 順)を使う必要がある。
- 乱数: 各端末が独立に乱数を引くと当然ズレる。シードを共有した決定的な擬似乱数生成器を全端末で同一手順で回す。
- 未初期化メモリ・ポインタ値・アドレス依存: 実行ごとに変わる値を計算やソートキーに使うと発散する。
IEEE 754 の四則演算自体は、同じ丸めモード・同じオペランドなら結果が一意に定まる規格です。問題はそこに至るまでの経路にあります。コンパイラは a*b + c を FMA 命令1つに融合するか、乗算と加算の2命令に分けるかを最適化で勝手に選び、両者は丸め回数が違うため最下位ビットがずれます。x87 の80ビット内部精度と SSE の64ビットでも結果が変わります。超越関数(sin, cos, sqrt)はプラットフォームのライブラリ実装ごとに最終ビットが異なることさえある。同一バイナリを全員が走らせるなら偶然一致することもありますが、Windows と macOS、あるいは異なる CPU 世代をまたぐと途端に破綻します。だからこそ RTS の多くは浮動小数点を避け、固定小数点(整数)演算でシミュレーションを組みます。
決定性を担保する代表的な戦略が固定小数点演算です。座標や速度を「実数」ではなく「1単位=1/1000 のような固定スケールの整数」で表し、加減乗除をすべて整数演算に落とします。整数演算はどのプラットフォームでも結果が完全に一致するため、浮動小数点の非決定性を根本から排除できます。平方根や三角関数も、整数入力・整数出力の自前ルックアップテーブルや近似で実装し、標準ライブラリに依存しないのが定石です。
遅延ステップ:回線遅延をどう吸収するか
ロックステップの2つ目の課題は遅延です。あるターンのシミュレーションを進めるには、その ターンに属する全プレイヤーの入力が到着していなければなりません。誰か1人のパケットが遅れると、全員がその人を待って停止する——これが素朴な同時ロックステップの弱点で、体感は最も遅いプレイヤーの回線に律速されます。
これを緩和するのが入力遅延(input delay)=遅延ステップです。プレイヤーが今フレーム N で出したコマンドを、その場では実行せず、N + d フレームで実行すると全員が約束します(例えば d = 3)。こうすると各コマンドには送信から実行まで d フレーム分の猶予が生まれ、その間にパケットが全端末へ届けばよいことになります。d はラウンドトリップ遅延(RTT)を吸収できる大きさに設定します。
入力遅延つきロックステップの1フレーム:
1. 今フレーム N の自分の入力を採取し、実行予定フレーム N+d を
付けて全プレイヤーへ送信する
2. 実行すべきフレーム N の入力(= d フレーム前に配られた
全員のコマンド)が揃っているか確認する
- 揃っている → フレーム N のシミュレーションを1ステップ進める
- 未着がある → stall(全員が待つ/描画は補間で滑らかに見せる)
3. フレーム N の状態からチェックサムを計算し、比較用に共有する
4. N ← N+1 へ進む
d を大きく取るほど遅い回線でも stall しにくくなりますが、そのぶんコマンドの反応が鈍くなります(クリックしてから d フレーム後に動く)。RTS で入力遅延が許容されやすいのは、ユニットへの命令が本質的に「これから向かえ」という予約的な操作で、数フレームの遅れが操作感を大きく損なわないからです。FPS のように自機の照準が即時に反応してほしいジャンルでは、この固定遅延は受け入れがたく、状態同期+クライアント予測が選ばれます。ジャンルの操作特性が同期方式の選択を決めるということです。適応的に d を回線状況へ合わせて増減させる実装や、局所入力だけ先行描画して見かけの遅延を隠す工夫も併用されます。
同期チェックサム:発散を即座に捕まえる
決定性は「壊れていないこと」を祈るものではなく、継続的に検証するものです。そのために各ターンごとに、そのフレームのゲーム状態全体(全ユニットの座標・体力・乱数生成器の内部状態など)を1つの値へ畳み込んだチェックサム(同期ハッシュ)を計算し、全プレイヤーで突き合わせます。あるフレームでチェックサムが1人でも他と食い違えば、その瞬間に desync が起きたと確定できます。
同期チェックサムの役割:
・毎フレーム(または一定間隔で)状態をハッシュ化して交換
・値が全員一致 → シミュレーションは同一の軌道上にある
・値が食い違う → その端末が発散。どのフレームで壊れたかが判明する
・デバッグ時は状態をより細かく(変数単位で)記録し、
壊れた最初のフレームを二分探索で特定してバグ源を絞り込む
チェックサムの真価はデバッグにあります。desync は再現が難しい最悪級のバグですが、「どのフレームで初めてズレたか」が分かれば、その1フレーム前後だけを詳細ログと突き合わせ、原因(順序依存のコンテナ走査、うっかり混入した浮動小数点、共有し忘れた乱数引き)を追い詰められます。運用面では、desync を検出したら即座にゲームを停止しリプレイやログを保存する設計にしておくと、事後解析が現実的になります。
決定的ロックステップの副産物として、入力列だけを保存すればリプレイが完全再現できるという強力な性質が得られます。初期状態+全コマンド列を再投入すれば、同じシミュレーションが同じ結果を再生するからです。リプレイファイルが数百 KB と極端に小さいのはこのためです。ただしこれは諸刃の剣で、シミュレーションのロジックを少しでも変更すると、過去のリプレイは再生不能になります。ゲームのパッチでユニットの挙動を1つ調整しただけで、旧バージョンのリプレイやオンライン対戦の互換が壊れる。だからロックステップ採用タイトルは、シミュレーションのバージョンを厳密に管理し、異なるバージョン間の対戦を禁止します。
まとめ:使い分けの判断軸
| 観点 | 決定的ロックステップ | 状態同期+クライアント予測 |
|---|---|---|
| 同期する対象 | 入力(コマンド)のみ | 権威が計算した状態を配信 |
| 帯域の増え方 | コマンド数に比例(ユニット数に非依存) | エンティティ数に比例 |
| 計算負荷 | 全クライアントが全シミュレーションを実行 | 権威サーバー中心、クライアントは軽い |
| 決定性の要求 | ビット単位一致が必須(浮動小数点が鬼門) | 不要(状態を配るだけ) |
| 遅延の扱い | 入力遅延で吸収、全員が待つと stall | 予測+補正で自機は即応 |
| 向くジャンル | 大量ユニットのRTS・シミュレーション | FPS・アクションなど即応が要る対戦 |
| リプレイ | 入力列だけで完全再現・極小サイズ | 別途状態記録が必要 |
- 本質: 状態ではなく入力だけを同期し、全クライアントが同一初期状態から同一の決定的シミュレーションを回して同じ結果に到達する。帯域がユニット数から独立する点が RTS に最適。
- 決定性が生命線: 1端末でも1ビットずれると desync が発散して破綻する。浮動小数点(FMA・x87/SSE 差・超越関数)、コンテナ走査順、乱数シードが三大要因。固定小数点演算での実装が定石。
- 遅延ステップ(入力遅延): 今フレームの入力を
dフレーム後に実行すると約束し、その猶予でパケット到着を待つ。dを大きくすると stall は減るが操作反応が鈍る。 - 同期チェックサム: 毎フレーム状態をハッシュ化して相互比較し、desync を発生フレーム単位で即検出。二分探索でバグ源を特定するデバッグ基盤にもなる。
- 副次効果: 入力列だけでリプレイが完全再現でき極小サイズ。反面シミュレーション変更で旧リプレイ・旧バージョン対戦が壊れるため、バージョン厳密管理が必須。
決定的ロックステップは、大量のユニットを低帯域で同期するという RTS 固有の要求に対する、極めて合理的な解です。入力だけを配り計算は全員が各自で回す——この単純な原理が、通信量をユニット数から解き放ちます。代償として全端末のシミュレーションを完全に決定的へ保つ厳しい規律が求められ、浮動小数点の非決定性を固定小数点で封じ、回線遅延を入力遅延で吸収し、同期チェックサムで発散を常時監視する。この三点セットを設計に織り込めるかどうかが、ロックステップを成立させる分水嶺です。GPU 側の描画やアニメーションは非決定的で構わず、決定性を要求されるのはあくまでゲームロジック(シミュレーション)に限られる——描画と計算を明確に分離する設計思想は、グラフィックスのトップで扱う描画パイプラインの考え方とも接続します。
ゲーム開発の記事ガイド
決定的ロックステップ(RTS)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ゲーム開発
比較で見る軸
難易度: advanced / カテゴリ: ゲーム開発 / タグ数: 5
導入後に効く点
全端末でビット単位に一致させる必要があるため、浮動小数点の非決定性・イテレーション順序・乱数シードが致命的。固定小数点や厳密なコード規律で決定性を担保し、遅延ステップ(入力遅延)で回線遅延を吸収する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ゲーム開発
- タグ数
- 5
判断チェックリスト
- 自社の用途が「ゲーム開発 / ネットワーク同期」に近いか確認する。
- 強みである「決定的ロックステップは状態ではなく入力(コマンド)だけを全員に配り、各クライアントが同一の初期状態から同一の決定的シミュレーションを回して同じ結果に到達する。ユニット数に依存しない一定・低帯域が最大の利点。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。