Multiplayer LAN Server List Help

Godot 4.4

Hello! I’m working on a retro styled boomer shooter FPS game and i want it to have a fun multiplayer feature built in where you can either play the campaign solo or host a server for others to see on their server list and join.

I’ve got it set up so you can type in a server name, your own username, and host it, and you’ll be put into the game just fine.

(When you host it you’re put into the game and your player and the basic map is spawned, your username you entered is also displayed for you in the screen for debug to show if you have the correct assigned name.)

The problem comes when i have another game of it running through 2 instances, the first player can start a server and get in-game, but the other player either: sees no server on their list at all, completely empty server list, or, they see two of the same server, both with only the default text, not the text it’s supposed to have for individual server info.

Here is how i’ve set up my scene:

Basically when you host a server i need it to send it to all of the other players to update the server list and show it so they can join it and join in the same world, so multiple people can be hosting servers and playing with different groups of people.

Here is my MultiplayerLAN.gd:


@export var default_server_port := 6767
@export var player_scene := preload("res://Player/player.tscn")

@onready var server_browser := $ServerBrowser
@onready var username_input := $Username
@onready var servername_input := $ServerName

var peer : ENetMultiplayerPeer
var my_player
var server_port := 0

func _ready():
	multiplayer.peer_connected.connect(peer_connected)
	multiplayer.peer_disconnected.connect(peer_disconnected)
	multiplayer.connected_to_server.connect(connected_to_server)
	multiplayer.connection_failed.connect(connection_failed)

	server_browser.joinGame.connect(_on_join_server_selected)

# --- HOST ---
func host_game():
	var server_name = servername_input.text
	var player_name = username_input.text
	server_port = default_server_port

	peer = ENetMultiplayerPeer.new()
	var err = peer.create_server(server_port, 8)
	if err != OK:
		print("Cannot create server: port may be in use")
		return

	multiplayer.set_multiplayer_peer(peer)
	print("Server started on port:", server_port, "Name:", server_name)

	# Start broadcasting
	server_browser.set_up_broadcast(server_name, server_port)

	# Spawn host player
	spawn_player(multiplayer.get_unique_id(), player_name)

	visible = false

# --- SPAWN PLAYER ---
func spawn_player(peer_id: int, player_name: String):
	if get_tree().current_scene.has_node(str(peer_id)):
		return
	var player_instance = player_scene.instantiate()
	player_instance.name = str(peer_id)
	player_instance.set_multiplayer_authority(peer_id)
	if peer_id == multiplayer.get_unique_id():
		player_instance.is_local_player = true
		my_player = player_instance
	var viewport = $"../Screen/SubViewport"
	viewport.add_child(player_instance)
	if player_instance.has_node("Username"):
		player_instance.get_node("Username").text = player_name

# --- RPC ---
@rpc("any_peer")
func rpc_spawn_player(peer_id: int, player_name: String):
	spawn_player(peer_id, player_name)

# --- CONNECT TO SERVER ---
func _on_join_server_selected(ip: String, port: int):
	peer = ENetMultiplayerPeer.new()
	var err = peer.create_client(ip, port)
	if err != OK:
		print("Failed to create client!")
		return
	multiplayer.set_multiplayer_peer(peer)

func connected_to_server():
	var player_name = username_input.text
	rpc_id(1, "rpc_spawn_player", multiplayer.get_unique_id(), player_name)
	print("Connected to server!")

func connection_failed():
	print("Connection failed!")

# --- PLAYER CONNECT/DISCONNECT ---
func peer_connected(id):
	print("Player connected:", id)

func peer_disconnected(id):
	print("Player disconnected:", id)
	var player = get_tree().current_scene.get_node_or_null(str(id))
	if player:
		player.queue_free()

My ServerBrowser.gd:


signal joinGame(ip: String, port: int)

@export var listen_port: int = 7777
@export var broadcast_port: int = 6767
@export var broadcast_address: String = "255.255.255.255"
@export var broadcast_interval: float = 1.0
@export var server_port: int = 6767

@onready var server_list: VBoxContainer = $Panel/ServerList
@export var server_info_scene = load("res://Multiplayer/server_info.tscn")

var listener: PacketPeerUDP
var broadcaster: PacketPeerUDP
var discovered_servers := {} # key = "ip:port"
var is_hosting := false
var server_name := "Server"

var room_info := {
	"name": "Server",
	"playerCount": 0,
	"port": 6767
}

func _ready() -> void:
	print("[ServerBrowser] Ready, setting up listener...")
	_setup_listener()
	_start_broadcast_timer()

# ----------------------------
# Listener Setup
# ----------------------------
func _setup_listener() -> void:
	listener = PacketPeerUDP.new()
	var err := listener.bind(listen_port, "0.0.0.0")
	if err == OK:
		print("[ServerBrowser] Bound to listener port", listen_port)
	else:
		print("[ServerBrowser] Failed to bind listener port", listen_port, "Error:", err)
	set_process(true)

# ----------------------------
# Broadcaster Setup
# ----------------------------
func set_up_broadcast(name: String, port: int) -> void:
	server_name = name
	server_port = port
	room_info.name = name
	room_info.port = port
	is_hosting = true

	print("[ServerBrowser] Setting up broadcaster for server: %s on port %d" % [name, port])
	broadcaster = PacketPeerUDP.new()
	broadcaster.set_broadcast_enabled(true)
	print("[ServerBrowser] Broadcast timer started")

func _on_BroadcastTimer_timeout() -> void:
	if not is_hosting or broadcaster == null:
		print("[ServerBrowser] Broadcaster not initialized yet!")
		return

	# Update player count
	room_info.playerCount = GameManager.Players.size() if Engine.has_singleton("GameManager") else 1
	var data := JSON.stringify(room_info)

	# Set broadcast destination
	broadcaster.set_dest_address(broadcast_address, listen_port)
	broadcaster.put_packet(data.to_utf8_buffer())
	print("[ServerBrowser] Broadcast sent:", data)

# ----------------------------
# Process Incoming Broadcasts
# ----------------------------
func _process(delta: float) -> void:
	if listener == null:
		return

	while listener.get_available_packet_count() > 0:
		var server_ip: String = listener.get_packet_ip()
		var packet_bytes: PackedByteArray = listener.get_packet()
		if packet_bytes.size() == 0:
			continue

		var packet_str: String = packet_bytes.get_string_from_utf8()
		var incoming_info = JSON.parse_string(packet_str) # returns Dictionary
		if typeof(incoming_info) != TYPE_DICTIONARY:
			print("[ServerBrowser] Failed to parse server broadcast:", packet_str)
			continue

		var server_port: int = int(incoming_info.get("port", broadcast_port))
		var server_key: String = "%s:%d" % [server_ip, server_port]

		if not discovered_servers.has(server_key):
			if server_info_scene == null:
				push_error("[ServerBrowser] server_info_scene is null! Assign your ServerInfo.tscn in the editor.")
				continue

			var entry: Control = server_info_scene.instantiate()

			# Immediately set info BEFORE adding to scene
			entry.update_server_info({
				"name": incoming_info.get("name", "Unknown"),
				"players": incoming_info.get("playerCount", 0),
				"max_players": 16,
				"ip": server_ip,
				"port": server_port
			})

			# Connect join signal
			entry.joinGame.connect(join_by_ip)

			# Add to scene AFTER info is set
			server_list.add_child(entry)

			discovered_servers[server_key] = entry

			print("[ServerBrowser] Added server:", incoming_info.get("name", "Unknown"), "IP:", server_ip, "Port:", server_port)


		# No need to update entries if info is set correctly at spawn

# ----------------------------
# Join Server Signal
# ----------------------------
func join_by_ip(ip: String, port: int) -> void:
	print("[ServerBrowser] Join requested for IP:", ip, "Port:", port)
	emit_signal("joinGame", ip, port)

# ----------------------------
# Timer Setup
# ----------------------------
func _start_broadcast_timer() -> void:
	var timer: Timer
	if has_node("BroadcastTimer"):
		timer = $BroadcastTimer
	else:
		timer = Timer.new()
		timer.name = "BroadcastTimer"
		timer.wait_time = broadcast_interval
		timer.autostart = true
		timer.one_shot = false
		add_child(timer)
	timer.timeout.connect(_on_BroadcastTimer_timeout)

# ----------------------------
# Clean up
# ----------------------------
func _exit_tree() -> void:
	if listener != null:
		listener.close()
	if broadcaster != null:
		broadcaster.close()

ServerDiscovery.gd:


signal server_found(info: Dictionary)

@export var listen_port: int = 7777

var listener: PacketPeerUDP
var discovered_servers := {}

func _ready():
	listener = PacketPeerUDP.new()
	var result = listener.bind(listen_port)
	if result != OK:
		push_error("[ServerDiscovery] ❌ Failed to bind UDP listener on port %d" % listen_port)
	else:
		print("[ServerDiscovery] Listening on port %d for broadcasts..." % listen_port)
	set_process(true)

func _process(_delta):
	if listener == null:
		return

	while listener.get_available_packet_count() > 0:
		var sender_ip := listener.get_packet_ip()
		var bytes := listener.get_packet()
		if bytes.is_empty():
			continue

		var json := JSON.new()
		var parse_result := json.parse(bytes.get_string_from_utf8())
		if parse_result != OK:
			print("[ServerDiscovery] ⚠️ Invalid JSON from %s" % sender_ip)
			continue

		var info: Dictionary = json.get_data()
		if typeof(info) != TYPE_DICTIONARY:
			print("[ServerDiscovery] ⚠️ Non-dictionary broadcast from %s" % sender_ip)
			continue

		# Get port from broadcast or default
		var port := int(info.get("port", listen_port))
		var key := "%s:%d" % [sender_ip, port]

		if not discovered_servers.has(key):
			info["ip"] = sender_ip  # Add IP field manually
			discovered_servers[key] = info
			print("[ServerDiscovery] 🟢 Found new server: %s (%s:%d)" %
				[info.get("name", "Unknown"), sender_ip, port])
			emit_signal("server_found", info)
		else:
			# Optional: update existing entry
			discovered_servers[key] = info

And my ServerInfo.gd: (the scene that needs to be instantiated in the server list to show active servers and their info)

extends Control

signal joinGame(ip: String, port: int)

var server_info := {}

@onready var server_name_label = $ServerName
@onready var players_label = $PlayersLabel
@onready var join_button = $Button

func _ready():
	join_button.pressed.connect(_on_join_button_pressed)

# Properly update the server info dictionary, including IP & port
func update_server_info(info: Dictionary) -> void:
	server_info = info
	if server_name_label:
		server_name_label.text = str(server_info.get("name", "Unknown"))
	if players_label:
		var players = int(server_info.get("playerCount", 0))
		var max_players = int(server_info.get("maxPlayers", 16))
		players_label.text = "%d/%d" % [players, max_players]

# Emit signal with correct IP & port
func _on_join_button_pressed() -> void:
	if server_info.size() == 0:
		return
	var ip = str(server_info.get("ip", ""))
	var port = int(server_info.get("port", 6767))
	if ip == "":
		push_error("Server IP missing! Cannot join server.")
		return
	emit_signal("joinGame", ip, port)

If anyone can help me out that would be amazing!!! Thank you so much

Do you get any warnings or errors?

When testing multiple instances on one machine it’s important to note one port can only be opened by one instance at a time. In affect only one instance can broadcast, while the others can listen.

If your current scene is changed to facilitate gameplay then your BroadcastTimer may be destroyed, is this “multiplayer_lan” scene a global? or instantiated along side gameplay?

The MultiplayerLAN scene is a child node of the World scene in the game, where the player character and map are spawned in aswell. there are no errors that i’m getting but i might try and stop broadcasting when you get spawned in the game after hosting later.

My World scene looks like this


I can start a server with player 1 and spawn in just fine, but player 2 sees no server on their server list. This is my debug output after testing it:

[ServerBrowser] Ready, setting up listener...
[ServerBrowser] Bound to listener port8411
[ServerDiscovery] Listening on port 7777 for broadcasts...
[ServerBrowser] Ready, setting up listener...
[ServerBrowser] Bound to listener port8415
[ServerBrowser] Ready, setting up listener...
[ServerBrowser] Bound to listener port8758
[ServerBrowser] Ready, setting up listener...
[ServerBrowser] Bound to listener port8327
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
Server started on port:6767Name:server
[ServerBrowser] Setting up broadcaster for server: server on port 6767
[ServerBrowser] Broadcast timer started
[DEBUG] add_player called - peer_id:0username:is_local:true
[DEBUG] multiplayer unique_id:1
[DEBUG] multiplayer is_server:true
[DEBUG] add_player called - peer_id:0username:is_local:true
[DEBUG] _ready() called for player:local:true
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcast sent:{"ip":"0:0:0:0:0:0:0:1","name":"server","port":6767}
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcast sent:{"ip":"0:0:0:0:0:0:0:1","name":"server","port":6767}
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcast sent:{"ip":"0:0:0:0:0:0:0:1","name":"server","port":6767}
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcaster not initialized yet!
[ServerBrowser] Broadcast sent:{"ip":"0:0:0:0:0:0:0:1","name":"server","port":6767}

Here is my current ServerBrowser script:

extends Control

signal joinGame(ip: String, port: int)

@export var listen_port: int = 7777
@export var broadcast_port: int = 6767
@export var broadcast_address: String = "255.255.255.255"
@export var broadcast_interval: float = 1.0
@export var server_port: int = 6767

@onready var server_list: VBoxContainer = $Panel/ServerList
@export var server_info_scene = load("res://Multiplayer/server_info.tscn")

var listener: PacketPeerUDP
var broadcaster: PacketPeerUDP
var discovered_servers := {} # key = "ip:port"
var is_hosting := false
var server_name := "Server"

var room_info := {
	"name": "Server",
	"playerCount": 0,
	"port": 6767
}

func _ready() -> void:
	print("[ServerBrowser] Ready, setting up listener...")
	_setup_listener()
	_start_broadcast_timer()

# ----------------------------
# Listener Setup
# ----------------------------
func _setup_listener() -> void:
	listen_port += randi_range(0, 1000) # Randomize listener port
	listener = PacketPeerUDP.new()
	var err := listener.bind(listen_port, "0.0.0.0")
	if err == OK:
		print("[ServerBrowser] Bound to listener port", listen_port)
	else:
		print("[ServerBrowser] Failed to bind listener port", listen_port, "Error:", err)
	set_process(true)


# ----------------------------
# Broadcaster Setup
# ----------------------------
func set_up_broadcast(name: String, port: int) -> void:
	server_name = name
	server_port = port
	room_info.name = name
	room_info.port = port
	is_hosting = true

	print("[ServerBrowser] Setting up broadcaster for server: %s on port %d" % [name, port])
	broadcaster = PacketPeerUDP.new()
	broadcaster.set_broadcast_enabled(true)
	print("[ServerBrowser] Broadcast timer started")

func _on_BroadcastTimer_timeout() -> void:
	if broadcaster == null:
		print("[ServerBrowser] Broadcaster not initialized yet!")
		return

	var packet = {
	"name": server_name,
	"ip": IP.get_local_addresses()[0],
	"port": server_port
}

	var json = JSON.stringify(packet)
	broadcaster.put_packet(json.to_utf8_buffer())
	print("[ServerBrowser] Broadcast sent:", json)



# ----------------------------
# Process Incoming Broadcasts
# ----------------------------
func _process(delta: float) -> void:
	if listener == null:
		return

	while listener.get_available_packet_count() > 0:
		var server_ip: String = listener.get_packet_ip()
		var packet_bytes: PackedByteArray = listener.get_packet()
		if packet_bytes.size() == 0:
			continue

		var packet_str: String = packet_bytes.get_string_from_utf8()
		var incoming_info = JSON.parse_string(packet_str) # returns Dictionary
		if typeof(incoming_info) != TYPE_DICTIONARY:
			print("[ServerBrowser] Failed to parse server broadcast:", packet_str)
			continue

		var server_port: int = int(incoming_info.get("port", broadcast_port))
		var server_key: String = "%s:%d" % [server_ip, server_port]

		if not discovered_servers.has(server_key):
			if server_info_scene == null:
				push_error("[ServerBrowser] server_info_scene is null! Assign your ServerInfo.tscn in the editor.")
				continue

			var entry: Control = server_info_scene.instantiate()

			# Immediately set info BEFORE adding to scene
			entry.update_server_info({
				"name": incoming_info.get("name", "Unknown"),
				"players": incoming_info.get("playerCount", 0),
				"max_players": 16,
				"ip": server_ip,
				"port": server_port
			})

			# Connect join signal
			entry.joinGame.connect(join_by_ip)

			# Add to scene AFTER info is set
			server_list.add_child(entry)

			discovered_servers[server_key] = entry

			print("[ServerBrowser] Added server:", incoming_info.get("name", "Unknown"), "IP:", server_ip, "Port:", server_port)


		# No need to update entries if info is set correctly at spawn

# ----------------------------
# Join Server Signal
# ----------------------------
func join_by_ip(ip: String, port: int) -> void:
	print("[ServerBrowser] Join requested for IP:", ip, "Port:", port)
	emit_signal("joinGame", ip, port)

# ----------------------------
# Timer Setup
# ----------------------------
func _start_broadcast_timer() -> void:
	var timer: Timer
	if has_node("BroadcastTimer"):
		timer = $BroadcastTimer
	else:
		timer = Timer.new()
		timer.name = "BroadcastTimer"
		timer.wait_time = broadcast_interval
		timer.autostart = true
		timer.one_shot = false
		add_child(timer)
	timer.timeout.connect(_on_BroadcastTimer_timeout)

# ----------------------------
# Clean up
# ----------------------------
func _exit_tree() -> void:
	if listener != null:
		listener.close()
	if broadcaster != null:
		broadcaster.close()

Seems like you dropped set_dest_address which is needed. Add that back in after you create the broadcaster

print("[ServerBrowser] Setting up broadcaster for server: %s on port %d" % [name, port])
broadcaster = PacketPeerUDP.new()
broadcaster.set_broadcast_enabled(true)
broadcaster.set_dest_address(broadcast_address, listen_port)

Ah i see, i’ve added back in the set_dest_address but it seems to be behaving the same way.