extends CanvasLayer ## 联机总入口(Autoload 名:Net,指向 res://autoload/net.tscn) ## ## 职责边界:只负责「选传输层 + 开局/加入/断开 + 玩家生成回收」。 ## 玩法代码不直接接触 MultiplayerPeer —— 以后要把 ENet 换成 Steam P2P, ## 只需要改本文件的 host_game / join_game 两个函数,RPC 与 Spawner 层不用动。 ## ## 界面是**节点配置**:整个 UI 在 res://autoload/net.tscn 里用节点搭好, ## 本脚本只做「改文字、切显隐」这类状态更新,不动态创建任何 Control。 ## 按钮的 pressed 信号也在 net.tscn 里连接(打开场景 → 选按钮 → 节点面板 → 信号)。 ## ## 本机双开调试(命令行): ## 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" ## 开局后要进的世界场景,以及断开后要回的入口场景 const WORLD_SCENE: String = "res://world/main.tscn" const MENU_SCENE: String = "res://ui/main_menu.tscn" var _spawner: MultiplayerSpawner = null @onready var _menu: CenterContainer = $Menu @onready var _address_edit: LineEdit = $Menu/Panel/Box/AddressEdit @onready var _menu_status: Label = $Menu/Panel/Box/StatusLabel @onready var _hud: Control = $Hud @onready var _hud_status: Label = $Hud/Box/StatusLabel 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) _show_menu_ui() call_deferred("_apply_cmdline") # ---------------- 对外接口 ---------------- ## 世界根节点在 _ready 里调用,把 MultiplayerSpawner 托管给 Net。 ## ## 世界是「开局之后」才加载的(入口是开始菜单),所以这里得把已经连上的玩家补生成一遍 —— ## 否则主机在菜单里就开好了,切进世界时一个角色都不会出现。 func bind_spawner(spawner: MultiplayerSpawner) -> void: _spawner = spawner if multiplayer.is_server(): call_deferred("_spawn_player", 1) for id in multiplayer.get_peers(): call_deferred("_spawn_player", id) ## 菜单那边用它开关「联机」面板 —— 面板默认是显示的,会盖住入口菜单。 func set_menu_visible(on: bool) -> void: _menu.visible = on ## 创建主机。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) # 入口是开始菜单,世界这时还没加载 —— 切过去,角色由 bind_spawner 补生成 call_deferred("_enter_world") ## 加入主机。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() # 世界一卸载 spawner 就跟着没了,留着引用会指向已释放的节点 _spawner = null Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _show_menu_ui() _set_status("已断开") call_deferred("_go_to", MENU_SCENE) ## 切到世界场景。已经在里面就不重复切 func _enter_world() -> void: _go_to(WORLD_SCENE) func _go_to(path: String) -> void: var current := get_tree().current_scene if current != null and current.scene_file_path == path: return get_tree().change_scene_to_file(path) # ---------------- 网络事件 ---------------- 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] 已连接,等待主机生成角色") call_deferred("_enter_world") 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 not is_instance_valid(_spawner) 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 not is_instance_valid(_spawner): 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) # ---------------- 界面(节点都在 net.tscn 里,这里只管状态) ---------------- ## 两个按钮的 pressed 信号在 net.tscn 里连到这里 func _on_host_button_pressed() -> void: host_game() func _on_join_button_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.visible = false _hud.visible = true _update_hud() func _show_menu_ui() -> void: _menu.visible = true _hud.visible = false func _set_status(text: String) -> void: _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]