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]