multiplayer-sync について
Godot 4.3以降の開発者向けマルチプレイ同期スキルです。MultiplayerSynchronizerによるプロパティ複製、補間、帯域幅調整に加え、クライアント予測や遅延補償も解説します。
主な機能
- 01MultiplayerSynchronizerによるプロパティ複製
- 02フル同期と差分同期
- 03リモート状態の補間
- 04クライアント予測と遅延補償
- 05帯域幅調整と同期方式の選択
活用シーン
- 01プレイヤーの位置、速度、体力の同期
- 02リモートプレイヤーの表示の飛びを抑制
- 03高速なゲームでの予測と命中判定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill multiplayer-syncインストール後、Claude Code で/multiplayer-syncを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/bandwidth-optimization.mdclient-prediction.mdinterpolation.mdlag-compensation.md詳細
Godot 4.3+ でのマルチプレイヤー同期
すべての例は、非推奨 API を使用せず、Godot 4.3+ を対象としています。まず GDScript を示し、次に C# を示します。
関連スキル: ENet のセットアップ、RPC、権限モデルについては multiplayer-basics、ヘッドレスエクスポートとデプロイについては dedicated-server、物理補間と RigidBody の同期については physics-system を参照してください。
1. MultiplayerSynchronizer
MultiplayerSynchronizer は、ネットワーク経由でプロパティを複製する Godot 組み込みのノードです。状態を共有したいノードの子として追加します。
機能
- 設定した間隔で、権限を持つピアから他のすべてのピアへプロパティ値を送信します
- 差分同期(変更時に信頼性を確保して送信する「On Change」プロパティ)と全件同期(間隔ごとに信頼性を確保せず送信する「Always」プロパティ)の両方に対応します
- 更新を受信するピアを制御する可視性フィルターを設定できます
エディターでのレプリケーション設定
- シーンツリーで
MultiplayerSynchronizerノードを選択します。 - インスペクターで Replication を開き、Add Property をクリックします。
- 親ノードのパスとプロパティ名(例:
position、velocity)を選択します。 - プロパティごとに Sync(間隔ごとに送信)または Spawn(スポーン時のみ送信)を設定します。
- Replication Interval(秒)を設定します。
0はネットワークの各プロセスフレームを意味します(マルチプレイヤー API は物理ティックごとではなく、プロセスフレームごとにポーリングします)。
主なプロパティ
| プロパティ | 説明 |
|---|---|
replication_interval | 全件同期の更新間隔(秒)。0(デフォルト)=ネットワークの各プロセスフレーム |
delta_interval | 差分同期の更新間隔(秒)。0(デフォルト)=ネットワークの各プロセスフレーム |
public_visibility | true の場合、更新はすべてのピアに送信されます(デフォルト) |
add_visibility_filter(callable) | プロパティではなくメソッドです。ピア ID を受け取り、そのピアが更新を受信する場合に true を返す Callable を追加します |
差分同期と全件同期
| モード | 動作 | 最適な用途 |
|---|---|---|
| 全件同期 | すべての「Always」プロパティを replication_interval ごとに、信頼性を確保せずに送信します | 継続的に変化する値(位置、速度) |
| 差分同期 | 変更された「On Change」プロパティを delta_interval ごとに、信頼性を確保して送信します | 変化はまれでも、確実に届く必要がある値(体力、状態) |
各プロパティが使用するモードはどちらか一方です。そのため、2 つの間隔は異なるプロパティ群を制御します。replication_interval は「On Change」プロパティの全状態を再送する間隔ではありません。
可視性フィルター(GDScript)
# Only send updates to peers within 500 units of this object.
func _ready() -> void:
$MultiplayerSynchronizer.add_visibility_filter(_is_peer_in_range)
func _is_peer_in_range(peer_id: int) -> bool:
var peer_player := _get_player_node(peer_id)
if peer_player == null:
return false
return global_position.distance_to(peer_player.global_position) <= 500.0
可視性フィルター(C#)
// Only send updates to peers within 500 units of this object.
public override void _Ready()
{
var sync = GetNode<MultiplayerSynchronizer>("MultiplayerSynchronizer");
sync.AddVisibilityFilter(Callable.From<int, bool>(IsPeerInRange));
}
private bool IsPeerInRange(int peerId)
{
var peerPlayer = GetPlayerNode(peerId);
if (peerPlayer is null)
return false;
return GlobalPosition.DistanceTo(peerPlayer.GlobalPosition) <= 500.0f;
}
2. プロパティの同期
同期するもの
リモートピア上で表示を再構築するために必要な最小限の状態を同期します。一般的なプロパティは次のとおりです。
| プロパティ | 型 | 備考 |
|---|---|---|
position | Vector2 / Vector3 | 基本となるトランスフォームです。毎フレーム同期するか、補間を使用します |
velocity | Vector2 / Vector3 | リモート側の予測が、位置のスナップより先行するのに役立ちます |
health | int / float | 変更時に信頼性を確保して同期します。差分同期が最適です |
animation_state | String / int | 変更時に同期します。帯域幅を節約するには enum の int を使用します |
is_crouching | bool | 変更頻度の低いブール値です。差分同期または変更時の RPC を使用します |
同期するプレイヤー(GDScript)
# synced_player.gd
extends CharacterBody2D
## Sync interval in seconds — exposed so designers can tune per object type.
@export var sync_interval: float = 0.05 # 20 Hz
@export var speed: float = 200.0
# These properties are listed in the MultiplayerSynchronizer replication config.
var synced_position: Vector2 = Vector2.ZERO
var synced_velocity: Vector2 = Vector2.ZERO
var synced_health: int = 100
var synced_anim: int = 0 # 0 = idle, 1 = run, 2 = jump
@onready var _sync: MultiplayerSynchronizer = $MultiplayerSynchronizer
func _ready() -> void:
_sync.replication_interval = sync_interval
# Only the authority (owner) drives movement.
set_physics_process(is_multiplayer_authority())
func _physics_process(_delta: float) -> void:
# Authority: write canonical state so MultiplayerSynchronizer can replicate it.
synced_position = global_position
synced_velocity = velocity
synced_anim = _compute_anim_state()
同期するプレイヤー(C#)
// SyncedPlayer.cs
using Godot;
public partial class SyncedPlayer : CharacterBody2D
{
/// <summary>Sync interval in seconds. Exposed so designers can tune per object type.</summary>
[Export] public float SyncInterval { get; set; } = 0.05f; // 20 Hz
[Export] public float Speed { get; set; } = 200.0f;
// These properties are listed in the MultiplayerSynchronizer replication config.
public Vector2 SyncedPosition { get; set; } = Vector2.Zero;
public Vector2 SyncedVelocity { get; set; } = Vector2.Zero;
public int SyncedHealth { get; set; } = 100;
public int SyncedAnim { get; set; } = 0; // 0=idle, 1=run, 2=jump
private MultiplayerSynchronizer _sync = null!;
public override void _Ready()
{
_sync = GetNode<MultiplayerSynchronizer>("MultiplayerSynchronizer");
_sync.ReplicationInterval = SyncInterval;
SetPhysicsProcess(IsMultiplayerAuthority());
}
public override void _PhysicsProcess(double delta)
{
// Authority: write canonical state for replication.
SyncedPosition = GlobalPosition;
SyncedVelocity = Velocity;
SyncedAnim = ComputeAnimState();
}
private int ComputeAnimState()
{
if (!IsOnFloor()) return 2;
return Velocity.Length() > 1f ? 1 : 0;
}
}
3. 補間
MultiplayerSynchronizer は同期間隔(例:30 Hz)で目標プロパティを更新しますが、レンダリングはフレームレート(60+ Hz)で実行されます。補間しない場合、リモートプレイヤーはスナップショット間をテレポートするように見えます。解決策は、synchronized シグナルが発生するたびに前回と今回の位置を保持し、_process で更新後の経過時間を同期間隔で割った値を使って線形補間することです。タイムスタンプ付きのスナップショットバッファー(約 100 ms 遅れて描画する方法)のほうが、より堅牢です。
GDScript と C# による前回/現在のスナップショット補間(
synchronizedシグナル、_process内での経過時間に基づく線形補間)の全例については、references/interpolation.md を参照してください。
4. クライアント側予測
ローカルプレイヤーの応答性を高めるには、クライアント側で移動を即座に予測し、入力をサーバーに送信して、サーバーのスナップショットが届いたら調整します。サーバーの状態がクライアントの予測からしきい値を超えてずれている場合は、位置を即座に合わせます。それ以外の場合は、100~200 ms かけて補正を滑らかに線形補間します。
GDScript + C# での予測と調整のパターン全体(入力リングバッファー、サーバーとの調整、リプレイ)については、references/client-prediction.md を参照してください。
5. ラグ補償
ペースの速いゲームでヒットスキャン武器を使う場合、サーバーがヒットを検証するときに、ワールドの状態をクライアントの表示時刻(now - client_rtt/2 - interp_delay)まで巻き戻し、その時点の履歴状態に対してヒットを判定します。
スナップショット履歴パターン、表示時刻の計算、GDScript + C# でのヒットスキャン検証については、references/lag-compensation.md を参照してください。
6. 状態同期と入力同期
ゲームの要件に合った同期モデルを選択してください。
| 要素 | 状態を同期 | 入力を同期 |
|---|---|---|
| 送信されるもの | 現在のプロパティ値(位置、体力など) | 毎フレームのプレイヤー入力アクション |
| シミュレーションを行う対象 | オーソリティのみ。他のピアは結果を受信 | すべてのピアが同じシミュレーションを実行 |
| 決定性の要件 | なし | あり — 同じ入力からすべてのピアが同一の出力を生成する必要があります |
| 帯域幅 | 大きい — 各同期間隔で状態全体を送信 | 小さい — 毎フレーム小さな入力構造体を送信 |
| 応答性 | 低い — オーソリティを持たないピアは次の同期ティックを待つ | 高い — 決定的であればローカル予測が容易 |
| 複雑さ | 低い — 調整ループは不要 | 高い — 決定的な物理演算、固定小数点演算、またはロックステップが必要 |
| 適した用途 | アクションゲーム、シューター、ほとんどのリアルタイムゲーム | 格闘ゲーム、RTS、ターン制ゲーム、シミュレーションゲーム |
| ラグ補償の必要性 | あり。ヒット判定に必要 | 通常は不要 — すべてのピアが同期している |
ハイブリッド方式(ほとんどのリアルタイムゲーム向け):ローカルプレイヤーのキャラクターは入力を同期して予測を可能にし、その他すべてのオブジェクトとゲームイベントは状態を同期します。
7. 帯域幅の最適化
最適化の手段は 4 つあります。変更されたプロパティのみを同期する(プロパティごとのレプリケーション設定フラグ)、浮動小数点数を量子化する(コンポーネントを浮動小数点数ではなく 16 ビットのセンチメートル単位にする — RPC 引数と同期プロパティでは整数が型バイトに加えて 1/2/4/8 バイトに圧縮されるため、16 ビット整数は 8 バイトの浮動小数点数に対して 3 バイトです)、距離に基づいて同期レートを設定する(遠くのオブジェクトは 5 Hz、近くのオブジェクトは 30 Hz で同期する)、チャネルを選択する(到達が必須の状態変化には信頼性ありを、後続の位置情報で置き換えられる位置ストリームには信頼性なしを使用する)。
各手段の GDScript + C# の完全なレシピと、信頼性あり/なしのチャネル選択フローチャートについては、references/bandwidth-optimization.md を参照してください。
8. 実装チェックリスト
-
MultiplayerSynchronizerは、複製するノードの直接の子になっています - 同期プロパティに書き込むのは オーソリティ ピアだけで、ほかのピアは読み取り専用です
- スポーン時に、正しいピア ID を指定して
set_multiplayer_authority()を呼び出します -
replication_intervalとdelta_intervalは、オブジェクトの更新頻度に合わせて調整されています - リモートプレイヤーのビジュアルには、
_physics_processではなく_processで補間を適用します - 補間では、前回と現在の状態を保持し、前回の更新からの経過時間に応じてブレンドします(ネットワーク更新ではなく物理ティックを追跡する
Engine.get_physics_interpolation_fraction()は使用しません) - クライアント側予測は、ローカルプレイヤー自身のキャラクターにのみ適用します
- 保留中の入力バッファには上限を設定し(最大約 128 ティック)、メモリの増加を防ぎます
- 微小な補正によるジッターを防ぐため、調整しきい値を設定します
- 位置と速度には
unreliableRPC を、状態変更にはreliableRPC を使用します - 位置データをネットワーク経由で送信する前に、浮動小数点数を量子化します
- ラグ補償用のスナップショット履歴は、各ティックで上限のある範囲に収まるよう削除します
- すべてのヒット判定はサーバーが検証し、クライアントがキルを自己申告することはありません
- 距離に基づく同期頻度の調整により、遠くのオブジェクトの帯域幅使用量を削減します