マルチプレイのゲームを作っていて、**「自分の画面では敵を倒したのに、相手の画面ではまだ生きている」**といった同期ズレに悩んだことはありませんか?
今回は、大規模なシミュレーションや格闘ゲームなどで使われる、データの重さに左右されず、かつ「絶対に計算がズレない」ための**決定論的同期(Deterministic Synchronization)**の実装エッセンスを解説します。
1. 「今の座標」を送るのをやめてみる
普通、マルチプレイを実装しようとすると「A君は今座標(100, 200)にいる」という情報を常に送ろうとします。しかし、これには弱点があります。
- 通信が重い: オブジェクトが数千、数万と増えると、全座標を送るだけで回線がパンクします。
- ラグの影響: 座標が届いたときには、相手はもう別の場所にいます。
そこで、発想を逆転させます。「データそのもの」ではなく、「変化のきっかけ(入力)」だけを共有するのです。
2. 絶対にズレないための「3つの鉄則」
全員の画面で全く同じ結果を再現するには、プログラムから「曖昧さ」を徹底的に排除する必要があります。
- 共通の時計(Tick)を持つ: ネットワーク全員で「今は第100拍目」という時間の単位を完璧に合わせます。
-
Math.random()の禁止: 端末ごとに違う結果が出る乱数は使えません。共通の「シード値」から計算される独自の乱数を使います。 -
整数で計算する:
0.1 + 0.2のような小数点計算は、環境によってごく稀に結果がズレます。1ビットの狂いも許さないよう、すべて「整数」で計算します。
3. 実装サンプル:決定論的な状態更新
「同じタイミングで、同じ入力があれば、結果は必ず同じになる」というコードの書き方を見てみましょう。
悪い例(環境によってズレる)
function updateEntity(entity) {
// Math.random() は端末ごとに結果が違うのでNG!
if (Math.random() > 0.5) {
entity.x += 1.5; // 小数点計算も環境によって微差が出る可能性がある
}
}
良い例(どこでも同じ結果になる)
// 共通のシード値から計算される乱数生成器(簡易版)
class DeterministicRNG {
constructor(seed) { this.seed = seed; }
next() {
this.seed = (this.seed * 1103515245 + 12345) & 0x7FFFFFFF;
return this.seed;
}
}
// 決定論的な更新ロジック
function updateState(state, inputs, tick) {
const rng = new DeterministicRNG(state.baseSeed + tick); // Tickをシードに混ぜる
// 1. 全員に届いた「そのTickの入力」を処理
inputs.forEach(input => {
if (input.type === 'MOVE_LEFT') state.x -= 100; // 整数で計算
if (input.type === 'MOVE_RIGHT') state.x += 100;
});
// 2. 共通の乱数で演出などを計算
if (rng.next() % 100 < 50) {
state.energy += 10;
}
}
4. 「共通の楽譜」を演奏するイメージ
この仕組みを例えるなら、**「演奏データ(録音)を送り合う」のではなく、「楽譜を全員に渡して、せーので演奏してもらう」**ようなものです。
- 絶対Tick: 全員の時計を「第100拍目」という単位で合わせます。
- アクション: 「第100拍目に、Aさんがボタンを押した」という最小限の情報だけを共有します。
もし通信が遅れて「過去の入力」が届いた場合は、一瞬だけ過去に巻き戻って計算し直し、現在の時間まで猛スピードで再計算(ロールバック)して追いつきます。
まとめ:データではなく「ルール」を同期させる
「状態」を送り合うのはビデオ動画を共有するようなものですが、「入力とルール」を同期させるのは**「数学の証明を共有する」**ような、非常にスマートな解決策です。
- 通信量は最小限(ボタン操作の数バイトだけ)
- 計算は完璧に一致(整数の決定論)
- 宇宙規模の多人数接続も可能にする
この考え方を取り入れると、ネットワークプログラミングの難易度は上がりますが、その先には「絶対にズレない理想の世界」が待っています。
用語解説
- 決定論(Determinism): 同じ入力・同じ状態なら、必ず同じ結果が出ること。
- 入力同期(Input Sync): 座標データではなく、操作ログを同期する手法。
- シード値(Seed): 乱数のパターンを決定する「種」。これさえ合っていれば、全員が同じ乱数の列を得られます。