mcpField
S
scenario-godot-multiplayer

scenario-godot-multiplayer

github.com/scenario-labs開発1.2k154 回インストール1 日前に更新
リポジトリ

scenario-godot-multiplayer について

Godot 4.7.2 のマルチプレイヤーネットワーク開発向けスキルです。権限とレプリケーションを設計し、複数プロセスや遅延を加えたテスト、ブラウザクライアントで実測検証します。

主な機能

  1. 01オブジェクト単位の権限設計
  2. 02シーンのレプリケーション設定生成
  3. 03複数クライアントでのセッションテスト
  4. 04遅延・ジッター・損失の注入
  5. 05レプリケーション帯域の測定
  6. 06WebSocket と専用サーバーの検証

活用シーン

  1. 01クライアント RPC のサーバー検証確認
  2. 02途中参加とプレイヤー退出のテスト
  3. 03移動に対するネットワーク遅延の評価
  4. 04マルチプレイヤーの帯域見積もり

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/scenario-labs/skills --skill scenario-godot-multiplayer

インストール後、Claude Code で/scenario-godot-multiplayerを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
critique.md
expert-notes.md
gui-paths.md
procedures.md
sources.md
scripts/
gd_net.py
scripts/agentkit/multiplayer/
net_audit.gd
net_boot.gd
net_build.gd
net_input.gd
net_lab.gd
net_player.gd
net_prop.gd
net_session_job.gd
net_util.gd

詳細

Godot マルチプレイヤー(ネットワークプログラマー)

対象:Godot 4.7.2、ENet および WebSocket 上の SceneMultiplayer。

ここでいうエキスパートレベルとは、誰かがプレイする前に、複数のプロセスを使い、遅延を注入した環境でネットワークゲームの動作が実証されていることを意味します。オブジェクトごとに権限を明文化し、クライアントからのすべてのメッセージをサーバーが検証するリクエストとして扱い、レプリケーションのコストを KiB/s 単位で測定します。このスキルでは、コードでレプリケーションを構築し、localhost 上でサーバーと 2~3 台のクライアントをヘッドレスで実行し(ブラウザクライアントも使用)、それらの実行結果の数値だけで合格を判定します。ここに記載された兄弟スキルが利用可能なスキルにない場合は、ユーザーにインストールを依頼してください(npx skills add scenario-labs/skills --skill <name>)。無人で作業を進める場合は、ツールのスキーマに基づいて続行し、スキルが不足していることを明記してください。

必須の背景知識: scenario-godot-expert(チャンネル、gd_run、レビューの反復、4.7 の注意点)。

ステータス(2026-10-02): ライブテスト 16 件(クライアント最大 3 台、ENet、WebSocket、エクスポートしたサーバー、ヘッドレス Chrome)とオフラインテスト 18 件に合格しています。証跡:tests/live_evidence/godot-multiplayer/。

方針(専門家の知見による差分)

  1. コードを書く前に、オブジェクトごとに権限を決めます。 各オブジェクトのデータを誰が管理するのかを明確にします (AndrooDev、jff9oxO8v1s [00:04:01])。気軽な協力プレイなら、ボディの権限をクライアントに持たせても問題ありません。 対戦ゲームやチートされやすいゲームでは、ボディをサーバー側に置き、クライアントには入力のみを渡します (Tamas Galffy、iP_xdJ0peFo [00:03:46, 00:06:47];BatteryAcidDev、GqHTNmRspjU [00:00:33])。 判断基準は、チートによる損害と入力遅延による損害のどちらが大きいかです。RTT 250 ms での測定では、サーバー権限の場合、自分の動きが反映されるまで 294~310 ms、クライアント権限の場合は 11~22 ms でした。また、クライアント権限のチーターによる 50 m のジャンプは、サーバーに拒否されました。
  2. _enter_tree でノード名から権限を設定します。 各プレイヤーノードにピア ID を名前として付けます。 _enter_tree で set_multiplayer_authority(name.to_int()) を呼び出せば、_ready より前に権限が設定されます (Travis Hunter、tK2ACXUGcrY [00:46:16])。スポーン同期されるプロパティをここで読み取らないでください。測定では、すべてのクライアント側コピーで、_enter_tree の時点では peer_id がまだ 0 でした。
  3. すべての any_peer RPC はリクエストとして扱います。 最初に multiplayer.get_remote_sender_id() を読み取ります(await 後は 0 になることを測定済みです)。送信者が対象の所有者であることと、値が妥当であることを確認してから処理します。実際のテストでは、他のプレイヤーへの偽装テレポートと 30 m のジャンプはどちらも拒否されました。
  4. RPC のチェックサムが比較するのはメソッド名だけです。ドキュメントの説明とは異なります。 高レベル API のドキュメントには、すべての @rpc 宣言を対象にし、引数は無視するとありますが、4.7.2 での実機テスト 2 回(M2、ラウンド 1 とラウンド 2)の結果は一致しません。片方にないメソッド名があると、両側で「The rpc node checksum failed」と記録され、RPC はソート済みインデックスで対応付けられます。つまり、クライアントが b を呼び出すとサーバーでは a が実行されました。モードが異なる場合は呼び出しごとにのみ失敗します(「not allowed」)。引数の数が異なる場合も呼び出しごとに失敗します(「expected 2 argument(s)」)。call_local や転送モードが異なっていても、エラーなしで通ります。サーバーとクライアントでは、各ノードパスに同じスクリプトを使ってください(単一のコードベース:Davide Di Staso、nyKBuM9Y_-Q)。
  5. 遅延を加え、3 人のプレイヤーでテストします。 ローカルホストでは遅延に関するバグがすべて隠れ、2 人だけでは第三者から見える問題が隠れます(Galffy、iP_xdJ0peFo [00:09:58, 00:42:31])。gd_net.UdpLagProxy を使うと、root 権限なしでローカルホストに遅延、ジッター、パケットロスを追加できます。
  6. レプリケーションのコストを設計に反映します。 ALWAYS は、値が変わっているかどうかにかかわらず、ネットワークフレームごとに送信します。ON_CHANGE は信頼性がありますが、値が動いている間は ALWAYS よりコストがかかります(測定値:動いているプロパティ 50 個で 72.6 対 100 KiB/s)。ヘッドレスサーバーには FPS 上限がありません。この環境では 145 fps となり、60 fps の 2.2~2.4 倍の帯域幅を使用しました。--max-fps、Engine.max_fps で上限を設定するか、replication_interval を設定してください。可視性を利用して通信量を減らします(Di Staso [00:14:29])。
  7. 接続イベントがどこで発生するかを把握します。 接続してくるクライアントでは、既存の各ピアについて peer_connected が一度ずつ発生します。サーバー ID 1 も含まれます。connected_to_server はクライアント上でのみ発生するため、クライアント情報は peer_connected ではなく、ここから送信してください(ドキュメントとの差分 7;FinePointCGI e0JLO_5UgQo [00:22:50])。サーバーが退出した後、get_unique_id() は 0 を返します。レジストリやスポーンループでは ID 0 を除外してください (pA9RGn87Uag [00:47:31])。すべて測定済みです(M14)。
  8. サーバーが防ぐのは速度チートであり、ウォールハックではありません。 サーバー権限は、あり得ない動きや無限の体力を防ぎますが、ウォールハックやエイムボットは防げません。すべてのクライアントが、全員の位置情報を受け取るためです。インタレスト管理(シンクロナイザーの可視性、サーバー側の視線チェック)で情報漏洩を抑えられます(MfjEB1cowsE [00:18:55];iP_xdJ0peFo [00:53:28])。
  9. 専用サーバーは、「dedicated server」モードでエクスポートした同じプロジェクトです。 このモードでは dedicated_server 機能が追加され、--headless を指定しなくても、ヘッドレス表示とダミー音声が強制されます(エクスポートした macOS バイナリで測定済み)。また、Strip Visuals により、サーバーパックから 3.1 MB のテクスチャが削除されました(BatteryAcidDev、jgJuX04cq7k [00:04:47])。

最初に決めること

ジャンルとプレイヤー数、不正行為を許容する対象(対戦、協力プレイ、友人同士のみ)、プラットフォーム(ブラウザーではWebSocketまたはWebRTCを使用し、ENetは使用しません)、トポロジ(専用サーバー、リッスンサーバー、リレー、P2P)、すでに選定済みのバックエンド(Steam、Nakama、Colyseus、自前サーバー)、ホスティング予算(クラウドには費用がかかります。デプロイの前に確認してください)、ティックレートと目標RTT(デフォルトはサーバー60 Hz、100 msと250 msでプレイテスト済み)。 デフォルト:プロジェクトは1つ、対戦要素のあるゲームでは入力のみを送るクライアントを使ったサーバー権威型、デスクトップとモバイルではENet、WebではTLS(wss://)経由のWebSocketを使用します。

ワークフロー

  1. ネットワーク設計メモ。 オブジェクトごとに、所有者、レプリケーションする内容(synchronizerのプロパティとモード)、イベントの内容(モード、転送方式、チャンネルを指定したRPC)、クライアントがリクエストできる内容を定めます。unreliable_orderedの更新ストリームには個別の転送チャンネルを割り当てます。チャンネルは@rpcの最後に指定します(docs deltas 4、5)。アイドル状態のプレイヤーは、速度がしきい値を下回ったら何も送信しません(pA9RGn87Uag [00:42:16])。GATE:所有者のいないオブジェクトがないこと。要件で不正行為を許容している場合を除き、クライアント所有の体力、スコア、インベントリがないこと。
  2. コードでレプリケーションを構築。 gd_net.install_netkit(P)を実行し、続いてnet_build.gd(make_sync、make_spawner)で、SceneReplicationConfigをコード内で設定したシーンを作成します。Replicationパネルは人間が操作するためのものです(references/gui-paths.md)。プレイヤーは、名前をpeer idに設定し、add_child(node, true)を呼び出し、_enter_treeで権限を設定します。GATE:net_audit.gd:sceneにフラグがないこと(ルートパスが解決できること、ObjectまたはRIDプロパティがないこと、@Node@2という名前がないこと、間隔がモードと一致すること)。
  3. localhostでセッションテスト。 gd_net.session(P, roles)でサーバーと2~3台のクライアントを実行します。各クライアントはそれぞれ別のAPFSクローンで起動し、最初にすべてのマシンスロットを確保します。ロビーのゲート、途中参加者、退出者、不正なRPC、そして両側のauth_callbackとcomplete_authを使ったパスワードを確認します(P5)。GATE:すべてのクライアントがすべてのプレイヤーを認識すること、途中参加者にも全員が表示されること、退出者のノードがすべての場所で解放されること、権限レポートが正しいこと、偽装RPCが拒否されること、ログがクリーンであること。
  4. レイテンシテスト。 UdpLagProxy経由で100 msと250 ms、さらに250 msにジッター30 msとパケットロス5%を加えて再実行します。replication_delay()で、リモートクライアントに表示されるプレイヤーがサーバーの実際の状態からどれだけ遅れているかを測定します。GATE:数値がジャンルに適していること、または予測処理を計画していること(netfoxまたは自前の実装)。
  5. 帯域幅テスト。 ENetカウンター(結果のbandwidth)から、モード、間隔、可視性ごとのクライアントあたりのサーバーKiB/sを確認します。GATE:最大プレイヤー数で予算内に収まること、fpsが上限設定されていること。
  6. Webとトランスポート。 transport="websocket"を指定して同じセッションを実行します。ブラウザービルドでは、Web向けにエクスポートし、ヘッドレスChromeでクライアントを実行します(手順P9)。WebSocketはTCPです。「unreliable」RPCも信頼性のある順序付きで到着し、パケットロス時にはヘッドオブラインブロッキングが発生します。WebRTCではブラウザーに信頼性のないチャネルを提供できますが、デスクトップではシグナリング、STUN/TURN、webrtc-nativeが必要です(WebRTC doc;wgIqB6JNcro [00:19:35])。HTTPSページではwss://が必要です。バックグラウンドタブでは_processが停止し、ソケットが切断されるため、再接続処理を実装します(web doc)。GATE:ブラウザークライアントが参加し、すべてのプレイヤーを認識し、RTTが測定されていること。
  7. 専用サーバー。 gd_net.server_preset()を実行してエクスポートし、クライアントとサーバーのパックを比較し(pack_report)、エクスポートしたバイナリを--headlessなしで実行してクライアントを接続します。systemdまたはjournaldのログ用にapplication/run/flush_stdout_on_print=trueを設定します(docs delta 12。デフォルトはfalseで、測定済みです)。GATE:dedicated_server機能がtrueであること、表示がheadlessであること、パックにクライアントのテクスチャが含まれないこと、終了コードが0であること。
  8. 表示の確認。 ウィンドウ表示のオブザーバークライアントでレプリケーションされたワールドをキャプチャし(capture_at)、開いて確認します。GATE:プレイヤーがサーバーの示す位置にいること、peerごとに色分けされていること、欠けているものがないこと。

1回の呼び出しでハーネスを実行

import sys; sys.path[:0] = ["<skills>/scenario-godot-expert/scripts", "<skills>/scenario-godot-multiplayer/scripts"]
import gd_net
gd_net.install_netkit(P); gd_net.build_scenes(P)          # res://net/world.tscn, player.tscn, props
roles = [gd_net.role("server", role="server", port=24010, lobby_size=2),
         gd_net.role("c1", role="client", port=24011, index=0, delay_s=0.3, cheat_at=2.0),
         gd_net.role("c2", role="client", port=24011, index=1, delay_s=0.3)]
with gd_net.UdpLagProxy(24011, 24010, rtt_ms=100, jitter_ms=0, loss=0.0):
    s = gd_net.session(P, roles)                            # one APFS clone and one Godot per role
s["roles"]["c1"]["result"]["rtt_median_ms"]                 # every NetLab number is in AGENT_RESULT

独自のゲームでも同じパターンを使います。ロールごとにジョブスクリプト(role(name, script=...))を用意し、ユーザー引数は--の後に指定し、各プロセスからAGENT_RESULTを出力します。プロジェクトごとにポートを固有にしてください。2つのロールで同じプロジェクトフォルダーを共有してはいけません。

予測、ロールバック、バックエンド(ここでは実行していません)

netfox は、クライアント予測とロールバックを組み合わせてサーバー権限を実現します。ネットワーク ティックで入力を収集し、_rollback_tick でロジックを実行し、move_and_slide の前後で NetworkTime.physics_factor を適用し、表示には TickInterpolator を使用します(GqHTNmRspjU [00:06:58, 00:18:55, 00:21:24]、iP_xdJ0peFo [00:28:00])。 まず小さなプロトタイプで実証してください。設計に強い意見が反映されています(GqHTNmRspjU [00:34:53])。決定論的な入力のみのロールバックは、2~8 人のプレイヤーが参加する格闘ゲームや RTS に似たゲームに適しています(David Snopek、 zvqQPbT8rAE)。Steam(GodotSteam、SteamMultiplayerPeer)、Nakama、Colyseus、WebRTC P2P では、 ピアとロビーの呼び出しを切り替えます。権限、スポーナー、シンクロナイザーのコードはそのままです(Gwizz、fUBdnocrc3Y; AndrooDev、wgIqB6JNcro [00:07:18])。これらはいずれもここでは実行していません。サードパーティー製アドオン、アカウント、 またはシグナリングサーバーが必要です。API を引用する前に、インストール済みのリリースで確認してください。

数値

値ここでの測定値(2026-10-02、Mシリーズ Mac、localhost)比較対象
プロキシなしの RPC 往復時間16~19 ms両端とも 60 fps。往復それぞれで 1 フレームが支配的
ENet RTT 統計15~23 ms同上
自分の入力から目に見える動きまで(サーバー権威方式)47~52 ms約 3 フレーム
リモートビューとサーバーの実際の状態との差20~30 ms、3 m/s で誤差 0.07 mプロキシなし
RTT が 100 / 250 ms の場合70~80 / 150~155 ms、0.22 / 0.44 mUdpLagProxy。各方向に約 6 ms を追加
50 個のプロップ、ALWAYS、60 fps72.6 KiB/s、移動中またはアイドル時クライアント 1 台
ON_CHANGE、移動中/アイドル時98~101 / 2.3 KiB/s同上
ALWAYS、replication_interval 0.0521~22 KiB/s同上
ALWAYS、上限なしのヘッドレスサーバー(145 fps)160~175 KiB/s同上
auth_timeout のデフォルト値。無応答ピアのキック3.0 s。1.5 に設定すると 1.50 sサーバー
Web エクスポート、シングルスレッド40 MB、wasm 39.5 MB(gzip 圧縮時 10.1 MB)scenario-godot-performance-export への引き継ぎ
専用サーバーパック/クライアントパック0.11 / 3.27 MB(テクスチャを除去すると 3.1 MB)同じプロジェクト
spawn_limit0 = 無制限(デフォルト)。上限を 2 にすると、3 回目の spawn() は「Spawn limit reached!」をログに出力して null を返し、クライアントには 2 個が表示されますM14。最大プレイヤー数まで増やしてください(jgJuX04cq7k [00:18:34])

品質ゲート

  • 測定可能性:net_audit に警告がないこと。すべてのロールが予期しないエンジンエラーなしで ok であること。 players_seen_max がすべての環境でプレイヤー数と一致すること。権威ボディーが 1、入力がピアであること。テレポート数が想定どおりであること。100 ms と 250 ms での RTT と replication_delay。各モードの KiB/s。rpc_audit をレビュー済みで、rpc_pair_check が ok であること。エクスポートしたサーバーのブート辞書が正しいこと。
  • 視覚的な確認:オブザーバーのキャプチャにすべてのプレイヤーが表示されること。250 ms 未満ではテレポート後に残像がないこと。ブラウザーのスクリーンショットに複製されたプレイヤーが表示されること。

よくある間違い

間違いどのように見えるか修正方法
スポーナーを使わずにサーバーだけでプレイヤーを生成する、またはスポーナーの spawn_path が空クライアントに誰も表示されないspawn_path とリスト内のシーンを設定した MultiplayerSpawner を使い、サーバーから add_child を呼び出します
_enter_tree で同期プロパティから権限を読み取るクライアント上で権限がどこでも 1 になるname.to_int() から権限を取得します
add_child(node, true) を使わずに実行時ノードを作成する@Node@2 という名前になり、RPC で「ノードが見つかりません」と表示される読み取り可能な名前を使います
get_remote_sender_id() の前に await する送信者が 0 になり、検証が誤って成功または失敗する先に送信者を読み取ります
RPC をサーバー側スクリプトにだけ追加するチェックサムエラーが発生し、誤ったメソッドが実行される1 つのスクリプトを使うか、rpc_pair_check を使います
RPC 引数にオブジェクトまたはリソースを使うエラーは発生しないが、受信側に EncodedObjectAsID が渡されるID、パス、またはプレーンデータを送信します
対戦ゲームでクライアントが自分の本体の権限を持つ速度変更やテレポートの不正行為が通用する入力のみの権限とサーバーシミュレーションを使います
ヘッドレスサーバーに上限を設定しない帯域幅が 2.4 倍になる--max-fps 60 またはインターバルを使います
P2P 形式の機能で server_relay を無効にする「不明なピア ID で RPC を呼び出そうとしています」と表示されるリレーを有効にするか、サーバー経由でルーティングします
ネットワーク接続されたブランチを誤った順序で解放する「現在アクティブではありません」、「ノードが見つかりません」と表示されるオフラインピアにし、内容を解放し、カスタム API を破棄してからブランチを解放します
通常モードでサーバーをエクスポートするフルサイズのパックになり、ウィンドウが開く専用サーバーのエクスポートモードと Strip Visuals を使います
assert() 内でサーバーを起動するエディターでは動作するが、リリース版のエクスポートでは接続を受け付けないvar err := peer.create_server(port) とし、その後 err を確認します(M15:リリース版では assert の本体が削除されます)
peer_connected からクライアント情報を送信する、または ID 0 を登録する後から参加したクライアントでノードが重複し、実体のないプレイヤーが表示されるconnected_to_server を使い、ID 0 をスキップします
クライアントとサーバーで異なる Godot バージョンを使う接続またはプロトコルのエラーが発生する同じ 4.7.2 ビルドを使います(プロトコルは 4.3 で変更されました)

引き継ぎ

  • 受け取るもの:scenario-godot-gameplay および scenario-godot-3d-world または scenario-godot-2d からゲームシーンとプレイヤーコントローラー、scenario-godot-architecture からアーキテクチャルール。
  • 提供するもの:ネットワーク設計ノート、キットシーンまたはスクリプト、セッションの証拠を記録した JSON、サーバープリセット。Web ビルドのサイズとヘッダーは scenario-godot-performance-export に、CI ジョブは scenario-godot-pipeline-automation に、ロビースクリーンは scenario-godot-ui に引き継ぎます。

Godot 4.7 に関する注意事項

  • get_rpc_config は get_node_rpc_config に変更されました(4.5)。@rpc テーブルは Script.get_rpc_config() にあります。
  • プロパティ名は auth_timeout です。ドキュメントページでは auth_timout と綴られています。
  • 4.7.2 では、スポーナーによって生成されたノードの削除後にレプリケーションが停止するピアが修正されました。
  • Godot 3 の名前(NetworkedMultiplayerENet、remote、rset、get_rpc_sender_id)は存在しません。gd_net.rpc_audit がこれらを検出します。
  • デスクトップで WebRTC を使用するには webrtc-native 拡張機能が必要です。これがなくても initialize() は OK を返し、エラーをログに出力するだけです(計測済み)。
  • 未解決事項:マシンに高負荷がかかっていると、20 回の ALWAYS 実行のうち 4 回で、ENet のスロットリングを 0 に設定しているにもかかわらず、約 68 パケット/秒ではなく約 10 パケット/秒が送信されました。長時間のセッションでは NetUtil.enet_throttle() をログに記録してください(P8)。
  • 1 つのサーバーフレーム内で 2 つのピアが退出すると、ENet では「Unable to send packet on channel 0」、WebSocket では ready_state != STATE_OPEN がログに記録されます。これは無害で、完全一致するテキストによって許可リストに登録されています。

参考資料

コメント