270 lines
7.4 KiB
GDScript
270 lines
7.4 KiB
GDScript
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]
|