feat: implement networked match HUD with real-time scoreboard and killfeed systems

This commit is contained in:
DottsGit
2026-06-06 13:49:12 -04:00
parent aefed46daf
commit 168bd8f1ce
11 changed files with 501 additions and 13 deletions
+43 -12
View File
@@ -217,11 +217,25 @@ func _ready() -> void:
hl_left.add_child(hl_title)
var hl_cards = HBoxContainer.new()
hl_cards.add_theme_constant_override("separation", 30)
hl_cards.add_theme_constant_override("separation", 20)
hl_left.add_child(hl_cards)
_build_level_card(hl_cards, "Dust 2", "res://scenes/dust2_level/dust2_level.tscn", d2_gradient, true)
_build_level_card(hl_cards, "Test Level", "res://scenes/test_level/test_level.tscn", tl_gradient, true)
_build_level_card(hl_cards, "Dust 2", "res://scenes/dust2_level/dust2_level.tscn", d2_gradient, true, Vector2(200, 250))
_build_level_card(hl_cards, "Test Level", "res://scenes/test_level/test_level.tscn", tl_gradient, true, Vector2(200, 250))
var mode_label = Label.new()
mode_label.text = "Gamemode"
mode_label.add_theme_font_size_override("font_size", 32)
hl_left.add_child(mode_label)
var _gamemode_opt = OptionButton.new()
_gamemode_opt.add_theme_font_size_override("font_size", 24)
_gamemode_opt.add_item("Deathmatch", 0)
_gamemode_opt.item_selected.connect(func(index: int):
if get_node_or_null("/root/NetworkManager"):
get_node("/root/NetworkManager").current_gamemode = _gamemode_opt.get_item_text(index)
)
hl_left.add_child(_gamemode_opt)
var hl_spacer = Control.new()
hl_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
@@ -251,21 +265,21 @@ func _ready() -> void:
_host_lobby_players_list.add_theme_font_size_override("font_size", 24)
hl_right.add_child(_host_lobby_players_list)
func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient, is_multiplayer: bool = false) -> void:
func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient, is_multiplayer: bool = false, card_size: Vector2 = Vector2(300, 400)) -> void:
var btn = TextureButton.new()
btn.custom_minimum_size = Vector2(300, 400)
btn.custom_minimum_size = card_size
var tex = GradientTexture2D.new()
tex.gradient = gradient
tex.width = 300
tex.height = 400
tex.width = int(card_size.x)
tex.height = int(card_size.y)
tex.fill_to = Vector2(1, 1)
btn.texture_normal = tex
var tex_hover = GradientTexture2D.new()
tex_hover.gradient = gradient
tex_hover.width = 300
tex_hover.height = 400
tex_hover.width = int(card_size.x)
tex_hover.height = int(card_size.y)
tex_hover.fill_to = Vector2(1, 1)
tex_hover.use_hdr = true # Slight bright effect on hover
btn.texture_hover = tex_hover
@@ -294,7 +308,12 @@ func _build_level_card(parent: Container, title: String, scene_path: String, gra
if is_multiplayer:
btn.pressed.connect(func(): get_node("/root/NetworkManager").rpc_load_level.rpc(scene_path))
else:
btn.pressed.connect(func(): get_tree().change_scene_to_file(scene_path))
btn.pressed.connect(func():
var nm = get_node_or_null("/root/NetworkManager")
if nm:
nm.start_singleplayer_match("Deathmatch")
get_tree().change_scene_to_file(scene_path)
)
parent.add_child(btn)
@@ -332,6 +351,8 @@ func _on_host_pressed() -> void:
if not nm.player_connected.is_connected(_on_network_player_connected):
nm.player_connected.connect(_on_network_player_connected)
nm.player_disconnected.connect(_on_network_player_disconnected)
if not nm.stats_updated.is_connected(_update_lobby_players):
nm.stats_updated.connect(_update_lobby_players)
func _on_direct_connect_pressed() -> void:
_multiplayer_panel.hide()
@@ -355,6 +376,8 @@ func _on_lobby_back_pressed() -> void:
if nm.player_connected.is_connected(_on_network_player_connected):
nm.player_connected.disconnect(_on_network_player_connected)
nm.player_disconnected.disconnect(_on_network_player_disconnected)
if nm.stats_updated.is_connected(_update_lobby_players):
nm.stats_updated.disconnect(_update_lobby_players)
func _on_network_player_connected(_id: int) -> void:
_update_lobby_players()
@@ -367,11 +390,19 @@ func _update_lobby_players() -> void:
_host_lobby_players_list.clear()
var nm = get_node("/root/NetworkManager")
var host_name = SettingsManager.username
var host_ping = 0
if nm.player_stats.has(1): host_ping = nm.player_stats[1].get("ping", 0)
for pid in nm.connected_players:
var p_ping = 0
if nm.player_stats.has(pid): p_ping = nm.player_stats[pid].get("ping", 0)
if pid == 1:
_host_lobby_players_list.add_item(host_name + " (Host)")
_host_lobby_players_list.add_item("%s (Host) - %sms" % [host_name, host_ping])
else:
_host_lobby_players_list.add_item("Player " + str(pid))
var p_name = "Player " + str(pid)
if nm.player_stats.has(pid): p_name = nm.player_stats[pid].get("username", p_name)
_host_lobby_players_list.add_item("%s - %sms" % [p_name, p_ping])
func _on_loadouts_pressed() -> void:
if has_node("/root/PauseMenu"):
+207
View File
@@ -0,0 +1,207 @@
extends CanvasLayer
var _nm: Node
# Top HUD
var _time_label: Label
var _local_kills_label: Label
var _leader_label: Label
# Killfeed
var _killfeed_vbox: VBoxContainer
# Scoreboard
var _scoreboard_panel: PanelContainer
var _scoreboard_grid: GridContainer
func _ready() -> void:
_nm = get_node_or_null("/root/NetworkManager")
# Top HUD
var top_hbox = HBoxContainer.new()
top_hbox.set_anchors_preset(Control.PRESET_TOP_WIDE)
top_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
top_hbox.add_theme_constant_override("separation", 50)
var margin = MarginContainer.new()
margin.add_theme_constant_override("margin_top", 20)
margin.set_anchors_preset(Control.PRESET_TOP_WIDE)
margin.add_child(top_hbox)
add_child(margin)
_time_label = Label.new()
_time_label.add_theme_font_size_override("font_size", 48)
_time_label.add_theme_color_override("font_outline_color", Color.BLACK)
_time_label.add_theme_constant_override("outline_size", 8)
top_hbox.add_child(_time_label)
_local_kills_label = Label.new()
_local_kills_label.text = "0 Kills"
_local_kills_label.add_theme_font_size_override("font_size", 64)
_local_kills_label.add_theme_color_override("font_color", Color(1, 0.8, 0.2))
_local_kills_label.add_theme_color_override("font_outline_color", Color.BLACK)
_local_kills_label.add_theme_constant_override("outline_size", 12)
top_hbox.add_child(_local_kills_label)
_leader_label = Label.new()
_leader_label.text = "Leader: None"
_leader_label.add_theme_font_size_override("font_size", 32)
_leader_label.add_theme_color_override("font_outline_color", Color.BLACK)
_leader_label.add_theme_constant_override("outline_size", 8)
top_hbox.add_child(_leader_label)
# Killfeed
var kf_margin = MarginContainer.new()
kf_margin.set_anchors_preset(Control.PRESET_TOP_RIGHT)
kf_margin.add_theme_constant_override("margin_top", 20)
kf_margin.add_theme_constant_override("margin_right", 20)
add_child(kf_margin)
_killfeed_vbox = VBoxContainer.new()
_killfeed_vbox.alignment = BoxContainer.ALIGNMENT_END
kf_margin.add_child(_killfeed_vbox)
# Scoreboard Overlay
var center_container = CenterContainer.new()
center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(center_container)
_scoreboard_panel = PanelContainer.new()
_scoreboard_panel.custom_minimum_size = Vector2(800, 600)
var sb_style = StyleBoxFlat.new()
sb_style.bg_color = Color(0, 0, 0, 0.8)
_scoreboard_panel.add_theme_stylebox_override("panel", sb_style)
_scoreboard_panel.hide()
center_container.add_child(_scoreboard_panel)
var sb_vbox = VBoxContainer.new()
var sb_margin = MarginContainer.new()
sb_margin.add_theme_constant_override("margin_left", 20)
sb_margin.add_theme_constant_override("margin_top", 20)
sb_margin.add_theme_constant_override("margin_right", 20)
sb_margin.add_theme_constant_override("margin_bottom", 20)
sb_margin.add_child(sb_vbox)
_scoreboard_panel.add_child(sb_margin)
var sb_title = Label.new()
sb_title.text = "Scoreboard"
sb_title.add_theme_font_size_override("font_size", 48)
sb_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
sb_vbox.add_child(sb_title)
_scoreboard_grid = GridContainer.new()
_scoreboard_grid.columns = 5
_scoreboard_grid.add_theme_constant_override("h_separation", 50)
_scoreboard_grid.add_theme_constant_override("v_separation", 10)
sb_vbox.add_child(_scoreboard_grid)
_populate_scoreboard_headers()
# Signals
if _nm:
_nm.stats_updated.connect(_update_hud)
_nm.killfeed_event.connect(_on_killfeed_event)
_nm.match_state_updated.connect(_update_timer)
func _process(_delta: float) -> void:
if Input.is_action_just_pressed("scoreboard"):
_scoreboard_panel.show()
_update_hud()
elif Input.is_action_just_released("scoreboard"):
_scoreboard_panel.hide()
if _nm and _nm.match_active:
_update_timer()
func _update_timer() -> void:
if not _nm: return
var t = int(_nm.match_time_remaining)
var m = int(t / 60.0)
var s = t % 60
_time_label.text = "%02d:%02d" % [m, s]
func _populate_scoreboard_headers() -> void:
for child in _scoreboard_grid.get_children():
child.queue_free()
var headers = ["Player", "Kills", "Deaths", "Assists", "Ping"]
for h in headers:
var l = Label.new()
l.text = h
l.add_theme_font_size_override("font_size", 24)
l.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
_scoreboard_grid.add_child(l)
func _update_hud() -> void:
if not _nm: return
var my_id = multiplayer.get_unique_id()
var leader_name = "None"
var max_kills = -1
var stats = _nm.player_stats
# Sort players by kills for scoreboard
var players = stats.keys()
players.sort_custom(func(a, b): return stats[a].get("kills", 0) > stats[b].get("kills", 0))
_populate_scoreboard_headers()
for pid in players:
var p_data = stats[pid]
var p_kills = p_data.get("kills", 0)
# Find leader
if p_kills > max_kills:
max_kills = p_kills
leader_name = p_data.get("username", "Player " + str(pid))
# Update local kills
if pid == my_id:
_local_kills_label.text = str(p_kills) + " Kills"
# Build Scoreboard Row
var c_name = Label.new()
c_name.text = p_data.get("username", "Player")
c_name.add_theme_font_size_override("font_size", 24)
_scoreboard_grid.add_child(c_name)
var c_k = Label.new()
c_k.text = str(p_kills)
c_k.add_theme_font_size_override("font_size", 24)
_scoreboard_grid.add_child(c_k)
var c_d = Label.new()
c_d.text = str(p_data.get("deaths", 0))
c_d.add_theme_font_size_override("font_size", 24)
_scoreboard_grid.add_child(c_d)
var c_a = Label.new()
c_a.text = str(p_data.get("assists", 0))
c_a.add_theme_font_size_override("font_size", 24)
_scoreboard_grid.add_child(c_a)
var c_p = Label.new()
c_p.text = str(p_data.get("ping", 0)) + "ms"
c_p.add_theme_font_size_override("font_size", 24)
_scoreboard_grid.add_child(c_p)
_leader_label.text = "Leader: %s (%s Kills)" % [leader_name, max_kills]
func _on_killfeed_event(victim: String, killer: String, weapon: String) -> void:
var l = Label.new()
if killer == "":
l.text = "%s died." % victim
else:
l.text = "%s [%s] %s" % [killer, weapon, victim]
l.add_theme_font_size_override("font_size", 20)
l.add_theme_color_override("font_outline_color", Color.BLACK)
l.add_theme_constant_override("outline_size", 4)
_killfeed_vbox.add_child(l)
# Fade out and queue_free after 5 seconds
var t = get_tree().create_tween()
t.tween_interval(4.0)
t.tween_property(l, "modulate:a", 0.0, 1.0)
t.tween_callback(l.queue_free)
+1
View File
@@ -0,0 +1 @@
uid://dymuoha2tdj63
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://c54h8k8u8p7q6"]
[ext_resource type="Script" path="res://ui/match_hud.gd" id="1_match"]
[node name="MatchHUD" type="CanvasLayer"]
script = ExtResource("1_match")