【Godot】GodotのLANマルチプレイ入門:同じWi-Fiにつないで、それぞれが自分のキャラを動かす

作成: 2026-09-23最終更新: 2026-10-10

ENetでホストと参加者をつなぎ、参加者ごとにキャラを出して、それぞれが自分のキャラだけを動かす方法を解説します。

別々のPCで同じゲームに入り、それぞれが自分のキャラを動かすLANのマルチプレイは、Godot に入っている ENet という通信の仕組みで作れます。試すだけなら、同じPCでウィンドウを2つ開き、ホストと参加者に分ければ足ります。

ただ、ホストと参加者の両方でキャラを出すと二重になり、自分のキャラを動かすと相手のキャラまで一緒に動きます。ウィンドウの数だけ同じシーンのコピーがあり、それぞれのコピーで役目を分ける必要があるからです。

この記事では、ENet でホストと参加者をつなぎ、参加者ごとにキャラを出して、自分のキャラだけを動かす方法を解説します。

左右2つのウィンドウに同じ2体の人形が映り、左のウィンドウでは左の人形だけ、右のウィンドウでは右の人形だけが矢印で動いている

この記事でわかること

  • ウィンドウの数だけ同じシーンのコピーがあり、違うのは「自分が持ち主のキャラ」だけという見方
  • ENet でホストが部屋を開き、参加者が入る手順
  • キャラを出すのはホストだけにする理由と、MultiplayerSpawner の使い方
  • 持ち主のキャラだけを動かす set_multiplayer_authority()

動作確認環境: Godot 4.7

Sponsored

ウィンドウごとのシーンのコピー

分割画面 の2人プレイは、1つのゲームの中に2人がいます。世界は1つで、コントローラーで2人を分けます。

LANのマルチプレイは違います。ホストのウィンドウと参加者のウィンドウは、それぞれ別に動いているゲームです。同じシーンのコピーが2つあり、どちらのコピーにも2人のキャラが同じ名前で並びます。

左は分割画面で、1つの世界に2人がいてコントローラーで分ける。右はLANで、ホストと参加者のウィンドウに同じシーンのコピーが1つずつある

キャラの名前は、持ち主の番号にします。ホストの番号は 1、参加者の番号は接続ごとに決まる数です。どのコピーでも、名前を見れば持ち主が分かります。図の「2584」は例で、実際の番号は接続のたびに変わります。

2つのコピーのどちらにも「1」と「2584」が並ぶ。ホストの自分は「1」、参加者の自分は「2584」

コピーの間でそろえるのは、次の3つです。

  • 誰がいるか: キャラを出すのはホストだけ。ホストが出すと、参加者のコピーにも同じ名前で出る
  • 誰が動かすか: キャラには持ち主(authority)がいる。自分のウィンドウで動かしていいのは、自分が持ち主のキャラだけ
  • どこにいるか: 持ち主のコピーの位置を、ほかのコピーへ写す

他のエンジンでいうと: 値をほかの画面へ写す MultiplayerSynchronizer は、Unity Netcode の NetworkVariable や NetworkTransform、UE の Replicated 変数に近い役割です。誰の変更を配るかの決め方は、エンジンごとに違います。

ホストと参加者のつなぎ方(ENet)

つなぐ役は、Godot に入っている通信の仕組み ENet を使う ENetMultiplayerPeer です。自分の側の接続口で、ピアと呼びます。ホストは番号(ポート)を決めて部屋を開き、参加者はホストのIPアドレス(ネットワークの中でPCを見分ける番号)とポートを指定して入ります。

# ホスト
var peer := ENetMultiplayerPeer.new()
peer.create_server(7000)              # 7000番で待つ
multiplayer.multiplayer_peer = peer   # このピアでゲームの通信をする

# 参加者
var peer := ENetMultiplayerPeer.new()
peer.create_client("127.0.0.1", 7000) # ホストのIPとポート
multiplayer.multiplayer_peer = peer
ホストが7000番の部屋を開き、参加者がIPとポートを指定して入る。参加者どうしは直接つながらず、ホストが中継する

create_server() と create_client() で、つなぐ準備をします。できたピアを multiplayer.multiplayer_peer に入れます。multiplayer は、どのノードからも使える、このゲームの通信の窓口です。ここに入れると、接続の知らせやキャラの同期を、シーンの側で扱えるようになります。参加者の接続が終わると connected_to_server が届き、失敗したときは connection_failed が届きます。どちらも参加者の側にだけ届くシグナルです。

このつなぎ方では、参加者は全員ホストにつながります。参加者どうしのやり取りも、ホストが中継します。

参加者が入力するIPアドレスは、同じPCで試すなら 127.0.0.1 です。別のPCから入るなら、ホストのPCの IPv4 アドレスにします。Windows なら ipconfig で、使っている Wi-Fi か有線の欄に出ます。vEthernet のような仮想の欄の番号は使いません。192.168. で始まることが多い番号です。

Sponsored

MultiplayerSpawner でキャラを出す

キャラを出すのは、ホストの仕事です。MultiplayerSpawner を置いておくと、ホストが出したキャラが、参加者のコピーにも同じ名前で出ます。

ホストが出したキャラは MultiplayerSpawner が参加者のコピーにも出す。参加者も出すと二重になる

設定は2つです。

  • Spawn Path: キャラを置く親ノード。ここの直下に足した物だけが写される
  • Auto Spawn List: 写してよいシーンの一覧。キャラのシーンを入れておく

ホストは、参加者がつながったときの peer_connected シグナルで、その番号のキャラを出します。ホストには、自分自身がつながったという peer_connected は届きません。自分のキャラは、部屋を開いたときに自分で出します。どちらも、キャラのシーンを作って名前を番号にし、Spawn Path の直下に足すだけです(実践の _add_player())。

参加者の側では、キャラを出しません。参加者も peer_connected を受け取ります。同じ関数をそのまま呼ぶと、自分のコピーにだけ余分なキャラが増えます。出す処理は multiplayer.is_server()(自分がホストか)で守ります。あとから入った参加者にも、それまでに出たキャラはそろって出ます。

キャラの持ち主(multiplayer authority)

どのウィンドウでも、キー入力はそのウィンドウの全員のキャラのスクリプトに届きます。そのままだと、自分のキャラを動かしたとき、相手のキャラまで一緒に動きます。

キャラ「1」の持ち主はホスト、「2584」は参加者。それぞれのウィンドウの矢印キーは、自分が持ち主のキャラだけを動かす

キャラごとに持ち主を決め、持ち主のウィンドウでだけ動かします。

func _enter_tree() -> void:
	set_multiplayer_authority(str(name).to_int())   # 名前の番号の人が持ち主

動かす処理の先頭では、if not is_multiplayer_authority(): return として、自分のキャラでなければ何もしないようにします。

持ち主は、何もしなければホスト(1)です。どのコピーでも、同じ名前から同じ番号を入れます。これで全員の考える持ち主がそろいます。

_enter_tree() は、ノードがツリーに入った瞬間に、_ready() より先に呼ばれる関数です(ライフサイクル入門)。ここで入れるのは、子の MultiplayerSynchronizer より先に持ち主を決めるためです。親の _ready() は子の準備が終わってから呼ばれます。そこまで待つと、子の MultiplayerSynchronizer が先に動き出してしまいます。

Playerの_enter_tree()で持ち主を入れると、子のMultiplayerSynchronizerが動き出す前に届く。Playerの_ready()で入れると遅い

MultiplayerSynchronizer で位置を写す

持ち主だけが動かすと、自分のウィンドウでは動いても、相手のウィンドウのコピーは止まったままです。持ち主の位置を、ほかのコピーへ写す必要があります。

持ち主のウィンドウで動いたキャラの position が、MultiplayerSynchronizer でもう一方のウィンドウのコピーへ写される

写すのは MultiplayerSynchronizer です。キャラのシーンに子として足し、下のパネルの Replication で写したいプロパティを選びます。今回はキャラの position だけです。

  • Replicate を Always にすると、値が変わっていなくても、持ち主の値を送り続ける
  • 持ち主でないコピーで position を書き換えても、次に届いた持ち主の値で上書きされる

MultiplayerSynchronizer の持ち主も、親と同じ番号になります。set_multiplayer_authority() は、既定で子まで同じ番号を入れるからです。

Sponsored

実践:2つのウィンドウで、ホストと参加者のキャラを動かす

友達と協力するアクション、LANパーティーの対戦ゲーム、2人で回る探索ゲーム——どれも最初の一歩は同じです。同じPCでウィンドウを2つ開き、ホストと参加者のキャラがどちらのウィンドウにも出て、それぞれ自分のキャラだけが動くところまで作ります。

① 置くもの

新しい2Dのプロジェクトで始めます。先に、キャラのシーン player.tscn を作ります。

  • ルート: CharacterBody2D。名前は Player。Motion Mode を Floating にする(上下左右に動く見下ろし型なので、床と壁の区別をしない)
  • 子の Sprite2D: Texture に icon.svg
  • 子の Label: 番号を出す
  • 子の CollisionShape2D: Shape に CircleShape2D(Radius は 32)
  • 子の MultiplayerSynchronizer: 下の Replication パネルで、Player の position を足す。Replicate を Always にする

次に、メインのシーン main.tscn を作ります。

  • ルート: Node2D。名前は Main。FileSystem ドックの右クリックから Set as Main Scene にする
  • 子の Lobby(子を縦に自動で並べる VBoxContainer): その下に Address(LineEdit)・Host・Join(Button)・Status(Label)。Address の Text は 127.0.0.1。ボタンは名前だけでは文字が出ないので、Text にもそれぞれ「Host」「Join」と入れる
  • 子の Players(Node2D): キャラの置き場所
  • 子の MultiplayerSpawner: Spawn Path に Players。Auto Spawn List に player.tscn

最後に、ウィンドウを2つ開く設定をします。エディタ上部の Debug メニューで Customize Run Instances... を開き、Enable Multiple Instances をオンにして、数を 2 にします。エディタの Game 画面の右上のメニューで Embed Game on Next Play をオフにしておくと、2つとも普通のウィンドウで開きます。ボタンのシグナルはコードでつなぐので、エディタでの接続は要りません。

② 組み立て

player.tscn のルートに付けます。

# player.gd(Player に付ける)
extends CharacterBody2D

const SPEED := 300.0

func _enter_tree() -> void:
	set_multiplayer_authority(str(name).to_int())   # 名前の番号の人が持ち主

func _ready() -> void:
	$Label.text = str(name)
	if is_multiplayer_authority():
		position = Vector2(randf_range(150, 650), randf_range(150, 450))   # 出る場所は持ち主が決める

func _physics_process(_delta: float) -> void:
	if not is_multiplayer_authority():
		return                                      # 自分のキャラだけ動かす
	velocity = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") * SPEED
	move_and_slide()

main.tscn のルートに付けます。

# main.gd(Main に付ける)
extends Node2D

const PORT := 7000
const PLAYER_SCENE := preload("res://player.tscn")

@onready var lobby: VBoxContainer = $Lobby
@onready var address: LineEdit = $Lobby/Address
@onready var status: Label = $Lobby/Status
@onready var players: Node2D = $Players

func _ready() -> void:
	$Lobby/Host.pressed.connect(_on_host_pressed)
	$Lobby/Join.pressed.connect(_on_join_pressed)
	multiplayer.peer_connected.connect(_on_peer_connected)
	multiplayer.peer_disconnected.connect(_on_peer_disconnected)
	multiplayer.connected_to_server.connect(_on_connected_to_server)
	multiplayer.connection_failed.connect(_on_connection_failed)

func _on_host_pressed() -> void:
	var peer := ENetMultiplayerPeer.new()
	var err := peer.create_server(PORT)
	if err != OK:
		status.text = "部屋を開けません:%s" % error_string(err)
		return
	multiplayer.multiplayer_peer = peer
	_add_player(1)                            # ホスト自身のキャラは自分で出す
	lobby.hide()
	get_window().title = "ホスト"

func _on_join_pressed() -> void:
	var peer := ENetMultiplayerPeer.new()
	var err := peer.create_client(address.text, PORT)
	if err != OK:
		status.text = "接続を始められません:%s" % error_string(err)
		return
	multiplayer.multiplayer_peer = peer
	status.text = "接続中…"

func _on_connected_to_server() -> void:
	lobby.hide()
	get_window().title = "参加者 %d" % multiplayer.get_unique_id()

func _on_connection_failed() -> void:
	status.text = "つながりませんでした"
	multiplayer.multiplayer_peer = null

func _on_peer_connected(id: int) -> void:
	if multiplayer.is_server():               # キャラを出すのはホストだけ
		_add_player(id)

func _on_peer_disconnected(id: int) -> void:
	if multiplayer.is_server():
		var player := players.get_node_or_null(str(id))
		if player:
			player.queue_free()               # 全員の画面から消える

func _add_player(id: int) -> void:
	var player := PLAYER_SCENE.instantiate()
	player.name = str(id)                     # 名前 = 持ち主の番号
	players.add_child(player, true)            # MultiplayerSpawner で写すときは true を付ける

③ 動かして確かめる

左の「ホスト」のウィンドウと右の「参加者」のウィンドウに同じ2体が映り、左のウィンドウの矢印キーで両方のウィンドウの「1」だけが動く

実行するとウィンドウが2つ開きます。見るのは3つです。

まず、片方のウィンドウで Host を押します。ウィンドウのタイトルに「ホスト」と出て、「1」のキャラが出ます。試しにもう片方のウィンドウでも Host を押すと、部屋を開けません と出ます。同じPCの同じポートで開ける部屋は1つだけです。

次に、そのウィンドウで Join を押します。タイトルに「参加者」と番号が出て、両方のウィンドウに「1」と参加者の番号の2体が表示されます。出る場所はランダムなので、重なっていたら少し動かして離します。相手のウィンドウでは、最初の位置が届くまで一瞬だけ左上に見えることがあります。片方のウィンドウにだけ2体いるなら、キャラを出す処理が is_server() で守られているかを見ます。

仕上げに、ホストのウィンドウをクリックして矢印キーを押します。両方のウィンドウで「1」だけが動きます。参加者のウィンドウをクリックして押すと、今度は参加者の番号のキャラだけが動きます。2体が一緒に動くなら、is_multiplayer_authority() の行が抜けています。

ポイントは2つです。

  • 出すのはホスト、動かすのは持ち主: 出す処理は is_server() で、動かす処理は is_multiplayer_authority() で守ります。それぞれのウィンドウで、自分の役目の分だけを実行します
  • 名前が持ち主の番号: キャラの名前を接続ごとに決まる番号にしておくと、どのコピーでも同じ持ち主を入れられます。持ち主の値を写すのは MultiplayerSynchronizer の仕事です

別のPCで試すときは、参加者の Address にホストの IPv4 アドレスを入れます。弾や敵は、持ち主をホストにして、ホストが出して動かします。その作りは マルチプレイ同期入門 で扱っています。

うまくいかないときは

症状原因直し方
別のPCから Join すると「つながりませんでした」になる入れたIPアドレスが、ホストのPCのものではないホストのPCで ipconfig を実行し、使っている Wi-Fi か有線の欄の IPv4 を入れる
IPアドレスは合っているのに、別のPCから入れないホストのPCのファイアウォールが通信を止めている。またはゲスト用Wi-Fiで、端末どうしの通信が止められている初回の実行で通信の許可を求められたら、信頼できるネットワークで許可する。受信の規則を自分で足すなら、ENet は UDP なので UDP の 7000 番を開ける。ゲスト用Wi-Fiなら、普段のWi-Fiにつなぎ直す
参加者のウィンドウを閉じても、ホストのウィンドウにキャラが残る切断をまだ検知していない。または抜けた番号のキャラを消していない少し待つ。ホストが peer_disconnected で、その番号のキャラを queue_free() しているかを見る
ホストのウィンドウを閉じたあと、参加者がロビーに戻れないホストがいなくなったことを受け取っていない参加者側で server_disconnected を受け取る。Players の子を消し、multiplayer.multiplayer_peer を null にしてからロビーを表示する

おまけ:先に知っておくと良いこと

  • 実行中のコピーの中身を見る: 実行中に Scene ドックの Remote タブを開くと、動いているゲームのノードが見えます。デバッガの実行インスタンスのタブを切り替えれば、ホストと参加者の Players の下に、それぞれ何体いるかを比べられます
  • ブラウザでは ENet が使えない: Web に書き出したゲームでは、この記事のつなぎ方は動きません。デスクトップ向けに書き出して試します。書き出し方は エクスポート設定 にあります
  • 相手のキャラのカクつき: 届いた位置をそのまま使うと、届く間隔が見た目に出ます。まずはこのままでよく、なめらかにするなら、届いた位置と表示する位置を分けて表示を少しずつ近づけます
  • インターネット越しは別の話: この記事は同じネットワークの中だけです。外から入るには、ルーターの設定や中継のサーバーが要ります。まずはLANで作り切ります
Sponsored

まとめ

  • ウィンドウの数だけ、同じシーンのコピーがある: どのコピーにも同じキャラが同じ名前・同じ持ち主で並び、違うのは自分が持ち主のキャラだけ
  • ホストは部屋を開き、参加者は入る: create_server() と create_client()。作ったピアを multiplayer_peer に入れて使う
  • キャラを出すのはホストだけ: MultiplayerSpawner が参加者のコピーにも出す。出す処理は is_server() で守る
  • 動かすのは持ち主だけ: _enter_tree() で持ち主を決め、動かす前に is_multiplayer_authority() を見る
  • 位置は写す: MultiplayerSynchronizer の Replication で position を足し、Always にする

同じPCの2つのウィンドウで2人が動いたら、ネットワークのゲームの骨組みはできています。自分のゲームでキャラを出すとき、そのキャラの持ち主は誰になるでしょうか。

次に読む学習パスの次の記事
ネットワーク

Godotのマルチプレイ同期入門:参加者が撃った弾と敵を、全員の画面でそろえる

この記事を読む
Godot このセクションのノート104