Files
agame/autoload/net.gd
T
2026-09-22 16:56:35 +08:00

270 lines
7.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node
## 联机总入口(Autoload 名:Net)
##
## 职责边界:只负责「选传输层 + 开局/加入/断开 + 玩家生成回收」。
## 玩法代码不直接接触 MultiplayerPeer —— 以后要把 ENet 换成 Steam P2P,
## 只需要改本文件的 host_game / join_game 两个函数,RPC 与 Spawner 层不用动。
##
## 本机双开调试(命令行):
## godot --path <项目目录> -- --host
## godot --path <项目目录> -- --join 127.0.0.1
const PORT: int = 7000
const MAX_PLAYERS: int = 8
const DEFAULT_ADDRESS: String = "127.0.0.1"
## 所有玩家节点都会加入这个组,方便整批清场
const PLAYER_GROUP: String = "net_player"
var _spawner: MultiplayerSpawner = null
var _menu_root: Control = null
var _hud_root: Control = null
var _address_edit: LineEdit = null
var _menu_status: Label = null
var _hud_status: Label = null
func _ready() -> void:
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
multiplayer.connected_to_server.connect(_on_connected_to_server)
multiplayer.connection_failed.connect(_on_connection_failed)
multiplayer.server_disconnected.connect(_on_server_disconnected)
_build_ui()
call_deferred("_apply_cmdline")
# ---------------- 对外接口 ----------------
## 世界根节点在 _ready 里调用,把 MultiplayerSpawner 托管给 Net。
func bind_spawner(spawner: MultiplayerSpawner) -> void:
_spawner = spawner
## 创建主机。ENet 是客户端-服务器模型,主机同时是服务器与玩家。
func host_game() -> void:
var peer := ENetMultiplayerPeer.new()
var err := peer.create_server(PORT, MAX_PLAYERS)
if err != OK:
_set_status("创建主机失败:%s" % error_string(err))
return
multiplayer.multiplayer_peer = peer
_show_game_ui()
print("[Net] 主机已启动,端口 %d" % PORT)
# 服务器自己也要一个角色。延后一帧,等各端场景就绪。
call_deferred("_spawn_player", 1)
## 加入主机。address 为主机在局域网内的内网 IP。
func join_game(address: String = DEFAULT_ADDRESS) -> void:
var target := address.strip_edges()
if target.is_empty():
target = DEFAULT_ADDRESS
var peer := ENetMultiplayerPeer.new()
var err := peer.create_client(target, PORT)
if err != OK:
_set_status("连接失败:%s" % error_string(err))
return
multiplayer.multiplayer_peer = peer
_set_status("正在连接 %s:%d …" % [target, PORT])
print("[Net] 正在连接 %s:%d" % [target, PORT])
## 断开并清场,回到主菜单。
func leave_game() -> void:
multiplayer.multiplayer_peer = null
_clear_players()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_show_menu_ui()
_set_status("已断开")
# ---------------- 网络事件 ----------------
func _on_peer_connected(id: int) -> void:
# 只有服务器生成角色,客户端靠 MultiplayerSpawner 自动收到同一份。
if multiplayer.is_server():
call_deferred("_spawn_player", id)
func _on_peer_disconnected(id: int) -> void:
if multiplayer.is_server():
_despawn_player(id)
func _on_connected_to_server() -> void:
_show_game_ui()
print("[Net] 已连接,等待主机生成角色")
func _on_connection_failed() -> void:
multiplayer.multiplayer_peer = null
_set_status("连接失败:主机无响应")
func _on_server_disconnected() -> void:
multiplayer.multiplayer_peer = null
_clear_players()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_show_menu_ui()
_set_status("主机已关闭")
# ---------------- 玩家生成与回收 ----------------
func _spawn_player(peer_id: int) -> void:
if _spawner == null or not multiplayer.is_server():
return
var parent := _spawner.get_node_or_null(_spawner.spawn_path)
if parent == null or parent.has_node(str(peer_id)):
return
_spawner.spawn(peer_id)
func _despawn_player(peer_id: int) -> void:
if _spawner == null:
return
var parent := _spawner.get_node_or_null(_spawner.spawn_path)
if parent == null:
return
var node := parent.get_node_or_null(str(peer_id))
if node != null:
node.queue_free()
func _clear_players() -> void:
for node in get_tree().get_nodes_in_group(PLAYER_GROUP):
node.queue_free()
# ---------------- 命令行调试入口 ----------------
func _apply_cmdline() -> void:
var args := OS.get_cmdline_user_args()
if args.has("--host"):
host_game()
elif args.has("--join"):
var index := args.find("--join")
var address := DEFAULT_ADDRESS
if index + 1 < args.size():
address = args[index + 1]
join_game(address)
# ---------------- 界面 ----------------
func _build_ui() -> void:
var menu_layer := CanvasLayer.new()
menu_layer.name = "NetMenu"
menu_layer.layer = 20
add_child(menu_layer)
_menu_root = _build_menu()
menu_layer.add_child(_menu_root)
var hud_layer := CanvasLayer.new()
hud_layer.name = "NetHud"
hud_layer.layer = 19
add_child(hud_layer)
_hud_root = _build_hud()
_hud_root.visible = false
hud_layer.add_child(_hud_root)
func _build_menu() -> Control:
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
var panel := PanelContainer.new()
center.add_child(panel)
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 10)
panel.add_child(box)
var title := Label.new()
title.text = "联机测试"
title.add_theme_font_size_override("font_size", 20)
box.add_child(title)
_address_edit = LineEdit.new()
_address_edit.text = DEFAULT_ADDRESS
_address_edit.custom_minimum_size = Vector2(240, 0)
box.add_child(_address_edit)
var host_button := Button.new()
host_button.text = "创建主机"
host_button.pressed.connect(host_game)
box.add_child(host_button)
var join_button := Button.new()
join_button.text = "加入主机"
join_button.pressed.connect(_on_join_pressed)
box.add_child(join_button)
_menu_status = Label.new()
_menu_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_menu_status.custom_minimum_size = Vector2(240, 0)
box.add_child(_menu_status)
var hint := Label.new()
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
hint.custom_minimum_size = Vector2(240, 0)
hint.modulate = Color(1, 1, 1, 0.7)
hint.text = "局域网内填主机的内网 IP;本机双开测试填 %s" % DEFAULT_ADDRESS
box.add_child(hint)
return center
func _build_hud() -> Control:
var layer_root := Control.new()
layer_root.set_anchors_preset(Control.PRESET_FULL_RECT)
layer_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
var box := VBoxContainer.new()
box.position = Vector2(16, 12)
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
layer_root.add_child(box)
_hud_status = Label.new()
box.add_child(_hud_status)
var hint := Label.new()
hint.modulate = Color(1, 1, 1, 0.7)
hint.text = "F10 断开 · Esc 释放鼠标"
box.add_child(hint)
return layer_root
func _on_join_pressed() -> void:
join_game(_address_edit.text)
func _unhandled_key_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo \
and event.keycode == KEY_F10 and multiplayer.multiplayer_peer != null:
leave_game()
get_viewport().set_input_as_handled()
func _show_game_ui() -> void:
_menu_root.visible = false
_hud_root.visible = true
_update_hud()
func _show_menu_ui() -> void:
_menu_root.visible = true
_hud_root.visible = false
func _set_status(text: String) -> void:
if _menu_status != null:
_menu_status.text = text
func _update_hud() -> void:
var role := "主机" if multiplayer.is_server() else "客户端"
var players := multiplayer.get_peers().size() + 1
_hud_status.text = "联机中 · %s · 玩家 %d" % [role, players]