test implement webrtc

This commit is contained in:
2026-01-14 18:25:07 +01:00
parent 0c0998cd05
commit f71b510cfc
9 changed files with 1091 additions and 56 deletions

View File

@@ -122,6 +122,28 @@ theme = SubResource("Theme_standard_font")
[node name="HBoxContainer" type="HBoxContainer" parent="UpperRight" unique_id=332290975]
layout_mode = 2
[node name="VBoxContainerHost" type="VBoxContainer" parent="UpperRight/HBoxContainer" unique_id=1933444958]
layout_mode = 2
size_flags_horizontal = 3
[node name="LabelHost" type="Label" parent="UpperRight/HBoxContainer/VBoxContainerHost" unique_id=1807484687]
layout_mode = 2
theme = SubResource("Theme_standard_font")
text = "HOST"
horizontal_alignment = 1
[node name="LabelPlayerCount" type="Label" parent="UpperRight/HBoxContainer/VBoxContainerHost" unique_id=1807484688]
layout_mode = 2
theme = SubResource("Theme_standard_font")
text = "Players: 1"
horizontal_alignment = 1
[node name="LabelRoomCode" type="Label" parent="UpperRight/HBoxContainer/VBoxContainerHost" unique_id=1807484689]
layout_mode = 2
theme = SubResource("Theme_standard_font")
text = ""
horizontal_alignment = 1
[node name="VBoxContainerBoss" type="VBoxContainer" parent="UpperRight/HBoxContainer" unique_id=1933444957]
layout_mode = 2
size_flags_horizontal = 3

View File

@@ -1,11 +1,11 @@
[gd_scene format=3 uid="uid://d4fgxay8kqp5u"]
[ext_resource type="Script" path="res://scripts/game_ui.gd" id="1"]
[ext_resource type="Script" uid="uid://ofhtysy8r43v" path="res://scripts/game_ui.gd" id="1"]
[node name="GameUI" type="CanvasLayer"]
[node name="GameUI" type="CanvasLayer" unique_id=2051025577]
script = ExtResource("1")
[node name="Control" type="Control" parent="."]
[node name="Control" type="Control" parent="." unique_id=1160035312]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
@@ -13,7 +13,7 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="MainMenu" type="Panel" parent="Control"]
[node name="MainMenu" type="Panel" parent="Control" unique_id=1231078062]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
@@ -27,7 +27,7 @@ offset_bottom = 250.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="Control/MainMenu"]
[node name="VBoxContainer" type="VBoxContainer" parent="Control/MainMenu" unique_id=1287690408]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
@@ -42,65 +42,85 @@ grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 20
[node name="Title" type="Label" parent="Control/MainMenu/VBoxContainer"]
[node name="Title" type="Label" parent="Control/MainMenu/VBoxContainer" unique_id=1219314113]
layout_mode = 2
text = "Multiplayer Coop RPG"
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2
[node name="Spacer1" type="Control" parent="Control/MainMenu/VBoxContainer"]
[node name="Spacer1" type="Control" parent="Control/MainMenu/VBoxContainer" unique_id=1826786842]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
[node name="LocalPlayersContainer" type="HBoxContainer" parent="Control/MainMenu/VBoxContainer"]
[node name="NetworkModeContainer" type="HBoxContainer" parent="Control/MainMenu/VBoxContainer" unique_id=1234567890]
layout_mode = 2
[node name="Label" type="Label" parent="Control/MainMenu/VBoxContainer/LocalPlayersContainer"]
[node name="Label" type="Label" parent="Control/MainMenu/VBoxContainer/NetworkModeContainer" unique_id=1234567891]
layout_mode = 2
size_flags_horizontal = 3
text = "Network Mode:"
vertical_alignment = 1
[node name="NetworkModeOption" type="OptionButton" parent="Control/MainMenu/VBoxContainer/NetworkModeContainer" unique_id=1234567892]
layout_mode = 2
size_flags_horizontal = 3
selected = 0
item_count = 3
popup/item_0/text = "ENet (PC/LAN)"
popup/item_0/id = 0
popup/item_1/text = "WebRTC (Browser/Web)"
popup/item_1/id = 1
popup/item_2/text = "WebSocket (Cross-Platform)"
popup/item_2/id = 2
[node name="LocalPlayersContainer" type="HBoxContainer" parent="Control/MainMenu/VBoxContainer" unique_id=1860926318]
layout_mode = 2
[node name="Label" type="Label" parent="Control/MainMenu/VBoxContainer/LocalPlayersContainer" unique_id=1617550731]
layout_mode = 2
size_flags_horizontal = 3
text = "Local Players:"
vertical_alignment = 1
[node name="SpinBox" type="SpinBox" parent="Control/MainMenu/VBoxContainer/LocalPlayersContainer"]
[node name="SpinBox" type="SpinBox" parent="Control/MainMenu/VBoxContainer/LocalPlayersContainer" unique_id=1805678257]
layout_mode = 2
min_value = 1.0
max_value = 4.0
value = 1.0
[node name="AddressContainer" type="HBoxContainer" parent="Control/MainMenu/VBoxContainer"]
[node name="AddressContainer" type="HBoxContainer" parent="Control/MainMenu/VBoxContainer" unique_id=69104887]
layout_mode = 2
[node name="Label" type="Label" parent="Control/MainMenu/VBoxContainer/AddressContainer"]
[node name="Label" type="Label" parent="Control/MainMenu/VBoxContainer/AddressContainer" unique_id=1727520435]
layout_mode = 2
size_flags_horizontal = 3
text = "Server Address:"
vertical_alignment = 1
[node name="AddressInput" type="LineEdit" parent="Control/MainMenu/VBoxContainer/AddressContainer"]
[node name="AddressInput" type="LineEdit" parent="Control/MainMenu/VBoxContainer/AddressContainer" unique_id=314768458]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "127.0.0.1"
expand_to_text_length = true
placeholder_text = "ruinborn.thefirstboss.com"
[node name="Spacer2" type="Control" parent="Control/MainMenu/VBoxContainer"]
[node name="Spacer2" type="Control" parent="Control/MainMenu/VBoxContainer" unique_id=1061067008]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
[node name="HostButton" type="Button" parent="Control/MainMenu/VBoxContainer"]
[node name="HostButton" type="Button" parent="Control/MainMenu/VBoxContainer" unique_id=268532531]
layout_mode = 2
text = "Host Game"
[node name="JoinButton" type="Button" parent="Control/MainMenu/VBoxContainer"]
[node name="JoinButton" type="Button" parent="Control/MainMenu/VBoxContainer" unique_id=417556513]
layout_mode = 2
text = "Join Game"
[node name="Spacer3" type="Control" parent="Control/MainMenu/VBoxContainer"]
[node name="Spacer3" type="Control" parent="Control/MainMenu/VBoxContainer" unique_id=1966231585]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
size_flags_vertical = 3
[node name="Instructions" type="Label" parent="Control/MainMenu/VBoxContainer"]
[node name="Instructions" type="Label" parent="Control/MainMenu/VBoxContainer" unique_id=1999421718]
layout_mode = 2
text = "Controls:
WASD/Arrows - Move (P1) | Gamepad (P2+)
@@ -108,4 +128,3 @@ E/A (Tap) - Lift/Throw
E/A (Hold) - Push/Pull"
horizontal_alignment = 1
autowrap_mode = 2

View File

@@ -5,6 +5,8 @@ extends CanvasLayer
@onready var main_menu = $Control/MainMenu
@onready var host_button = $Control/MainMenu/VBoxContainer/HostButton
@onready var join_button = $Control/MainMenu/VBoxContainer/JoinButton
@onready var network_mode_option = $Control/MainMenu/VBoxContainer/NetworkModeContainer/NetworkModeOption
@onready var network_mode_container = $Control/MainMenu/VBoxContainer/NetworkModeContainer
@onready var local_players_spinbox = $Control/MainMenu/VBoxContainer/LocalPlayersContainer/SpinBox
@onready var address_input = $Control/MainMenu/VBoxContainer/AddressContainer/AddressInput
@@ -32,6 +34,27 @@ func _ready():
host_button.pressed.connect(_on_host_pressed)
join_button.pressed.connect(_on_join_pressed)
# Setup network mode dropdown
if network_mode_option:
network_mode_option.item_selected.connect(_on_network_mode_changed)
# On web builds, filter out ENet option (only WebRTC and WebSocket available)
if OS.get_name() == "Web":
print("GameUI: Web platform detected, filtering network mode options")
# Remove ENet option (index 0) for web builds
network_mode_option.remove_item(0)
# Adjust selected index (was 0 for ENet, now 0 is WebRTC)
network_mode_option.selected = 0
# Update network manager to use WebRTC by default
network_manager.set_network_mode(1)
else:
# On native builds, default to ENet (index 0)
network_mode_option.selected = 0
network_manager.set_network_mode(0)
# Update address input placeholder based on initial mode
_on_network_mode_changed(network_mode_option.selected)
# Connect network signals
if network_manager:
network_manager.connection_succeeded.connect(_on_connection_succeeded)
@@ -50,7 +73,8 @@ func _check_command_line_args():
var should_host = false
var should_join = false
var should_debug = false
var join_address = "127.0.0.1"
var force_webrtc = false
var join_address = "ruinborn.thefirstboss.com"
var local_count = 1
for arg in args:
@@ -60,6 +84,9 @@ func _check_command_line_args():
elif arg == "--join":
should_join = true
print("GameUI: Found --join argument")
elif arg == "--websocket" or arg == "--webrtc":
force_webrtc = true
print("GameUI: Found --websocket/--webrtc argument (forcing WebSocket mode)")
elif arg == "--room-debug":
should_debug = true
print("GameUI: Found --room-debug argument")
@@ -68,7 +95,18 @@ func _check_command_line_args():
elif arg.begins_with("--players="):
local_count = int(arg.split("=")[1])
print("GameUI: Parsed flags - should_host: ", should_host, ", should_join: ", should_join, ", should_debug: ", should_debug)
print("GameUI: Parsed flags - should_host: ", should_host, ", should_join: ", should_join, ", force_webrtc: ", force_webrtc, ", should_debug: ", should_debug)
# Force WebRTC mode if --webrtc flag is present
if force_webrtc:
network_manager.set_network_mode(1) # WebRTC
if network_mode_option:
if OS.get_name() == "Web":
network_mode_option.selected = 0 # WebRTC is first option on web
else:
network_mode_option.selected = 1 # WebRTC is second option on native
_on_network_mode_changed(network_mode_option.selected)
print("GameUI: WebRTC mode forced via --webrtc flag")
# Set debug flag only if --room-debug is used with --host or --join
if should_debug and (should_host or should_join):
@@ -91,24 +129,68 @@ func _check_command_line_args():
# Connection callback will handle starting the game
pass
func _on_network_mode_changed(index: int):
# On web builds, index 0 = WebRTC, index 1 = WebSocket
# On native builds, index 0 = ENet, index 1 = WebRTC, index 2 = WebSocket
var actual_mode: int
if OS.get_name() == "Web":
# Web builds: 0 = WebRTC, 1 = WebSocket
actual_mode = index + 1 # Map 0->1 (WebRTC), 1->2 (WebSocket)
else:
# Native builds: 0 = ENet, 1 = WebRTC, 2 = WebSocket
actual_mode = index
network_manager.set_network_mode(actual_mode)
# Update address input placeholder based on mode
if address_input:
match actual_mode:
0: # ENet
address_input.placeholder_text = "Server IP or domain"
1: # WebRTC
address_input.placeholder_text = "Enter Room Code (e.g., ABC123)"
2: # WebSocket
address_input.placeholder_text = "Enter Room Code (e.g., ABC123)"
var mode_names = ["ENet", "WebRTC", "WebSocket"]
print("GameUI: Network mode changed to: ", mode_names[actual_mode])
func _on_host_pressed():
var local_count = int(local_players_spinbox.value)
network_manager.set_local_player_count(local_count)
if network_manager.host_game():
print("Hosting game with ", local_count, " local players")
var mode = network_manager.network_mode
if mode == 1 or mode == 2: # WebRTC or WebSocket
var room_id = network_manager.get_room_id()
var mode_name = "WebRTC" if mode == 1 else "WebSocket"
print("Hosting ", mode_name, " game - Room Code: ", room_id)
print("Share this code with players!")
else:
print("Hosting ENet game with ", local_count, " local players")
_start_game()
func _on_join_pressed():
var address = address_input.text
if address.is_empty():
address = "127.0.0.1"
var mode = network_manager.network_mode
if mode == 1 or mode == 2: # WebRTC or WebSocket
print("Error: Please enter a room code")
return
else: # ENet
address = "127.0.0.1"
var local_count = int(local_players_spinbox.value)
network_manager.set_local_player_count(local_count)
if network_manager.join_game(address):
print("Joining game at ", address, " with ", local_count, " local players")
var mode = network_manager.network_mode
if mode == 1: # WebRTC
print("Joining WebRTC game with room code: ", address)
elif mode == 2: # WebSocket
print("Joining WebSocket game with room code: ", address)
else: # ENet
print("Joining ENet game at ", address, " with ", local_count, " local players")
func _on_connection_succeeded():
print("Connection succeeded, starting game")

View File

@@ -12,8 +12,12 @@ var label_time: Label = null
var label_time_value: Label = null
var label_boss: Label = null
var texture_progress_bar_boss_hp: TextureProgressBar = null
var label_host: Label = null
var label_player_count: Label = null
var label_room_code: Label = null
var game_world: Node = null
var network_manager: Node = null
var local_player: Node = null
var level_start_time: float = 0.0
var player_search_attempts: int = 0
@@ -34,6 +38,16 @@ func _ready():
label_time_value = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerTime/LabelTimeValue")
label_boss = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerBoss/LabelBoss")
texture_progress_bar_boss_hp = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerBoss/TextureProgressBarBossHP")
label_host = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelHost")
label_player_count = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelPlayerCount")
label_room_code = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelRoomCode")
# Find network manager
network_manager = get_node_or_null("/root/NetworkManager")
if network_manager:
# Connect to player connection signals to update player count
network_manager.player_connected.connect(_on_player_connected)
network_manager.player_disconnected.connect(_on_player_disconnected)
# Debug: Check if nodes were found
if not label_time_value:
@@ -56,6 +70,9 @@ func _ready():
if label_boss:
label_boss.visible = false
# Update host info display
_update_host_info()
# Start level timer
level_start_time = Time.get_ticks_msec() / 1000.0
@@ -63,6 +80,41 @@ func _ready():
player_search_attempts = 0
_find_local_player()
func _on_player_connected(_peer_id: int, _player_info: Dictionary):
_update_host_info()
func _on_player_disconnected(_peer_id: int, _player_info: Dictionary):
_update_host_info()
func _update_host_info():
if not network_manager:
return
# Update HOST label visibility
if label_host:
label_host.visible = network_manager.is_hosting
# Update player count
if label_player_count and network_manager.players_info:
var total_players = 0
for peer_id in network_manager.players_info.keys():
var info = network_manager.players_info[peer_id]
total_players += info.get("local_player_count", 1)
label_player_count.text = "Players: " + str(total_players)
# Update room code (only show if WebRTC/WebSocket and hosting)
if label_room_code:
var mode = network_manager.network_mode
if (mode == 1 or mode == 2) and network_manager.is_hosting: # WebRTC or WebSocket
var room_id = network_manager.get_room_id()
if not room_id.is_empty():
label_room_code.text = "Room: " + room_id
label_room_code.visible = true
else:
label_room_code.visible = false
else:
label_room_code.visible = false
func _find_local_player():
# Prevent infinite recursion
player_search_attempts += 1

View File

@@ -0,0 +1,387 @@
extends Node
# Matchbox Client - Handles WebSocket signaling for WebRTC via Matchbox server
# Protocol: https://github.com/johanhelsing/matchbox
signal peer_joined(peer_id: int)
signal peer_left(peer_id: int)
signal peer_connected(peer_id: int)
signal connection_failed()
signal connection_succeeded()
signal webrtc_ready() # Emitted when WebRTC mesh is set up after Welcome message
const MATCHBOX_SERVER = "wss://matchbox.thefirstboss.com"
const STUN_SERVER = "stun:ruinborn.thefirstboss.com:3578"
var websocket: WebSocketPeer = null
var webrtc_peer: WebRTCMultiplayerPeer = null
var room_name: String = ""
var is_connected: bool = false
var is_hosting: bool = false # Set by NetworkManager to determine if we're host (ID 1) or client (ID 2+)
var my_uuid: String = "" # Our UUID assigned by Matchbox
var my_peer_id: int = 0 # Our integer peer ID (1 = host, 2+ = clients)
var peer_uuid_to_id: Dictionary = {} # UUID -> integer peer ID
var peer_id_counter: int = 1 # Counter for assigning integer peer IDs
var peer_connections: Dictionary = {} # peer_id (int) -> WebRTCPeerConnection
var pending_offers: Dictionary = {} # peer_id -> offer data
func _ready():
pass
func connect_to_room(room: String, hosting: bool = false) -> bool:
if websocket:
disconnect_from_room()
room_name = room
is_hosting = hosting
print("MatchboxClient: Connecting to room: ", room, " (hosting: ", hosting, ")")
# Create WebSocket connection to Matchbox
websocket = WebSocketPeer.new()
var url = MATCHBOX_SERVER + "/" + room_name
var error = websocket.connect_to_url(url)
if error != OK:
push_error("MatchboxClient: Failed to connect to Matchbox: " + str(error))
return false
print("MatchboxClient: Connecting to: ", url)
return true
func disconnect_from_room():
if websocket:
websocket.close()
websocket = null
# Close all peer connections
for peer_id in peer_connections:
var pc = peer_connections[peer_id]
if pc:
pc.close()
peer_connections.clear()
pending_offers.clear()
is_connected = false
room_name = ""
func _process(_delta):
if not websocket:
return
websocket.poll()
var state = websocket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
if not is_connected:
is_connected = true
connection_succeeded.emit()
print("MatchboxClient: Connected to Matchbox server")
# Process incoming messages
while websocket.get_available_packet_count() > 0:
var packet = websocket.get_packet()
_handle_message(packet.get_string_from_utf8())
elif state == WebSocketPeer.STATE_CLOSED:
if is_connected:
is_connected = false
connection_failed.emit()
print("MatchboxClient: Disconnected from Matchbox server")
else:
# Connection failed before being established
print("MatchboxClient: Connection failed (state: CLOSED)")
connection_failed.emit()
elif state == WebSocketPeer.STATE_CONNECTING:
# Still connecting, wait for next poll
pass
elif state == WebSocketPeer.STATE_CLOSING:
# Connection is closing
print("MatchboxClient: Connection closing...")
func _handle_message(message: String):
print("MatchboxClient: Received message: ", message)
var json = JSON.new()
var error = json.parse(message)
if error != OK:
push_error("MatchboxClient: Failed to parse message: " + message)
return
var data = json.data
print("MatchboxClient: Parsed data: ", data)
# Matchbox protocol uses direct keys: IdAssigned, NewPeer, PeerLeft, Signal
if data.has("IdAssigned"):
_handle_id_assigned(data.get("IdAssigned", ""))
elif data.has("NewPeer"):
_handle_new_peer(data.get("NewPeer", ""))
elif data.has("PeerLeft"):
_handle_peer_left_uuid(data.get("PeerLeft", ""))
elif data.has("Signal"):
_handle_signal_message_dict(data.get("Signal", {}))
else:
print("MatchboxClient: Unknown message format: ", data)
func _handle_id_assigned(uuid: String):
print("MatchboxClient: IdAssigned message received: ", uuid)
my_uuid = uuid
# Assign peer ID based on whether we're hosting or joining
# Host gets ID 1, clients get ID 2, 3, 4, etc.
if is_hosting:
my_peer_id = 1
peer_uuid_to_id[my_uuid] = my_peer_id
peer_id_counter = 2 # Next peer will get ID 2
else:
# Client: we'll assign ourselves a temporary ID, but this needs coordination
# Actually, we can't determine our ID from IdAssigned alone - we need to wait
# For now, assign ID 2 (assumes only one client joins at a time)
# TODO: Proper peer ID coordination needed for multiple simultaneous joins
my_peer_id = 2 # Client gets ID 2
peer_uuid_to_id[my_uuid] = my_peer_id
peer_id_counter = 3 # Next peer will get ID 3
print("MatchboxClient: Assigned UUID: ", my_uuid, " -> Peer ID: ", my_peer_id, " (hosting: ", is_hosting, ")")
# Setup WebRTC mesh now that we know our peer ID
if setup_webrtc_peer():
webrtc_ready.emit()
print("MatchboxClient: WebRTC mesh ready, signal emitted")
func _handle_new_peer(uuid: String):
if uuid.is_empty():
return
print("MatchboxClient: NewPeer message received: ", uuid)
# Assign sequential integer peer ID
var peer_id = peer_id_counter
peer_uuid_to_id[uuid] = peer_id
peer_id_counter += 1
print("MatchboxClient: New peer UUID: ", uuid, " -> Peer ID: ", peer_id)
peer_joined.emit(peer_id)
_create_peer_connection(peer_id)
func _handle_peer_left_uuid(uuid: String):
if uuid.is_empty():
return
var peer_id = peer_uuid_to_id.get(uuid, 0)
if peer_id == 0:
return
print("MatchboxClient: Peer left UUID: ", uuid, " -> Peer ID: ", peer_id)
peer_uuid_to_id.erase(uuid)
peer_left.emit(peer_id)
_close_peer_connection(peer_id)
func _handle_signal_message_dict(signal_data: Dictionary):
# Matchbox Signal format: {"From": "uuid", "signal": {...}}
# We need to convert UUID to integer peer ID
var from_uuid = signal_data.get("From", "")
if from_uuid.is_empty():
return
var peer_id = peer_uuid_to_id.get(from_uuid, 0)
if peer_id == 0:
# Unknown peer - might be a new peer, assign ID
peer_id = peer_id_counter
peer_uuid_to_id[from_uuid] = peer_id
peer_id_counter += 1
print("MatchboxClient: Unknown peer in Signal, assigned ID: ", peer_id)
var sig = signal_data.get("signal", {})
if sig.is_empty():
return
_handle_signal_message(peer_id, sig)
func _handle_signal_message(peer_id: int, signal_data: Dictionary):
var peer_conn = peer_connections.get(peer_id)
if not peer_conn:
# Create connection if it doesn't exist
_create_peer_connection(peer_id)
peer_conn = peer_connections.get(peer_id)
if not peer_conn:
return
var type = signal_data.get("type", "")
match type:
"offer":
_handle_offer(peer_id, signal_data)
"answer":
_handle_answer(peer_id, signal_data)
"ice-candidate":
_handle_ice_candidate(peer_id, signal_data)
func _create_peer_connection(peer_id: int) -> WebRTCPeerConnection:
if peer_connections.has(peer_id):
return peer_connections[peer_id]
print("MatchboxClient: Creating peer connection for peer ", peer_id)
var pc = WebRTCPeerConnection.new()
# Configure STUN server
var config = {
"iceServers": [
{
"urls": [STUN_SERVER]
}
]
}
var error = pc.initialize(config)
if error != OK:
push_error("MatchboxClient: Failed to initialize peer connection: " + str(error))
return null
# Connect signals
pc.session_description_created.connect(_on_session_description_created.bind(peer_id))
pc.ice_candidate_created.connect(_on_ice_candidate_created.bind(peer_id))
peer_connections[peer_id] = pc
# If we're the host (peer_id 1), create offer for other peers
# Otherwise wait for offer
if my_peer_id == 1 and peer_id != 1:
# We're the host, create offer
pc.create_offer()
return pc
func _close_peer_connection(peer_id: int):
var pc = peer_connections.get(peer_id)
if pc:
pc.close()
peer_connections.erase(peer_id)
pending_offers.erase(peer_id)
func _handle_offer(peer_id: int, signal_data: Dictionary):
var pc = peer_connections.get(peer_id)
if not pc:
pc = _create_peer_connection(peer_id)
if not pc:
return
var sdp = signal_data.get("sdp", "")
if sdp.is_empty():
return
var set_error = pc.set_remote_description("offer", sdp)
if set_error != OK:
push_error("MatchboxClient: Failed to set remote offer: " + str(set_error))
return
# Create answer
pc.create_answer()
func _handle_answer(peer_id: int, signal_data: Dictionary):
var pc = peer_connections.get(peer_id)
if not pc:
return
var sdp = signal_data.get("sdp", "")
if sdp.is_empty():
return
var error = pc.set_remote_description("answer", sdp)
if error != OK:
push_error("MatchboxClient: Failed to set remote answer: " + str(error))
return
print("MatchboxClient: Answer received for peer ", peer_id)
peer_connected.emit(peer_id)
func _handle_ice_candidate(peer_id: int, signal_data: Dictionary):
var pc = peer_connections.get(peer_id)
if not pc:
return
var candidate = signal_data.get("candidate", "")
var sdp_mid = signal_data.get("sdpMid", "")
var sdp_mline_index = signal_data.get("sdpMLineIndex", 0)
if candidate.is_empty():
return
var error = pc.add_ice_candidate(sdp_mid, sdp_mline_index, candidate)
if error != OK:
push_error("MatchboxClient: Failed to add ICE candidate: " + str(error))
return
func _on_session_description_created(peer_id: int, type: String, sdp: String):
var message = {
"type": "Signal",
"id": peer_id,
"signal": {
"type": type,
"sdp": sdp
}
}
_send_message(message)
func _on_ice_candidate_created(peer_id: int, media: String, index: int, name: String):
var message = {
"type": "Signal",
"id": peer_id,
"signal": {
"type": "ice-candidate",
"candidate": name,
"sdpMid": media,
"sdpMLineIndex": index
}
}
_send_message(message)
func _send_message(message: Dictionary):
if not websocket or websocket.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
var json = JSON.stringify(message)
var error = websocket.send_text(json)
if error != OK:
push_error("MatchboxClient: Failed to send message: " + str(error))
func get_my_peer_id() -> int:
return my_peer_id
func setup_webrtc_peer() -> bool:
if webrtc_peer:
# Already set up
return true
webrtc_peer = WebRTCMultiplayerPeer.new()
# Create mesh with our peer ID (assigned by Matchbox)
# For host: peer_id = 1, for clients: peer_id = 2, 3, 4, etc.
var error = webrtc_peer.create_mesh(my_peer_id)
if error != OK:
push_error("MatchboxClient: Failed to create WebRTC mesh: " + str(error))
return false
multiplayer.multiplayer_peer = webrtc_peer
print("MatchboxClient: WebRTC mesh created with peer ID: ", my_peer_id)
return true
func add_peer_to_mesh(peer_id: int):
if not webrtc_peer:
if not setup_webrtc_peer():
return
var pc = peer_connections.get(peer_id)
if not pc:
push_error("MatchboxClient: No peer connection for peer ", peer_id)
return
var error = webrtc_peer.add_peer(pc, peer_id)
if error != OK:
push_error("MatchboxClient: Failed to add peer to mesh: " + str(error))
return
print("MatchboxClient: Added peer ", peer_id, " to WebRTC mesh")

View File

@@ -0,0 +1 @@
uid://bm8jiiypl81wj

View File

@@ -2,21 +2,40 @@ extends Node
# Network Manager - Handles multiplayer connections and player spawning
# Supports both hosting and joining games
# Auto-detects platform and uses appropriate networking:
# - ENet for native builds (Windows, Linux, Mac)
# - WebRTC for web builds (requires signaling server)
signal player_connected(peer_id, player_info)
signal player_disconnected(peer_id, player_info)
signal connection_failed()
signal connection_succeeded()
const DEFAULT_PORT = 7777
const DEFAULT_PORT = 21212
const MAX_PLAYERS = 8
const MATCHBOX_SERVER = "wss://matchbox.thefirstboss.com"
const STUN_SERVER = "stun:ruinborn.thefirstboss.com:3578"
var players_info = {} # Dictionary of peer_id -> {local_player_count: int, player_names: []}
var local_player_count = 1 # How many local players on this machine
var is_hosting = false
var show_room_labels = false # Show room labels when in debug mode
var network_mode: int = 0 # 0 = ENet, 1 = WebRTC, 2 = WebSocket
var room_id = "" # Room ID for Matchbox (WebRTC) or WebSocket server URL
var matchbox_client: Node = null # Matchbox client instance
const WEBSOCKET_SERVER_URL = "ws://ruinborn.thefirstboss.com:21212" # WebSocket server URL
func _ready():
# Detect if running in browser - default to WebRTC on web, ENet on native
if OS.get_name() == "Web":
network_mode = 1 # WebRTC default for web
print("NetworkManager: Detected Web platform, defaulting to WebRTC")
print("NetworkManager: Matchbox server: ", MATCHBOX_SERVER)
else:
network_mode = 0 # ENet default for native
print("NetworkManager: Using ENet by default for native platform")
print("NetworkManager: You can switch network modes in the menu")
# Connect multiplayer signals
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
@@ -24,47 +43,221 @@ func _ready():
multiplayer.connection_failed.connect(_on_connection_failed)
multiplayer.server_disconnected.connect(_on_server_disconnected)
func host_game(port: int = DEFAULT_PORT) -> bool:
var peer = ENetMultiplayerPeer.new()
var error = peer.create_server(port, MAX_PLAYERS)
func set_network_mode(mode: int):
# 0 = ENet, 1 = WebRTC, 2 = WebSocket
# WebRTC is only available on web builds
if mode == 1 and OS.get_name() != "Web":
push_error("NetworkManager: WebRTC is not available on native platforms. WebRTC only works in web builds. Falling back to ENet.")
network_mode = 0
print("NetworkManager: ENet mode enabled (WebRTC not available on native platforms)")
return
if error != OK:
push_error("Failed to create server: " + str(error))
return false
network_mode = mode
match mode:
0:
print("NetworkManager: ENet mode enabled")
1:
print("NetworkManager: WebRTC mode enabled")
print("NetworkManager: Matchbox server: ", MATCHBOX_SERVER)
2:
print("NetworkManager: WebSocket mode enabled")
print("NetworkManager: WebSocket server: ", WEBSOCKET_SERVER_URL)
func force_webrtc_mode(enable: bool):
# Legacy function for backwards compatibility
if enable:
set_network_mode(1)
else:
set_network_mode(0)
func host_game(port: int = DEFAULT_PORT, matchbox_room: String = "") -> bool:
var peer
var error
multiplayer.multiplayer_peer = peer
is_hosting = true
# Register the host as a player
var my_id = multiplayer.get_unique_id()
players_info[my_id] = {
"local_player_count": local_player_count,
"player_names": _generate_player_names(local_player_count, my_id)
}
print("Server started on port ", port)
return true
if network_mode == 1: # WebRTC
# WebRTC for browser builds using Matchbox signaling
# Generate room ID if not provided
if matchbox_room.is_empty():
room_id = _generate_room_id()
else:
room_id = matchbox_room
print("NetworkManager: Creating WebRTC host with room ID: ", room_id)
print("NetworkManager: Share this room code with players!")
print("NetworkManager: Matchbox URL: ", MATCHBOX_SERVER, "/", room_id)
# Create Matchbox client
var matchbox_script = load("res://scripts/matchbox_client.gd")
matchbox_client = Node.new()
matchbox_client.set_script(matchbox_script)
add_child(matchbox_client)
# Connect Matchbox signals
matchbox_client.peer_joined.connect(_on_matchbox_peer_joined)
matchbox_client.peer_left.connect(_on_matchbox_peer_left)
matchbox_client.peer_connected.connect(_on_matchbox_peer_connected)
matchbox_client.connection_succeeded.connect(_on_matchbox_connected)
matchbox_client.connection_failed.connect(_on_matchbox_connection_failed)
matchbox_client.webrtc_ready.connect(_on_matchbox_webrtc_ready)
# Setup WebRTC peer (will be initialized when Matchbox connects)
# For now, we'll set it up after Matchbox connection
is_hosting = true
# Connect to Matchbox room (pass is_hosting = true)
if not matchbox_client.connect_to_room(room_id, true):
push_error("Failed to connect to Matchbox room")
return false
# Register the host as a player (peer_id 1)
players_info[1] = {
"local_player_count": local_player_count,
"player_names": _generate_player_names(local_player_count, 1)
}
return true
elif network_mode == 2: # WebSocket
# WebSocket for both native and web builds
# Generate room ID if not provided
if matchbox_room.is_empty():
room_id = _generate_room_id()
else:
room_id = matchbox_room
print("NetworkManager: Creating WebSocket host with room ID: ", room_id)
print("NetworkManager: Share this room code with players!")
print("NetworkManager: WebSocket URL: ", WEBSOCKET_SERVER_URL, "/", room_id)
peer = WebSocketMultiplayerPeer.new()
var url = WEBSOCKET_SERVER_URL + "/" + room_id
error = peer.create_server(port)
if error != OK:
push_error("Failed to create WebSocket server: " + str(error))
return false
multiplayer.multiplayer_peer = peer
is_hosting = true
print("WebSocket server started on port ", port)
print("Players can join at: ", url)
# Register the host as a player
var my_id = multiplayer.get_unique_id()
players_info[my_id] = {
"local_player_count": local_player_count,
"player_names": _generate_player_names(local_player_count, my_id)
}
return true
else: # ENet (mode 0)
# ENet for native builds
peer = ENetMultiplayerPeer.new()
error = peer.create_server(port, MAX_PLAYERS)
if error != OK:
push_error("Failed to create ENet server: " + str(error))
return false
multiplayer.multiplayer_peer = peer
is_hosting = true
print("ENet server started on port ", port)
print("Players can join at: ", get_local_ip(), ":", port)
# Register the host as a player
var my_id = multiplayer.get_unique_id()
players_info[my_id] = {
"local_player_count": local_player_count,
"player_names": _generate_player_names(local_player_count, my_id)
}
return true
func join_game(address: String, port: int = DEFAULT_PORT) -> bool:
var peer = ENetMultiplayerPeer.new()
var error = peer.create_client(address, port)
var peer
var error
if error != OK:
push_error("Failed to create client: " + str(error))
return false
multiplayer.multiplayer_peer = peer
is_hosting = false
print("Attempting to connect to ", address, ":", port)
return true
if network_mode == 1: # WebRTC
# WebRTC for browser builds using Matchbox signaling
# 'address' is the room ID for WebRTC
room_id = address
print("NetworkManager: Joining WebRTC game with room ID: ", room_id)
print("NetworkManager: Matchbox URL: ", MATCHBOX_SERVER, "/", room_id)
# Create Matchbox client
var matchbox_script = load("res://scripts/matchbox_client.gd")
matchbox_client = Node.new()
matchbox_client.set_script(matchbox_script)
add_child(matchbox_client)
# Connect Matchbox signals
matchbox_client.peer_joined.connect(_on_matchbox_peer_joined)
matchbox_client.peer_left.connect(_on_matchbox_peer_left)
matchbox_client.peer_connected.connect(_on_matchbox_peer_connected)
matchbox_client.connection_succeeded.connect(_on_matchbox_connected)
matchbox_client.connection_failed.connect(_on_matchbox_connection_failed)
matchbox_client.webrtc_ready.connect(_on_matchbox_webrtc_ready)
is_hosting = false
# Connect to Matchbox room (pass is_hosting = false)
if not matchbox_client.connect_to_room(room_id, false):
push_error("Failed to connect to Matchbox room")
return false
return true
elif network_mode == 2: # WebSocket
# WebSocket for both native and web builds
# 'address' is the room ID for WebSocket
room_id = address
print("NetworkManager: Joining WebSocket game with room ID: ", room_id)
print("NetworkManager: WebSocket URL: ", WEBSOCKET_SERVER_URL, "/", room_id)
peer = WebSocketMultiplayerPeer.new()
var url = WEBSOCKET_SERVER_URL + "/" + room_id
error = peer.create_client(url)
if error != OK:
push_error("Failed to create WebSocket client: " + str(error))
return false
multiplayer.multiplayer_peer = peer
is_hosting = false
print("Attempting to connect to WebSocket server: ", url)
return true
else: # ENet (mode 0)
# ENet for native builds
peer = ENetMultiplayerPeer.new()
error = peer.create_client(address, port)
if error != OK:
push_error("Failed to create ENet client: " + str(error))
return false
multiplayer.multiplayer_peer = peer
is_hosting = false
print("Attempting to connect to ", address, ":", port)
return true
func disconnect_from_game():
if matchbox_client:
matchbox_client.disconnect_from_room()
matchbox_client.queue_free()
matchbox_client = null
if multiplayer.multiplayer_peer:
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = null
players_info.clear()
is_hosting = false
room_id = ""
func set_local_player_count(count: int):
local_player_count = max(1, min(count, 4)) # Limit to 1-4 local players
@@ -159,3 +352,140 @@ func get_all_player_ids() -> Array:
func get_player_info(peer_id: int) -> Dictionary:
return players_info.get(peer_id, {})
# Matchbox callback handlers
func _on_matchbox_connected():
print("NetworkManager: Connected to Matchbox server")
# WebRTC peer will be set up when Welcome message is received with our peer ID
func _on_matchbox_webrtc_ready():
print("NetworkManager: WebRTC mesh is ready")
# Register ourselves if we're not hosting (clients need to register)
if not is_hosting and matchbox_client:
var my_peer_id = matchbox_client.get_my_peer_id()
if my_peer_id > 0 and not players_info.has(my_peer_id):
players_info[my_peer_id] = {
"local_player_count": local_player_count,
"player_names": _generate_player_names(local_player_count, my_peer_id)
}
print("NetworkManager: Registered joining player with peer ID: ", my_peer_id)
# Emit connection_succeeded signal so the game can start
connection_succeeded.emit()
func _on_matchbox_connection_failed():
print("NetworkManager: Failed to connect to Matchbox server")
connection_failed.emit()
func _on_matchbox_peer_joined(peer_id: int):
print("NetworkManager: Matchbox peer joined: ", peer_id)
# Peer connection will be created by Matchbox client
# Once connected, we'll add it to WebRTC mesh
func _on_matchbox_peer_left(peer_id: int):
print("NetworkManager: Matchbox peer left: ", peer_id)
# Player disconnect will be handled by multiplayer signals
func _on_matchbox_peer_connected(peer_id: int):
print("NetworkManager: Matchbox peer connected: ", peer_id)
# Add peer to WebRTC mesh
if matchbox_client:
matchbox_client.add_peer_to_mesh(peer_id)
# Register player info
if not players_info.has(peer_id):
players_info[peer_id] = {
"local_player_count": 1, # Default, will be updated via RPC
"player_names": _generate_player_names(1, peer_id)
}
# Emit player connected signal
player_connected.emit(peer_id, players_info[peer_id])
func get_room_id() -> String:
return room_id
func get_local_ip() -> String:
var addresses = IP.get_local_addresses()
for addr in addresses:
# Skip localhost and IPv6
if addr.begins_with("127.") or ":" in addr:
continue
return addr
return "127.0.0.1"
func _generate_room_id() -> String:
# Generate a random 6-character room code
var chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # Avoid confusing chars like O/0, I/1
var code = ""
for i in range(6):
code += chars[randi() % chars.length()]
return code
func get_webrtc_peer() -> WebRTCMultiplayerPeer:
if network_mode == 1 and multiplayer.multiplayer_peer is WebRTCMultiplayerPeer:
return multiplayer.multiplayer_peer as WebRTCMultiplayerPeer
return null
# Create a WebRTC peer connection with STUN server configured
func create_peer_connection() -> WebRTCPeerConnection:
var peer_connection = WebRTCPeerConnection.new()
# Configure STUN server for NAT traversal
var config = {
"iceServers": [
{
"urls": [STUN_SERVER]
}
]
}
var error = peer_connection.initialize(config)
if error != OK:
push_error("Failed to initialize WebRTC peer connection: " + str(error))
return null
print("WebRTC peer connection initialized with STUN server: ", STUN_SERVER)
return peer_connection
# Add a peer connection for WebRTC mesh networking
func add_webrtc_peer(peer_id: int) -> bool:
var webrtc = get_webrtc_peer()
if not webrtc:
push_error("Not using WebRTC")
return false
var peer_connection = create_peer_connection()
if not peer_connection:
return false
var error = webrtc.add_peer(peer_connection, peer_id)
if error != OK:
push_error("Failed to add WebRTC peer: " + str(error))
return false
print("Added WebRTC peer: ", peer_id, " with STUN server configured")
return true
# Get the peer connection info for a specific peer
func get_peer_connection(peer_id: int) -> Dictionary:
var webrtc = get_webrtc_peer()
if not webrtc:
return {}
return webrtc.get_peer(peer_id)
# Get connection info to share with remote peer (for manual signaling)
# This would typically be sent via a signaling server
# Note: With matchbox, signaling is handled automatically
func get_connection_offer(peer_id: int) -> Dictionary:
var peer_conn = get_peer_connection(peer_id)
if peer_conn.is_empty():
return {}
# In a real implementation, you'd get the SDP offer from the peer connection
# With matchbox, this is handled automatically via the signaling server
print("To implement: Get SDP offer for peer ", peer_id)
return {}

View File

@@ -0,0 +1,141 @@
extends Node
# WebRTC Network Manager - For browser/WebAssembly builds
# Uses WebRTCMultiplayerPeer for P2P connections
signal connection_ready
signal connection_failed
signal session_created(session_data)
signal peer_connected_webrtc(peer_id)
signal peer_disconnected_webrtc(peer_id)
const STUN_SERVER = "stun:ruinborn.thefirstboss.com:3578"
var webrtc_peer: WebRTCMultiplayerPeer = null
var is_host: bool = false
func _ready():
print("WebRTC: Using STUN server: ", STUN_SERVER)
func create_host() -> bool:
print("WebRTC: Creating host")
webrtc_peer = WebRTCMultiplayerPeer.new()
# Godot 4.x API: initialize(peer_id, server_compat)
# peer_id 1 for server, server_compat = true
var error = webrtc_peer.initialize(1, true)
if error != OK:
push_error("WebRTC: Failed to initialize server: " + str(error))
return false
multiplayer.multiplayer_peer = webrtc_peer
is_host = true
# Connect signals
webrtc_peer.peer_connected.connect(_on_peer_connected)
webrtc_peer.peer_disconnected.connect(_on_peer_disconnected)
print("WebRTC: Host initialized successfully")
connection_ready.emit()
return true
func create_client() -> bool:
print("WebRTC: Creating client")
webrtc_peer = WebRTCMultiplayerPeer.new()
# Godot 4.x API: initialize(peer_id, server_compat)
# peer_id 0 for auto-assign, server_compat = false for client
var error = webrtc_peer.initialize(0, false)
if error != OK:
push_error("WebRTC: Failed to initialize client: " + str(error))
return false
multiplayer.multiplayer_peer = webrtc_peer
is_host = false
# Connect signals
webrtc_peer.peer_connected.connect(_on_peer_connected)
webrtc_peer.peer_disconnected.connect(_on_peer_disconnected)
print("WebRTC: Client initialized successfully")
connection_ready.emit()
return true
func create_mesh(peer_id: int) -> bool:
print("WebRTC: Creating mesh peer for ID ", peer_id)
if not webrtc_peer:
push_error("WebRTC: Peer not initialized")
return false
# Create peer connection with STUN server configured
var peer_connection = WebRTCPeerConnection.new()
# Configure STUN server for NAT traversal
var config = {
"iceServers": [
{
"urls": [STUN_SERVER]
}
]
}
var init_error = peer_connection.initialize(config)
if init_error != OK:
push_error("WebRTC: Failed to initialize peer connection: " + str(init_error))
return false
var error = webrtc_peer.add_peer(peer_connection, peer_id)
if error != OK:
push_error("WebRTC: Failed to add peer: " + str(error))
return false
print("WebRTC: Mesh peer created for ID ", peer_id, " with STUN server")
return true
# Get the local session description (offer or answer) to share with remote peer
func get_local_session() -> String:
if not webrtc_peer:
return ""
# Get all connection states and session descriptions
var session_data = {}
# For host, we need to create offers for each connected peer
# For client, we need to create an answer
# Note: WebRTCMultiplayerPeer handles this internally
# We just need to get the peer connection for manual signaling
# This is a simplified approach - for full WebRTC, you'd need to:
# 1. Create RTCPeerConnection
# 2. Create offer/answer
# 3. Exchange ICE candidates
# For now, return empty - we'll use a signaling server approach instead
return ""
# Set the remote session description from another peer
func set_remote_session(session_data: String) -> bool:
if not webrtc_peer:
return false
# Parse and apply remote session
# This would involve SDP parsing and ICE candidate exchange
return true
func _on_peer_connected(id: int):
print("WebRTC: Peer connected: ", id)
peer_connected_webrtc.emit(id)
func _on_peer_disconnected(id: int):
print("WebRTC: Peer disconnected: ", id)
peer_disconnected_webrtc.emit(id)
func close_connection():
if webrtc_peer:
webrtc_peer.close()
webrtc_peer = null
multiplayer.multiplayer_peer = null
is_host = false

View File

@@ -0,0 +1 @@
uid://cyyaodbagbthp