mcpField
M
multiplayer-basics

multiplayer-basics

github.com/jame581開発889164 回インストール4 日前に更新
リポジトリ

multiplayer-basics について

Godot 4.3以降の開発者向けマルチプレイ基礎スキルです。クライアント・サーバー構成、RPC、権限、オブジェクト生成を、GDScriptとC#の例で説明します。

主な機能

  1. 01クライアント・サーバー構成の基礎
  2. 02ENetMultiplayerPeerの接続設定
  3. 03RPCモードと転送オプション
  4. 04マルチプレイの権限とオブジェクト生成
  5. 05参加フローと切断処理

活用シーン

  1. 01マルチプレイゲームのサーバーとクライアントの構築
  2. 02RPCによるゲームイベントや入力の送信
  3. 03プレイヤー参加、生成、切断後の後処理

インストール

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

$npx skills add https://github.com/jame581/godotprompter --skill multiplayer-basics

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

同梱ファイル

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

references/
disconnect-handling.md
enet-setup.md
player-join-flow.md
spawning-networked-objects.md

詳細

Godot 4.3+ のマルチプレイヤーの基本

すべての例は、非推奨 API を使用せずに Godot 4.3+ を対象としています。まず GDScript を示し、続いて C# を示します。

関連スキル: 状態の同期と補間については multiplayer-sync を参照してください。ヘッドレスエクスポートとサーバーのデプロイについては dedicated-server を参照してください。


1. マルチプレイヤーのアーキテクチャ

Godot は MultiplayerAPI を基盤とするクライアントサーバーモデルを使用します。1 つのピアがサーバーとして動作し、それ以外はすべてクライアントです。各ピアには、ネットワークレイヤーによって一意の整数 ID が割り当てられます。

ピア ID役割
1サーバー(常に)
2+接続されたクライアント — ランダムに生成される一意の ID であり、連番ではありません

マルチプレイヤーの権限とは、ノードの所有権に関する概念です。権限を持つピアのみが入力を読み取り、そのノードの状態を制御する必要があります。デフォルトでは、サーバー(ピア 1)がすべてのノードの権限を持ちます。set_multiplayer_authority(peer_id) を呼び出すと、所有権をクライアントに移譲できます。この呼び出しはローカルで実行され、複製されないため、すべてのピアで呼び出してください。

Server (peer 1)
    ├── Owns game state by default
    ├── Spawns and validates objects
    └── Routes RPCs
Client (peer 2, 3, …)
    ├── Sends input to server via RPC
    └── Receives state updates from server

2. ENetMultiplayerPeer のセットアップ

サーバー側とクライアント側では、同じ 3 つの手順を使用します。ENetMultiplayerPeer を作成し、create_server(port, max_clients) または create_client(address, port) を呼び出してから、それを multiplayer.multiplayer_peer に割り当て、MultiplayerAPI のシグナル(peer_connected、peer_disconnected、connected_to_server、connection_failed、および server_disconnected — §7 を参照)に接続します。create_* の戻り値を確認してください。戻り値は Error です。これを確認しないと、ポートがすでに使用中のときに発生する無言の ERR_CANT_CREATE が、単なるハングとまったく同じように見えてしまいます。

サーバーのピア ID は常に 1 です。クライアントにはランダムに生成された一意の ID が割り当てられるため、連番だと決めつけないでください。

すべてのシグナルハンドラーを含む、GDScript と C# でのサーバーおよびクライアントの完全な実装:references/enet-setup.md


3. RPC

@rpc(GDScript)/[Rpc](C#)は、ネットワーク越しに呼び出し可能なメソッドを示します。モードと転送設定は、セキュリティとパフォーマンスの両方に影響するため、慎重に選んでください。

RPC モード
モード呼び出せるピア実行先
"authority"(デフォルト)権限を持つピアのみ送信先のピア
"any_peer"接続されているすべてのピア送信先のピア
転送モード
モード配信順序用途
"reliable"保証あり順序どおりチャット、スポーンイベント、重要な状態
"unreliable"ベストエフォート順不同高頻度の位置更新
"unreliable_ordered"ベストエフォートチャネルごとに順序どおり滑らかな移動のストリーム
GDScript
# chat.gd
extends Node2D

# Any peer can call; server validates then broadcasts to all peers.
@rpc("any_peer", "reliable")
func send_chat_message(text: String) -> void:
	if not multiplayer.is_server():
		return
	var sender_id := multiplayer.get_remote_sender_id()
	_broadcast_chat.rpc(sender_id, text)


# Only the authority (server) can call this; runs on every peer.
@rpc("authority", "reliable", "call_local")
func _broadcast_chat(sender_id: int, text: String) -> void:
	print("[%d]: %s" % [sender_id, text])


# Client → server: request to spawn an object.
@rpc("any_peer", "reliable")
func request_spawn(scene_path: String, spawn_position: Vector2) -> void:
	if not multiplayer.is_server():
		return
	# Server validates and performs the actual spawn.
	var scene: PackedScene = load(scene_path)
	if scene == null:
		return
	var instance := scene.instantiate()
	instance.global_position = spawn_position
	get_tree().root.add_child(instance)


# High-frequency sync; unreliable_ordered + a channel keeps this off other RPC traffic.
@rpc("authority", "unreliable_ordered", "call_local", 1)
func sync_position(pos: Vector2) -> void:
	global_position = pos

特定のピアへの送信:

# Send to everyone (including self if call_local is set):
send_chat_message.rpc("Hello!")

# Send to one specific peer:
send_chat_message.rpc_id(target_peer_id, "Hello!")
C#
// Chat.cs
using Godot;

public partial class Chat : Node2D
{
    // Any peer can call; executes on the server only.
    [Rpc(MultiplayerApi.RpcMode.AnyPeer, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
    public void SendChatMessage(string text)
    {
        if (!Multiplayer.IsServer()) return;
        int senderId = Multiplayer.GetRemoteSenderId();
        Rpc(MethodName.BroadcastChat, senderId, text);
    }

    // Authority only; runs on every peer including the caller.
    [Rpc(MultiplayerApi.RpcMode.Authority,
         CallLocal = true,
         TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
    private void BroadcastChat(int senderId, string text)
        => GD.Print($"[{senderId}]: {text}");

    // Client → server: request a spawn.
    [Rpc(MultiplayerApi.RpcMode.AnyPeer, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
    public void RequestSpawn(string scenePath, Vector2 spawnPosition)
    {
        if (!Multiplayer.IsServer()) return;
        var scene = GD.Load<PackedScene>(scenePath);
        if (scene == null) return;
        var instance = scene.Instantiate<Node2D>();
        instance.GlobalPosition = spawnPosition;
        GetTree().Root.AddChild(instance);
    }

    // High-frequency position sync.
    [Rpc(MultiplayerApi.RpcMode.Authority,
         CallLocal = true,
         TransferMode = MultiplayerPeer.TransferModeEnum.UnreliableOrdered,
         TransferChannel = 1)]
    public void SyncPosition(Vector2 pos)
        => GlobalPosition = pos;
}

C# で特定のピアに送信:

// Broadcast to all:
Rpc(MethodName.SendChatMessage, "Hello!");

// Send to one peer:
RpcId(targetPeerId, MethodName.SendChatMessage, "Hello!");

4. 権限モデル

すべてのノードには、権限を持つピアが 1 つだけあります。そのピアは、そのノードの状態更新を送信することを許可されたピアです。それ以外のピアは、受信した状態を読み取り専用として扱ってください。

GDScript
# player.gd
extends CharacterBody2D

func _ready() -> void:
	# multiplayer.get_unique_id() = this peer's ID; server assigns authority during spawn (see Section 6).
	pass


func _physics_process(delta: float) -> void:
	# Guard: authority-only input and movement.
	if not is_multiplayer_authority():
		return

	var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	velocity = direction * 200.0
	move_and_slide()

	sync_position.rpc(global_position)


@rpc("authority", "unreliable_ordered", "call_local", 1)
func sync_position(pos: Vector2) -> void:
	if not is_multiplayer_authority():
		global_position = pos


func print_authority_info() -> void:
	print("My peer ID : %d" % multiplayer.get_unique_id())
	print("Authority  : %d" % get_multiplayer_authority())
	print("Am I auth? : %s" % str(is_multiplayer_authority()))
C#
// Player.cs
using Godot;

public partial class Player : CharacterBody2D
{
    public override void _PhysicsProcess(double delta)
    {
        // Guard: authority-only input.
        if (!IsMultiplayerAuthority()) return;

        var direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
        Velocity = direction * 200f;
        MoveAndSlide();

        Rpc(MethodName.SyncPosition, GlobalPosition);
    }

    [Rpc(MultiplayerApi.RpcMode.Authority,
         CallLocal = true,
         TransferMode = MultiplayerPeer.TransferModeEnum.UnreliableOrdered,
         TransferChannel = 1)]
    private void SyncPosition(Vector2 pos)
    {
        if (!IsMultiplayerAuthority())
            GlobalPosition = pos;
    }
}

API の概要:

メソッド戻り値備考
multiplayer.get_unique_id()intこのピアの ID
get_multiplayer_authority()intこのノードを所有するピアの ID
is_multiplayer_authority()boolこのピアがこのノードを所有している場合は True
set_multiplayer_authority(id)void所有権を移譲します。ローカルのみで有効なため、すべてのピアで呼び出してください(例:スポーン関数内)

5. ネットワークオブジェクトのスポーン

MultiplayerSpawner を使用して、シーンインスタンスをピア間で複製します。シーンを登録し(add_spawnable_scene() またはインスペクターの Auto Spawn List)、サーバーが spawn_path ノードの直接の子としてそのシーンのインスタンスを追加すると、スポナーがすべてのピアでインスタンスを生成します。データ(位置、所有者など)が必要なスポーンでは、spawn_function を設定し、サーバーで spawn(data) を呼び出します。

MultiplayerSpawner のシーン設定とサーバーでのスポーンの流れ(GDScript + C#)については、references/spawning-networked-objects.md を参照してください。


6. プレイヤーの参加フロー

ロビー参加のライフサイクル全体:ピアが接続 → サーバーがスロットを割り当て → ロビーシーンを読み込み → プレイヤーノードをスポーン → ピアリストをすべてのクライアントにブロードキャスト → 「start match」RPC で、すべてのピアをゲームプレイシーンに移行。

完全な GDScript および C# の実装(ピア接続ハンドラー、スロット割り当て、ロビーの状態、ゲームプレイへの移行)については、references/player-join-flow.md を参照してください。


7. 切断処理

multiplayer API の peer_disconnected(id) を監視します。サーバーでは、切断したピアのプレイヤーノードを解放し、更新されたピアリストをブロードキャストします。クライアントでは、サーバーとの切断を検知し、再接続画面またはメインメニュー画面に遷移します。

タイムアウト検出の設定、サーバー側のクリーンアップ、クライアント側の再接続フロー(GDScript + C#)については、references/disconnect-handling.md を参照してください。


8. よくある落とし穴

落とし穴症状対処法
誤った権限で RPC を呼び出すメソッドが実行されず、受信側に RPC '…' is not allowed on node … と記録される送信前に is_multiplayer_authority() を確認し、意図した箇所でのみ "any_peer" を使用する
順序が保証されない RPC による同期ずれ位置が揺れたり、急に移動したりするストリームには "unreliable_ordered" を使用し、重要な状態変更には "reliable" を使用する
_process で入力を読み取る(_physics_process ではなく)フレームレートの違いにより移動が同期しないCharacterBody2D は必ず _physics_process で移動させ、同期 RPC もそこから送信する
入力前に is_multiplayer_authority() を確認しないすべてのピアがすべてのプレイヤーを操作する入力処理の先頭に if not is_multiplayer_authority(): return ガードを追加する
MultiplayerSpawner を使わずにスポーンするオブジェクトがサーバーには表示されるが、クライアントには表示されないMultiplayerSpawner の spawn_path の下に複製対象のインスタンスを追加する(シーンをスポーン可能なリストに登録する)か、spawn_function とともに spawn() を使用する
権限 RPC に call_local を付け忘れるサーバーの状態が自身のノードと食い違う送信側もローカルで RPC を実行する必要がある場合は "call_local" を追加する
クライアントの接続前に rpc() を呼び出すTrying to call an RPC via a multiplayer peer which is not connected. エラーが発生するクライアントでは、connected_to_server の後にのみ RPC を送信する(ピアが割り当てられる前は、デフォルトの OfflineMultiplayerPeer で実行されるのは call_local RPC のみで、ローカルでのみ実行される)
エクスポートしたビルドから res:// のシーンを除外しないクライアントがサーバー専用スクリプトを読み取れるクライアントのプリセットからサーバー専用ファイルを除外する(Resources タブでエクスポートモードまたは除外フィルターを設定する)か、機密性の高いサーバーコードに PCK 暗号化を使用する

9. チェックリスト

  • ENetMultiplayerPeer.create_server() / create_client() が multiplayer.multiplayer_peer に割り当てる前に OK を返す
  • 5 つすべてのマルチプレイヤーシグナルが接続されている:peer_connected、peer_disconnected、connected_to_server、connection_failed、server_disconnected
  • プレイヤー入力を読み取るすべてのノードで、if not is_multiplayer_authority(): return によるチェックを行う
  • 入力処理と sync_position RPC がどちらも _process ではなく _physics_process 内にある
  • RPC モードを意図的に選択している:クライアントからサーバーへの呼び出しにのみ "any_peer" を使用し、サーバーからクライアントへの呼び出しには "authority" を使用する
  • 高頻度の更新(位置、回転)にのみ信頼性のない RPC を使用し、イベント(スポーン、ダメージ、チャット)には信頼性のある RPC を使用する
  • 最初のプレイヤーが参加する前に、MultiplayerSpawner にスポーン可能なシーンをすべて設定している
  • 各プレイヤーノードのスポーン時に、すべてのピアで set_multiplayer_authority(peer_id) を呼び出している(例:spawn_function 内)
  • peer_disconnected ハンドラーでプレイヤーノードを解放し、追跡用コレクションから削除する
  • クライアント側の server_disconnected ハンドラーでメインメニューに戻り、multiplayer.multiplayer_peer を null にする
  • 切断コールバック内で、保存されたノード参照を参照する前に is_instance_valid() を確認する
  • connected_to_server が発生する前に、クライアントから rpc() を呼び出さない

コメント