Add direct-connect multiplayer foundation

This commit is contained in:
Alexander Sellite 2026-07-29 15:01:46 -04:00
parent 9e9850dd20
commit 24f8263175
36 changed files with 2633 additions and 45 deletions

View file

@ -0,0 +1,175 @@
class_name JoinGamePage
extends Control
signal join_requested(endpoint: String)
signal back_requested
@onready var _address: LineEdit = %Address
@onready var _join_button: BubbleButton = %JoinButton
@onready var _cancel_button: BubbleButton = %CancelButton
@onready var _back_button: BubbleButton = %BackButton
@onready var _open_close_button: BubbleButton = %OpenCloseButton
@onready var _status: Label = %Status
@onready var _session_summary: Label = %SessionSummary
var _network_session: NetworkSession
var _saved_servers: SavedServerStore
var _gameplay_context: bool = false
func _ready() -> void:
_join_button.pressed.connect(_on_join_pressed)
_cancel_button.pressed.connect(_on_cancel_pressed)
_back_button.pressed.connect(_on_back_pressed)
_open_close_button.pressed.connect(_on_open_close_pressed)
_address.text_submitted.connect(_on_address_submitted)
hide()
func setup(
network_session: NetworkSession,
saved_servers: SavedServerStore,
gameplay_context: bool,
) -> void:
_network_session = network_session
_saved_servers = saved_servers
_gameplay_context = gameplay_context
if not _network_session.state_changed.is_connected(_on_state_changed):
_network_session.state_changed.connect(_on_state_changed)
if not _network_session.status_message_changed.is_connected(
_on_status_message_changed
):
_network_session.status_message_changed.connect(
_on_status_message_changed
)
if not _network_session.connection_error.is_connected(
_on_connection_error
):
_network_session.connection_error.connect(_on_connection_error)
if not _network_session.peer_count_changed.is_connected(
_on_peer_count_changed
):
_network_session.peer_count_changed.connect(_on_peer_count_changed)
_refresh()
func open_page(preserved_endpoint: String = "") -> void:
if not preserved_endpoint.is_empty():
_address.text = preserved_endpoint
elif _address.text.is_empty():
_address.text = "127.0.0.1:7777"
show()
_refresh()
_address.grab_focus()
_address.select_all()
func close_page() -> void:
hide()
get_viewport().gui_release_focus()
func get_endpoint_text() -> String:
return _address.text
func set_status(message: String) -> void:
_status.text = message
func _on_join_pressed() -> void:
_request_join()
func _on_address_submitted(_value: String) -> void:
_request_join()
func _request_join() -> void:
if _network_session.state in [
NetworkSession.State.CONNECTION_FAILED,
NetworkSession.State.SERVER_LOST,
]:
_network_session.reset_failure()
var endpoint: ConnectionEndpoint = EndpointParser.parse(_address.text)
if not endpoint.is_valid():
_status.text = endpoint.error_message
return
_address.text = endpoint.normalized_display
join_requested.emit(endpoint.normalized_display)
func _on_cancel_pressed() -> void:
if _network_session != null:
_network_session.cancel_connection()
_refresh()
func _on_back_pressed() -> void:
if (
_network_session != null
and _network_session.state in [
NetworkSession.State.CONNECTING,
NetworkSession.State.AUTHENTICATING,
]
):
_network_session.cancel_connection()
back_requested.emit()
func _on_open_close_pressed() -> void:
if _network_session == null or not _network_session.is_host():
return
_network_session.set_host_open(not _network_session.is_open_host())
_refresh()
func _on_state_changed(_state: NetworkSession.State) -> void:
_refresh()
func _on_status_message_changed(message: String) -> void:
_status.text = message
_refresh()
func _on_connection_error(message: String) -> void:
_status.text = message
_refresh()
func _on_peer_count_changed(player_count: int, max_players: int) -> void:
_session_summary.text = "%d / %d players" % [
player_count,
max_players,
]
func _refresh() -> void:
if not is_node_ready() or _network_session == null:
return
var connecting: bool = _network_session.state in [
NetworkSession.State.CONNECTING,
NetworkSession.State.AUTHENTICATING,
]
_address.editable = not connecting
_join_button.disabled = connecting
_cancel_button.visible = connecting
_open_close_button.visible = (
_gameplay_context and _network_session.is_host()
)
if _open_close_button.visible:
_open_close_button.text = (
"close\ngame"
if _network_session.is_open_host()
else "open\ngame"
)
_session_summary.visible = (
_gameplay_context
and _network_session.state != NetworkSession.State.INACTIVE
)
if _session_summary.visible:
_session_summary.text = "%d / %d players" % [
_network_session.get_player_count(),
_network_session.get_session_max_players(),
]

View file

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

View file

@ -0,0 +1,145 @@
[gd_scene load_steps=5 format=3]
[ext_resource type="Script" path="res://ui/network/join_game_page.gd" id="1_script"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_button.tscn" id="3_bubble"]
[sub_resource type="StyleBoxFlat" id="StyleBox_paper"]
bg_color = Color(0.93, 0.885, 0.73, 1)
shadow_color = Color(0.02, 0.075, 0.11, 0.42)
shadow_size = 10
shadow_offset = Vector2(7, 8)
corner_radius_top_left = 54
corner_radius_top_right = 46
corner_radius_bottom_right = 58
corner_radius_bottom_left = 48
[node name="JoinGamePage" type="Control"]
visible = false
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("2_theme")
script = ExtResource("1_script")
[node name="Paper" type="PanelContainer" parent="."]
layout_mode = 0
offset_left = 340.0
offset_top = 132.0
offset_right = 940.0
offset_bottom = 570.0
theme_override_styles/panel = SubResource("StyleBox_paper")
[node name="Margin" type="MarginContainer" parent="Paper"]
layout_mode = 2
theme_override_constants/margin_left = 70
theme_override_constants/margin_top = 52
theme_override_constants/margin_right = 70
theme_override_constants/margin_bottom = 52
[node name="Layout" type="VBoxContainer" parent="Paper/Margin"]
layout_mode = 2
theme_override_constants/separation = 18
alignment = 1
[node name="Title" type="Label" parent="Paper/Margin/Layout"]
layout_mode = 2
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
theme_override_font_sizes/font_size = 34
text = "join game"
horizontal_alignment = 1
[node name="Hint" type="Label" parent="Paper/Margin/Layout"]
layout_mode = 2
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
theme_override_font_sizes/font_size = 17
text = "hostname or IP address • optional port"
horizontal_alignment = 1
[node name="Address" type="LineEdit" parent="Paper/Margin/Layout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 52)
layout_mode = 2
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
theme_override_font_sizes/font_size = 22
placeholder_text = "example.net:7777"
alignment = 1
max_length = 300
[node name="Status" type="Label" parent="Paper/Margin/Layout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 42)
layout_mode = 2
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
theme_override_font_sizes/font_size = 17
text = "Direct UDP connection • default port 7777"
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2
[node name="SessionSummary" type="Label" parent="Paper/Margin/Layout"]
unique_name_in_owner = true
visible = false
layout_mode = 2
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
theme_override_font_sizes/font_size = 17
text = "1 / 8 players"
horizontal_alignment = 1
[node name="Actions" type="HBoxContainer" parent="Paper/Margin/Layout"]
layout_mode = 2
theme_override_constants/separation = 12
alignment = 1
[node name="JoinButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
unique_name_in_owner = true
custom_minimum_size = Vector2(112, 106)
layout_mode = 2
text = "join"
neutral_size = Vector2(112, 106)
minimum_font_size = 18
maximum_font_size = 24
horizontal_amplitude = 0.0
vertical_amplitude = 0.0
deformation_amplitude = 0.0
[node name="OpenCloseButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(112, 106)
layout_mode = 2
text = "open\ngame"
neutral_size = Vector2(112, 106)
minimum_font_size = 17
maximum_font_size = 22
horizontal_amplitude = 0.0
vertical_amplitude = 0.0
deformation_amplitude = 0.0
[node name="CancelButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(98, 94)
layout_mode = 2
text = "cancel"
neutral_size = Vector2(98, 94)
minimum_font_size = 16
maximum_font_size = 20
horizontal_amplitude = 0.0
vertical_amplitude = 0.0
deformation_amplitude = 0.0
[node name="BackButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
unique_name_in_owner = true
custom_minimum_size = Vector2(98, 94)
layout_mode = 2
text = "back"
neutral_size = Vector2(98, 94)
minimum_font_size = 16
maximum_font_size = 20
horizontal_amplitude = 0.0
vertical_amplitude = 0.0
deformation_amplitude = 0.0