前言:本文是Godot使用新框架实现的一个全局AutoLoad网络管理中心,相对于旧框架

针对Godot(2D)游戏架构的研究_godot(2d)最佳架构-CSDN博客 ,本文将使用全局自动加载直接作为单例系统,而不是手写。

在阅读本文前,会用到编辑器操作:

Godot(2D)架构细分4:编辑器全局设置解释-CSDN博客 中的全局AutoLoad设置

1. Godot全局AutoLoad网络管理中心概述

        1. Godot中使用的网络

        (这段是本架构实现需要知道的基本知识)

        Godot中网络联机使用的网络库是ENet,ENet 是基于 UDP 的可靠传输库,相对原始UDP传输,ENet在TCP与UDP之间取了平衡 (通过 ACK 确认、超时重传、有序重组实现了可控的可靠性),具有一定的可靠性。

        UDP原生可以理解为数据直接往指定位置发送,与接收端不建立连接、不重传、不保证有序,速度极快,但是会丢包

        Godot中ENet的核心用法结构是:1个服务端,多个客户端

        同时ENet可以支持设置 Reliable(可靠不丢包) 传输与 Unreliable(不可靠速度快) 等4种传输模式,演示只使用提到的两种

自动缓存玩家已发送的数据,底层自动处理连接管理(UDP实现的伪三次握手,自动超时断连等)

需要注意的是,Godot中使用 @rpc 收发数据,由发送端发起,由 @rpc 配置决定 同结构下的同名带 @rpc关键字的函数 谁执行

所以网络AutoLoad结构必须一致 ,AutoLoad只负责数据的收发 ,处理必须将数据传出

        2. Godot本网络架构实现核心要点

1. 封装原生 multiplayerAPI 到全局网络服务单例,避免直接使用 multiplayer 实现网络连接

2. 全局网络对于数据传输,只负责数据的传输与接收,并且接收数据后需将数据传出供逻辑层使用

3. 外部函数应避免直接修改单例中的变量,尽量使用开放API

2. 全局AutoLoad网络管理中心的具体实现

        首先给出本演示文件结构(网络部分) 以及网络管理器场景

        1. 构建全局网络AutoLoad

构建全局网络AutoLoad,首先将全局网络节点加入全局自动加载,这里我给全局网络节点命名为Server

使用AutoLoad创建的全局场景,可以直接使用 名字.代码 的方式调用自动加载的场景的根节点对象的代码

然后如上图管理器场景所示,将主管理器下面划分为服务端客户端,用以分别控制服务器代码和客户端代码

这里根节点Server只负责向外提供API,内部创建网络等一系列操作由子管理器完成并返回给根节点。

        2. 网络根管理器向外API开放实现

根Server节点负责向外提供创建网络,创建连接等一系列操作的API,这里是实现代码

extends Node2D

#服务端口,客户端口,服务器运行端口的变量(子管理器)
@export var ServerSide: 		Node2D		# 服务端管理器
@export var Client: 			Node2D		# 客户端管理器
@export var ServerRunning:		Node2D		# 服务器运行时管理器
@export var GameRunning:		Node2D		# 游戏运行时管理器

# 是否已经创建服务端,即服务器状态管理(客户端)
var alreadyCreateServe: bool 		= false
var alreadyCreateClient: bool		= false
		
func _ready() -> void:
	#这里接收所有子管理器信号as
	_signal_initialize()

#创建服务端
func create_serve() -> void:
	# 只启动服务端或者客户端
	if alreadyCreateServe or alreadyCreateClient:
		return
	
	# 调用子管理器创建,得到是否创建成功的返回值
	if ServerSide.create_server():
		alreadyCreateServe = true
		ServerRunning.player_list.append("1")

# 创建客户端
func create_client() 				-> void:
	# 只启动服务端或者客户端
	if alreadyCreateClient or alreadyCreateServe:
		return
	
	## 创建客户端成功,但是没有连接成功返回true
	## 连接失败返回信号并重新变更为false,信号函数在下文
	if Client.create_client():
		alreadyCreateClient = true

# 对外开放 关闭服务器
func close_serve() 					-> void:
	if !alreadyCreateServe:
		return
	
	# 关闭服务器
	if !ServerSide.close_serve():
		print("服务器关闭失败")
		return
	alreadyCreateServe = false
	
# 对外开放 关闭客户端
func close_client() 				-> void:
	if !alreadyCreateClient:
		return
	
	# 关闭客户端连接
	if !Client.close_client():
		print("客户端关闭失败")
		return
	alreadyCreateClient = false
	
## 子管理器信号连接
func _signal_initialize()					-> void:
	#这里连接的是子管理器客户端连接超时的信号
	Client.server_connect_failed.connect(_on_client_connect_failed)

# 客户端连接失败
func _on_client_connect_failed()			-> void:
	alreadyCreateClient = false
	
# 同步玩家位置信息
func sync_position(send_position: Vector2)-> void:
	GameRunning.sync_with_player_position_dic(send_position)
	
# 同步玩家杂项属性信息(补充)
func sync_infomation(send_information: Dictionary) -> void:
	GameRunning.sync_with_player_information(send_information)
	
# 网络全局场景切换	
func change_all_scene(path: String) -> void:
	# 服务器切换场景
	if !(multiplayer.get_unique_id() == 1):
		return
	
	rpc_change_scene(path)
	rpc_change_scene.rpc(path)

# 得到玩家列表
func get_player_list()				-> Array:
	# 这里 .duplicate() 返回的是数组的副本,防止篡改内部信息
	return ServerRunning.player_list.duplicate()
	
# 得到玩家位置信息字典
func get_player_position_dic()						-> Dictionary:
	# 这里 .duplicate() 返回的是字典的副本,防止篡改内部信息
	return GameRunning.player_position_dic.duplicate()

# 得到玩家杂项信息字典(补充)
func get_player_information()						-> Dictionary:
	# 这里 .duplicate() 返回的是字典的副本,防止篡改内部信息
	return GameRunning.player_information.duplicate()
	
# 更新主机玩家位置
func change_main_player_position(main_position: Vector2)			-> void:
	GameRunning.server_position = main_position
	
# 更新主机玩家信息(补充)
func change_main_player_infomation(main_information: Dictionary)	-> void:
	GameRunning.server_dic = main_information
	
# 发送信息
func send_message(msg: String) 		-> void:
	GameRunning.send_player_message(msg)
	
# 获取玩家信息列表
func get_player_chat_list() 		-> Array:
	# 这里 .duplicate() 返回的是数组的副本,防止篡改内部信息
	return GameRunning.player_chat_messages.duplicate()

# 返回服务器(客户端)连接状态
func is_connect() -> bool:
	var peer = multiplayer.multiplayer_peer
	if peer == null or peer is OfflineMultiplayerPeer:
		return false

	return peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED

# 得到玩家网络ID
func get_player_id() -> int:
	return multiplayer.get_unique_id()
		
# 切换场景全局广播
@rpc("any_peer", "reliable")
func rpc_change_scene(path: String):
	get_tree().change_scene_to_file(path)

以上代码对外开放了服务器的基本操作,其他场景仅需要使用

Server.create_serve()

Server.create_client()

等操作实现服务器的创建,关闭等操作

3. 子管理器代码实现

首先解释: 本网络架构为对等体架构(Peer - to - Peer 架构)

对等体架构:

对等网络(Peer-to-Peer,简称P2P)是一种分布式应用架构

在这种架构中,网络中的每个节点(Peer)既是资源、服务和内容的提供者(Server),又是资源、服务和内容的获取者(Client)。这种网络结构没有中心节点,每个节点都可以直接与其他节点进行通信和资源共享。

Godot中通过创建对等体进行网络数据的传输,每个在网络中的对等体有自己独特的ID,其中服务器对等体的ID固定为1, 其余客户端均为随机值

        子管理器负责网络的实现部分,包括创建网络,关闭网络,检查错误等等。

        这里将网络子管理器的客户端服务端,以及服务器和客户端共用的接收端三个子管理器分开,便于实现不同操作的单独控制

        1. 服务端

        服务器代码包含对根管理器开放的创建服务器关闭服务器等操作,以下为实现代码

extends Node2D

#演示,固定端口 7777,以及最大连接数量为4
var PORT: int							= 7777
var MAX_PLAYER: int 					= 4

#创建服务器
func create_server() -> bool:
	#创建连接器(创建对等体,注意此时还没有规定服务端)
	var peer = ENetMultiplayerPeer.new()
	
	if peer == null:
		push_error("服务器启动失败")
		return false
		
	#创建服务端(这时候规定了此对等体为服务端)
	var err = peer.create_server(PORT, MAX_PLAYER)
	if err != OK:
		push_error("服务器启动失败")
		return false
	multiplayer.multiplayer_peer = peer
	
	## 启动监听
	## peer_connected 			是Godot中网络连接自带信号,负责监听是否有新连接
	##	peer_disconnected 		是Godot中网络连接自带信号,负责监听是否有连接断开
	
	multiplayer.peer_connected.connect(_on_peer_connect)
	multiplayer.peer_disconnected.connect(_on_peer_disconnect)
	print("服务器启动,端口号:", PORT)
	return true
	
#服务器接入端口
func _on_peer_connect(peer_id) -> void:
	print("玩家连入: ID:", peer_id)
	rpc_id(peer_id, "attend_game")
	
#服务器接出端口
func _on_peer_disconnect(peer_id) -> void:
	print("玩家断开: ID:", peer_id)
	
#关闭服务器
func close_serve() -> bool:
	#获取服务器对等体并断开(关闭服务器)
	var peer = multiplayer.multiplayer_peer
	if peer == null:
		return false
	
	#关闭服务器(防止客户端因为某种原因点击关闭服务器后误关客户端自己)
	if !(multiplayer.get_unique_id() == 1):
		return false
	
	#断开信号连接
	multiplayer.peer_connected.disconnect(_on_peer_connect)
	multiplayer.peer_disconnected.disconnect(_on_peer_disconnect)
	
	peer.close()
	#清空网络设置
	multiplayer.multiplayer_peer = null
	if multiplayer.multiplayer_peer != null:
		return false

	print("服务器已关闭")
	return true

        2. 客户端

        客户端代码包含对根管理器开放的创建客户端关闭客户端等操作,以下为实现代码

extends Node2D

#固定连接端口(演示)
var PORT: int 			= 7777
#本机测试地址
var SERVER_IP: String 	= "26.224.10.101"

#服务器连接超时信号
signal server_connect_failed

#创建客户端
func create_client() -> bool:
	var peer = ENetMultiplayerPeer.new()
	if peer == null:
		push_error("客户端启动失败")
		
	#创建客户端(对等体连接IP)
	var err = peer.create_client(SERVER_IP, PORT)
	if err != OK:
		push_error("服务器启动失败")
		return false
	multiplayer.multiplayer_peer = peer
	
	## 监听连接结果
	## connected_to_server 			为Godot中网络自带API,负责检测成功连接到服务器(连接成功)
	## connection_failed			为Godot中网络自带API,负责检测未能连接到服务器(连接超时)
	
	multiplayer.connected_to_server.connect(_on_connected_ok)
	multiplayer.connection_failed.connect(_on_connected_fail)
	print("正在连接服务器")
	
	return true

func _on_connected_ok() -> void:
	print("成功连接到服务器")
	
func _on_connected_fail() -> void:
	print("连接服务器失败")
	
	#连接超时发送连接超时信号
	server_connect_failed.emit()

# 断开客户端连接
func close_client() -> bool:
	# 获取客户端对等体并清空
	var peer = multiplayer.multiplayer_peer
	if peer == null:
		return false
	
	#关闭客户端(防止服务器端口误点击关闭客户端而关闭服务端)
	if multiplayer.get_unique_id() == 1:
		push_error("本机器为服务端口,不是客户端口,不可使用客户端口关闭")
		return false
	
	#断开信号连接
	multiplayer.connected_to_server.disconnect(_on_connected_ok)
	multiplayer.connection_failed.disconnect(_on_connected_fail)

	peer.close()
	#清空网络设置
	multiplayer.multiplayer_peer = null
	if multiplayer.multiplayer_peer != null:
		return false
	
	print("已断开连接")
	return true

        3. 服务器和客户端共用的接收端

服务器和客户端共用的接收端 负责统一全局网络网络数据传输操作,保证网络中各个对等体的需要同步的数据相同,同时将得到的数据传给游戏逻辑层或其他层

需要注意的是,只有服务器端第一时间更新数据,再同步发送给其他对等体客户端

extends Node2D

##	这里先演示同步玩家ID
##	这里是手动同步,Godot可以使用multiplayer.get_peers()直接获得玩家列表
##	拆开的手写同步的目的是:
##	1. Godot中同步的玩家数据不会有服务器ID,需要手动加
##	2. 将玩家列表从 服务器AutoLoad传出去而不是直接调用Godot网络API
##	需要注意,@rpc 关键字标明的代码需要使用如 player_list_get.rpc(player_list) 来进行发射
##	.rpc() 发射代码,括号里面可以写入传参

#玩家列表
var player_list: Array[String]				= []

func _ready() -> void:
	#初始化玩家信号连接
	_initialize_signal()
	
func _physics_process(delta: float) -> void:
	#	持续检测玩家连接(注意这里是清空列表,不是持续更新列表),更新列表
	#	不是持续更新玩家列表,这里条件到了只执行一次
	_detect_connect()

#	接收到玩家连接信号,后将玩家列表更新
func online_player_connect(peer_id: int) 			-> void:
	
	##	只有服务器端第一时间更新数据
	##	multiplayer.get_unique_id() == 1 这句是判断是否为服务器端
	if multiplayer.get_unique_id() == 1:
		print("服务器接收: 新玩家加入: ID: %d" % peer_id)
		player_list.append(str(peer_id))
		#发送数据给其他客户端对等体
		player_list_get.rpc(player_list)
		print("服务端口: ", player_list)
	
	#	客户端受到玩家加入信号,输出玩家信息	
	if multiplayer.get_unique_id() != 1:
		print("客户端接收: 新玩家加入: ID: %d" % peer_id)

#	接收玩家断连接信号,更新玩家列表
func online_player_disconnect(disconnect_peer_id: int)						-> void:
	# 先转换为字典种存储的字符串类型
	var peer_id: String = str(disconnect_peer_id)
	# 断开连接清空数据(只有服务端)
	if multiplayer.get_unique_id() == 1:
		player_list.erase(peer_id)
		# 服务器端发送玩家列表
		player_list_get.rpc(player_list)

		# 清理玩家数据
		var game = get_parent().GameRunning
		if game:
			# 擦除玩家字典与属性
			if game.player_position_dic.has(peer_id):
				game.player_position_dic.erase(peer_id)
			if game.player_information.has(peer_id):
				game.player_information.erase(peer_id)
				
			# 重新发送玩家数据同步
			game.player_list_get.rpc(game.player_position_dic)
			game.player_list_information.rpc(game.player_information)


## 统一发送玩家列表
## @rpc("authority", "reliable")
## 意思为 1. 可发送端: 服务权威端  2. 发送模式: 可靠不丢包发送
@rpc("authority", "reliable")
func player_list_get(get_player_list: Array[String]) 				-> void:
	# 服务器端发送玩家列表
	if multiplayer.get_unique_id() != 1:
		player_list = get_player_list
		print("客户端口: ", player_list)

#初始化连接所有玩家信号
func _initialize_signal() 				-> void:
	## 连接玩家连接,断连数据
	## 连接函数 online_player_connect()
	## 连接函数 online_player_disconnect()
	## 触发信号自动连接发送函数
	multiplayer.peer_connected.connect(online_player_connect)
	multiplayer.peer_disconnected.connect(online_player_disconnect)
	
#客户端自检测
func _detect_connect()				-> void:
	## 检测联机节点
	## 断连自动清除玩家列表
	if multiplayer.multiplayer_peer == null:
		if player_list != []:
			player_list.clear()
		return
	
	# 这里检测对等体是否为空或者本地离线对等体,清空玩家列表
	if multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
		if player_list != []:
			player_list.clear()
		return
	
	var state = multiplayer.multiplayer_peer.get_connection_status()
	
	#检查断连
	if state == multiplayer.multiplayer_peer.CONNECTION_DISCONNECTED:
		if player_list != []:
			player_list.clear()
			

	

这里得到的数据列表,要外部需要的部分手动来取

3. 实际演示

        这里实际演示也有调用代码,将会分文章细讲,这里先给出效果

1. 启动三个演示

2. 使用其中一个窗口创建服务器

3. 加入一个玩家客户端口

4. 再次加入一个玩家,服务器自动同步

5. 关闭服务器,其余客户端自动断开

这里的玩家列表显示,是外部调用服务器玩家列表后,实例化管理玩家列表的效果,后续将给出文章细讲。

4. 开源以及调用细节

        1. 游戏运行时管理器补充(游戏数据传输管理器)

           Godot(4.X): 全局AutoLoad网络管理中心: 全局数据同步-CSDN博客

        2. 游戏实际使用演示

                1. 玩家位置控制与信息同步

                   Godot(4.X): 网络管理中心细分:玩家位置控制与同步-CSDN博客

                2. 玩家聊天系统

                   Godot(4.X): 网络管理中心细分:玩家通用聊天系统-CSDN博客

        2. 演示开源地址(本网络中心AutoLoad支持扩展)

looooiiui/2D-Online-Sample-Game: 这是一个2D联机的示例游戏,使用全局管理器,联机解耦,可以扩展

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐