前言: 本文是对 Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客 的扩展,在此基础上增加了全局网络信息管理,可以通过AutoLoad提供的开放API实现玩家数据字典的同步,同时将部分全局网络连接状态如是否连上服务器等需要用于判断的状态写入了API

请先阅读 Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客

1. 全局网络管理中心运行时子管理器

       1. 运行时子管理器概述

        全局网络管理中心除了启动和创建服务器的2个子管理器 (服务端管理器客户端管理器) 之外,还需要运行时管理器。运行时管理器负责各个独立游戏窗口之间的数据传输,在本网络架构中(Godot中实现的P2P架构,即对等体架构),分成了

1. 服务器运行时管理器

2. 游戏运行时管理器

同时需要注意的是,子管理器中的各种函数,如发送玩家数据,均使用主管理器开放公共API来提供调用

这里给出新的节点结构图

        2. 核心实现要点与强调

1. Godot中使用 @rpc 收发数据,由发送端发起,由 @rpc 配置决定 同结构下的同名带 @rpc关键字的函数 谁执行,所以网络AutoLoad结构必须一致 ,AutoLoad只负责数据的收发 ,处理必须将数据传出。

对于本实现而言,字典数据必须由根管理器通过API统一传出副本(因为Godot中直接return返回的是地址引用,有直接更改网络中心数据的风险)

2. 服务器数据传输逻辑完全由运行时管理器负责,根管理器只负责API接口

2. 全局网络管理中心运行时子管理器具体实现

        在实现代码之前,有必要先对描述的顺序进行说明:

        1. 服务器运行时管理器实现

        2. 游戏运行时管理器代码实现 (本文重点)

        3. 根管理器代码开放API代码补充

注意:这里的服务器运行时管理器即

Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客

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

此外,本架构为对等体(P2P)架构,伪服务器中心为主机端

        1. 服务器运行时管理器

        服务器运行时管理器 负责统一全局网络服务器 基本网络数据 传输操作,保证网络中各个对等体的需要同步的数据相同,同时将得到的数据传给游戏逻辑层或其他层

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

这里具体代码在

Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客

已经演示,可以点进查阅。

         2. 游戏运行时管理器代码实现

        游戏运行时管理器 负责同步网络服务器中 所有玩家游戏数据(玩家数据,关卡信息) ,保证网络中各玩家之间 客户端显示内容 以及 关卡信息 的同步。

        游戏运行时管理器传输玩家数据,但是需要注意的是:

        1. 最终确定游戏信息的是主机端口

        2. 绝大部分同步数据均只有主机能够发送,客户端负责接收同步数据并渲染。

        3. Godot中主机 rpc发送 并不会执行主机端自己的函数,同时主机的玩家列表也不会包含主机,也就意味着要手动对服务器加入主机的信息。

本处传输的玩家数据字典结构为 { "玩家ID" : 每个玩家各自的数据字典(包含玩家信息) }

以下为代码部分

## Godot 中主机端的对等体(multiplayer.multiplayer_peer)ID 为 1
## 而其他连入的客户端的ID每次加入服务器都是随机产生的
## 所以除了 multiplayer.is_server() 外
## 还可以通过 multiplayer.get_unique_id() == 1 识别主机

extends Node2D

@export var player_position_dic:		Dictionary[String, Vector2]		= {}  # 玩家同步位置字典
@export var player_information:			Dictionary[String, Dictionary]	= {}  # 玩家同步杂项信息字典
@export var player_list:				Array[String]					= []  # 玩家同步列表
@export var player_chat_messages:		Array[String]					= []  # 客户端自己维护的聊天信息缓存
@export var max_chat_num:				int								= 10  # 最大聊天数量缓存

## 服务器主玩家自己从根管理器传进来的信息
## 因为主机不会自己维护自己的信息,需要手动加入
var server_position: 			 		Vector2				= Vector2(0, 0) # 主机玩家位置
var server_dic:							Dictionary			= {}			# 主机玩家属性杂项字典

# 外置调用同步API(这里是给根管理器调用的API,调用后主机端发送玩家位置)
func sync_with_player_position_dic(send_position: Vector2 = Vector2(0, 0)):
	# 发送函数 _sync_with_player_position_dic()
	_sync_with_player_position_dic.rpc(send_position)
	
# 玩家属性信息发送(这里是给根管理器调用的API,调用后主机端发送玩家杂项信息)
func sync_with_player_information(send_information: Dictionary = {}):
	# 发送函数 _sync_with_player_information()
	_sync_with_player_information.rpc(send_information)

# 这里是玩家聊天信息发送
func send_player_message(msg: String):
	## 存入格式 [player_id] : information
	## 存入格式在发送前自动处理完毕
	var send_msg = "[" + str(multiplayer.get_unique_id()) + "]: " + msg
	
	## 需要本地先执行一次 rpc_player_chat()
	## 因为 rpc_player_chat.rpc() 发送不会执行本地的函数
	## 所以主机需要提前同步一次
	rpc_player_chat(send_msg)
	rpc_player_chat.rpc(send_msg)
	
## 任意客户端发送位置
## 这里需要输入发送方的位置信息,后自动全网广播
## 其他对等体收到后会均会执行函数,实参为发送方发送(.rpc)的变量
@rpc("any_peer", "unreliable")
func _sync_with_player_position_dic(send_position: Vector2 = Vector2(0, 0)) 				-> void:
	## 增加新玩家位置
	## multiplayer.get_unique_id() == 1 的意思是 如果是主机端
	if multiplayer.get_unique_id() == 1:	
		# 得到发送信息的对等体的远程ID
		var player_id: String = str(multiplayer.get_remote_sender_id())
		## 未连接退出
		## 对等体未收到远程消息的时候远程ID默认为0
		if player_id == "0":
			return
		
		## 这里是为了防止丢包后玩家位置闪回为Vector(0, 0)
		## 因为可以看到在发送位置但是没有传参的情况下 send_position 的值为 Vector(0, 0)
		if send_position == Vector2(0, 0):
			return
	
		player_position_dic[player_id] 	= send_position		# 主机端在位置同步字典中更新服务器发送方玩家位置
		player_position_dic["1"]		= server_position	# 主机端自己更新自己位置
		player_list_get.rpc(player_position_dic)			# 主机端发送玩家数据字典,调用 player_list_get.rpc
## 任意客户端发送玩家数据		
## 发送玩家离散数据
@rpc("any_peer", "unreliable")
func _sync_with_player_information(send_information: Dictionary = {})	-> void:
	
	## 主机端收到后更新玩家数据
	if multiplayer.get_unique_id() == 1:
		# 得到远程ID
		var player_id: String = str(multiplayer.get_remote_sender_id())
		#未连接退出
		if player_id == "0":
			return
			
		# 防止接收空数据导致外部取到不存在的键值
		if send_information == {}:
			return

		player_information[player_id] = send_information	# 主机端在玩家杂项数据字典中更新服务器发送方玩家位置
		player_information["1"]	  = server_dic				# 主机端更新自己的杂项数据
		player_list_information.rpc(player_information)		# 主机端发送玩家数据字典,调用 player_list_information.rpc
	
# 仅主机端可发送,可靠发送
@rpc("authority", "reliable")
func player_list_get(get_player_position_dic: Dictionary) 				-> void:
	## 服务器端发送玩家列表
	## 只有非主机端玩家可执行字典替换操作
	if multiplayer.get_unique_id() != 1:
		player_position_dic = get_player_position_dic

# 仅主机端可发送,可靠发送	
@rpc("authority", "reliable")
func player_list_information(get_player_information: Dictionary)		-> void:
	## 服务器端发送玩家杂项信息
	## ## 只有非主机端玩家可执行字典替换操作
	if multiplayer.get_unique_id() != 1:
		player_information = get_player_information	
	
## 玩家聊天信息广播
## 任何人可发送,可靠发送
@rpc("any_peer", "reliable")
func rpc_player_chat(msg: String):
	## 存入格式 [player_id] : information 
	## 格式处理在上面调用API传入前已经处理完毕
	## 更新聊天缓存列表(直接加入新获取消息)
	player_chat_messages.append(msg)
	
	## 限制消息存储限制
	if player_chat_messages.size() > max_chat_num:
		player_chat_messages.pop_front()
		
#客户端自检测
func _detect_connect()				-> void:
	
	## 断连有两个表示
	## 1 是 multiplayer.multiplayer_peer == null
	## 2 是 multiplayer.multiplayer_peer is OfflineMultiplayerPeer
	## 检测联机节点
	## 断连自动清除玩家列表
	if multiplayer.multiplayer_peer == null:
		if player_list != []:
			player_list.clear()
			player_position_dic.clear()
			player_chat_messages.clear()
		return
	
	## 这里检测对等体是否为空或者本地离线对等体
	## 检查断线后清除所有同步数据
	if multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
		if player_list != []:
			player_list.clear()
			player_position_dic.clear()
			player_chat_messages.clear()
		return
	
	## 这里是使用Godot内置API获得对等体连接状态
	## 内置状态为断连同样清理一次
	var state = multiplayer.multiplayer_peer.get_connection_status()
	
	#检查断连
	if state == multiplayer.multiplayer_peer.CONNECTION_DISCONNECTED:
		if player_list != []:
			player_list.clear()
			player_position_dic.clear()
			player_chat_messages.clear()

       3. 根管理器代码开放API代码补充

这里补充 Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客 中根管理器对游戏运行时管理器的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:
	#这里接收所有子管理器信号
	_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)

3. 实际演示

        1. 玩家聊天栏

        2. 玩家开始游戏,位置线性同步,以及相机自动寻找对应玩家

4. 补充

        本篇文章是 Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客 中网络系统的补充,添加了游戏运行时管理器,具体 聊天,移动,相机 的自动对应系统实现将放于主文章中

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

返回主文章

Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客

Logo

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

更多推荐