Files
2026-10-10 16:07:53 +08:00

230 lines
7.5 KiB
GDScript
Raw Permalink 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 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:
# ENet 对「端口已被占用」和其他建站失败返回**同一个错误码**(ERR_CANT_CREATE,实测为 20),
# 所以这里没法按错误码分流,直接把最常见的原因写进提示 ——
# 同机双开调试时,先启动的那个实例已经是主机了,第二个必然抢不到端口。
_set_status("创建主机失败:端口 %d 无法绑定,可能已被占用(已有实例在当主机?)" % PORT)
print("[Net] 创建主机失败:%s(端口 %d)" % [error_string(err), PORT])
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]