feat: implement initial lobby scene with main menu, server browser, and networking options.

This commit is contained in:
Yogi Wiguna
2026-03-16 16:19:30 +08:00
parent 64dc1de15a
commit eb018903aa
6 changed files with 317 additions and 35 deletions
+93 -7
View File
@@ -36,6 +36,8 @@ var current_room: Dictionary = {}
var players_in_room: Array = [] # [{id, name, is_ready}]
var available_rooms: Array = []
var is_host: bool = false
var is_lan_mode: bool = false # True when using direct ENet (no Nakama)
const LAN_PORT: int = 7777 # Port for LAN direct connections
var local_player_name: String = "Player"
# Match duration in seconds (configurable in lobby by host)
@@ -113,8 +115,9 @@ func _update_available_areas(mode: String) -> void:
# =============================================================================
func create_room(room_name: String) -> void:
"""Host creates a new room with the given name."""
"""Host creates a new room with the given name (Nakama)."""
is_host = true
is_lan_mode = false
current_room = {
"room_name": room_name,
"host_name": local_player_name,
@@ -130,8 +133,9 @@ func create_room(room_name: String) -> void:
NakamaManager.host_game()
func join_room(match_id: String) -> void:
"""Client joins an existing room by match ID."""
"""Client joins an existing room by match ID (Nakama)."""
is_host = false
is_lan_mode = false
var success = await NakamaManager.connect_to_nakama_async()
if not success:
@@ -140,6 +144,66 @@ func join_room(match_id: String) -> void:
NakamaManager.join_game(match_id)
# =============================================================================
# LAN Mode (Direct ENet, no Nakama/Docker needed)
# =============================================================================
func create_room_lan(room_name: String = "LAN Game") -> bool:
"""Host creates a LAN room via direct ENet. No Nakama/Docker required."""
is_host = true
is_lan_mode = true
var peer = ENetMultiplayerPeer.new()
var err = peer.create_server(LAN_PORT, GameStateManager.max_players)
if err != OK:
push_error("[LAN] Failed to create ENet server on port %d: %s" % [LAN_PORT, err])
return false
multiplayer.set_multiplayer_peer(peer)
current_room = {
"room_name": room_name,
"host_name": local_player_name,
"max_players": GameStateManager.max_players,
"match_id": "LAN"
}
# Add host to player list
var my_id = multiplayer.get_unique_id() # Will be 1
players_in_room.clear()
players_in_room.append({
"id": my_id,
"name": local_player_name,
"is_ready": false,
"character": available_characters[local_character_index]
})
print("[LAN] Server created on port %d. Waiting for players..." % LAN_PORT)
emit_signal("room_joined", current_room)
return true
func join_room_lan(host_ip: String) -> bool:
"""Client joins a LAN room by the host's IP address. No Nakama/Docker required."""
is_host = false
is_lan_mode = true
var peer = ENetMultiplayerPeer.new()
var err = peer.create_client(host_ip, LAN_PORT)
if err != OK:
push_error("[LAN] Failed to connect to %s:%d: %s" % [host_ip, LAN_PORT, err])
return false
multiplayer.set_multiplayer_peer(peer)
current_room = {
"room_name": "LAN Game",
"match_id": "LAN"
}
print("[LAN] Connecting to %s:%d..." % [host_ip, LAN_PORT])
# _on_peer_connected will fire once connected and trigger request_room_info.
return true
func leave_room() -> void:
"""Leave the current room."""
print("[LobbyManager] Leaving room. Clearing all local state.")
@@ -147,15 +211,19 @@ func leave_room() -> void:
# If we are the host, notify all clients to kick them back to menu/lobby
if is_host and multiplayer.has_multiplayer_peer() and multiplayer.is_server():
print("[LobbyManager] Host is leaving. Kicking all clients...")
# We use rpc() instead of .rpc() for compatibility with older Godot 4 versions if applicable,
# but .rpc() is standard in 4.x. Let's stick to standard.
kick_all_clients.rpc()
# Important: Reset all lobby settings and player lists first
reset()
# Disconnect from Nakama and reset multiplayer peer
NakamaManager.cleanup()
if is_lan_mode:
# LAN mode: just close the ENet peer directly
if multiplayer.has_multiplayer_peer():
multiplayer.set_multiplayer_peer(null)
is_lan_mode = false
else:
# Nakama mode: full Nakama cleanup
NakamaManager.cleanup()
# Important: Clean up game state as well to prevent ghost players
GameStateManager.reset()
@@ -206,7 +274,9 @@ func sync_ready_state(player_id: int, is_ready: bool) -> void:
func _check_all_ready() -> void:
"""Check if all players are ready."""
if players_in_room.size() < 2:
# In LAN mode allow solo play - only 1 player needed
var min_players = 1 if is_lan_mode else 2
if players_in_room.size() < min_players:
_all_ready = false
return
@@ -218,6 +288,17 @@ func _check_all_ready() -> void:
_all_ready = true
emit_signal("all_players_ready")
func force_solo_ready() -> void:
"""Mark the local player as ready immediately (for solo LAN play)."""
if not multiplayer.has_multiplayer_peer():
return
var my_id = multiplayer.get_unique_id()
for player in players_in_room:
if player["id"] == my_id:
player["is_ready"] = true
break
_check_all_ready()
func is_all_ready() -> bool:
return _all_ready
@@ -562,6 +643,11 @@ func _on_game_starting() -> void:
func _on_match_joined(match_id: String) -> void:
"""Called when successfully joined a Nakama match."""
# LAN mode handles room setup entirely in create_room_lan() / join_room_lan().
# Skip this Nakama-specific handler to avoid double-adding the player.
if is_lan_mode:
return
current_room["match_id"] = match_id
# Use first 8 chars of match ID as room name (matches server browser)
var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id
+93 -9
View File
@@ -38,6 +38,7 @@ var is_loading: bool = false
# Server Selection Controls
var server_option: OptionButton
var server_ip_input: LineEdit
var lan_section: VBoxContainer # LAN-specific controls
func _ready() -> void:
_connect_signals()
@@ -203,7 +204,7 @@ func _setup_server_config_ui() -> void:
# Server Label
var label = Label.new()
label.text = "NAKAMA SERVER"
label.text = "CONNECTION MODE"
label.add_theme_color_override("font_color", Color(0.69, 0.529, 0.357, 1))
label.add_theme_font_size_override("font_size", 13)
server_section.add_child(label)
@@ -212,8 +213,9 @@ func _setup_server_config_ui() -> void:
server_option = OptionButton.new()
server_option.name = "ServerOption"
server_option.custom_minimum_size = Vector2(0, 44)
server_option.add_item("Localhost (Testing)")
server_option.add_item("Remote Server (Host IP)")
server_option.add_item("Nakama - Localhost (Testing)")
server_option.add_item("Nakama - Remote Server (Host IP)")
server_option.add_item("LAN Direct (No Server)")
# Set initial state based on NakamaManager
if NakamaManager.nakama_host == "localhost":
@@ -224,17 +226,62 @@ func _setup_server_config_ui() -> void:
server_option.item_selected.connect(_on_server_option_selected)
server_section.add_child(server_option)
# Server IP Input
# Nakama Server IP Input
server_ip_input = LineEdit.new()
server_ip_input.name = "ServerIPInput"
server_ip_input.custom_minimum_size = Vector2(0, 44)
server_ip_input.placeholder_text = "Enter Server IP Address..."
server_ip_input.placeholder_text = "Enter Nakama Server IP..."
server_ip_input.text = NakamaManager.nakama_host if NakamaManager.nakama_host != "localhost" else "127.0.0.1"
server_ip_input.visible = server_option.selected == 1
server_ip_input.text_submitted.connect(_on_server_ip_submitted)
server_ip_input.focus_exited.connect(func(): _on_server_ip_submitted(server_ip_input.text))
server_section.add_child(server_ip_input)
# --- LAN Section ---
lan_section = VBoxContainer.new()
lan_section.name = "LANSection"
lan_section.add_theme_constant_override("separation", 8)
lan_section.visible = false
server_section.add_child(lan_section)
var lan_info = Label.new()
lan_info.text = "Play over LAN without any server.\nFirewall may need to allow port 7777."
lan_info.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7, 1))
lan_info.add_theme_font_size_override("font_size", 12)
lan_info.autowrap_mode = TextServer.AUTOWRAP_WORD
lan_section.add_child(lan_info)
# Host LAN button
var lan_host_btn = Button.new()
lan_host_btn.name = "LANHostBtn"
lan_host_btn.text = "HOST LAN GAME"
lan_host_btn.custom_minimum_size = Vector2(0, 44)
lan_host_btn.pressed.connect(_on_lan_host_pressed)
lan_section.add_child(lan_host_btn)
var lan_sep = Label.new()
lan_sep.text = "── or join a friend ──"
lan_sep.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
lan_sep.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5, 1))
lan_sep.add_theme_font_size_override("font_size", 11)
lan_section.add_child(lan_sep)
# LAN Host IP input
var lan_ip = LineEdit.new()
lan_ip.name = "LANIPInput"
lan_ip.custom_minimum_size = Vector2(0, 44)
lan_ip.placeholder_text = "Host IP (e.g. 192.168.1.10)"
lan_ip.text = "127.0.0.1"
lan_section.add_child(lan_ip)
# Join LAN button
var lan_join_btn = Button.new()
lan_join_btn.name = "LANJoinBtn"
lan_join_btn.text = "JOIN LAN GAME"
lan_join_btn.custom_minimum_size = Vector2(0, 44)
lan_join_btn.pressed.connect(func(): _on_lan_join_pressed(lan_ip.text))
lan_section.add_child(lan_join_btn)
# Add a separator after the section
var separator = HSeparator.new()
vbox.add_child(separator)
@@ -242,18 +289,55 @@ func _setup_server_config_ui() -> void:
func _on_server_option_selected(index: int) -> void:
if index == 0:
# Localhost
# Nakama Localhost
server_ip_input.visible = false
if lan_section: lan_section.visible = false
NakamaManager.set_server("localhost")
else:
# Remote
elif index == 1:
# Nakama Remote
server_ip_input.visible = true
if lan_section: lan_section.visible = false
NakamaManager.set_server(server_ip_input.text)
else:
# LAN Direct
server_ip_input.visible = false
if lan_section: lan_section.visible = true
func _on_server_ip_submitted(new_text: String) -> void:
if server_option.selected == 1:
if server_option and server_option.selected == 1:
NakamaManager.set_server(new_text.strip_edges())
func _on_lan_host_pressed() -> void:
"""Host a LAN game without logging in to Nakama."""
var player_name = email_input.text.strip_edges()
if player_name.is_empty():
player_name = "Host"
LobbyManager.local_player_name = player_name
var ok = await LobbyManager.create_room_lan()
if ok:
_go_to_lobby()
else:
_show_error("Failed to create LAN server. Check firewall for port 7777.")
func _on_lan_join_pressed(host_ip: String) -> void:
"""Join a LAN game without logging in to Nakama."""
var ip = host_ip.strip_edges()
if ip.is_empty():
_show_error("Please enter the host's IP address.")
return
var player_name = email_input.text.strip_edges()
if player_name.is_empty():
player_name = "Player"
LobbyManager.local_player_name = player_name
var ok = LobbyManager.join_room_lan(ip)
if ok:
_go_to_lobby()
else:
_show_error("Failed to connect to %s. Is the host running?" % ip)
# =============================================================================
# Registration Handlers
# =============================================================================