前言: 本文是 Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客 中网络代码架构的具体实现之一: 玩家位置同步

        本文将会使用主文章中提供的开放的API来实现玩家位置同步在阅读本文章之前,请先 点开主文章并对照其中API阅读,即使下文会将使用的API列出

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

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

本文在解释玩家位置信息同步的时候,代码中含有同步杂项信息同步的代码,不是本文重点,但思路一致,所以会标注,仅重点解释玩家位置同步

需要注意的是,若是比较安全的移动方式,应完全将逻辑处理交予主机端,而客户端仅负责传输操作数据,同时本框架是Host为主,主机为伪主服务器,同时也是客户端

Godot网络 ID 为 int类型,处理层统一转为字符串作为字典键,防止数值类型溢出隐患,保证多客户端字典匹配稳定。

解释时拆解成临散代码,文章最后将会附上完整的玩家代码。

本文使用工具类的参考代码:

extends Node

# 通用工具类
class_name NormalUtil

# 输入参量与服务器的信息
static func player_server_information_refresh(
	refresh_dic: 			Dictionary		= {}, 
	refresh_position_dic: 	Dictionary		= {},
	refresh_player_list:	Array			= []	
) -> void:
	
	## 清空后重新同步
	## 注意这里是临时清空,可能会有外部突然获取的危险
	refresh_dic.clear()
	refresh_position_dic.clear()
	refresh_player_list.clear()
	
	## 同步服务器数值
	## Godot中 (字典.merge) 为检查追加,只会替换已有值和追加没有值,所以需要上面清空
	## Godot中 (数组.append_array) 为检查追加,只会替换已有值和追加没有值,所以需要上面清空
	refresh_dic.merge(Server.get_player_information())
	refresh_position_dic.merge(Server.get_player_position_dic())
	refresh_player_list.append_array(Server.get_player_list())

1. 实现机制与解释

        本服务器AutoLoad命名为 Server

        本架构中使用Godot的网络互联,网络之间广播会同时运行 同结构同节点下同脚本 同样带 @rpc 关键字的函数,所以玩家之间传输数据的数据原始形态是 在多个客户端之间同步同一个位置字典

主文章的架构中提供了以下API

# 同步玩家位置信息
func sync_position(send_position: Vector2)        -> void:
	GameRunning.sync_with_player_position_dic(send_position)

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

# 更新主机玩家位置
func change_main_player_position(main_position: Vector2)		-> void:
	GameRunning.server_position = main_position

# 得到玩家网络ID
func get_player_id() -> int:
	return multiplayer.get_unique_id()

这些API是同步玩家位置要使用的核心API,具体实现请参考主文章

实现玩家位置控制与同步,核心要点为

1. 使用玩家识别 ID 或者其他标识方法确定本地移动的玩家实体

2.  使用服务器 API 客户端发送本地位置到主机端口

3. 主机端口受到客户端位置更新数据,向网络中需要发送的客户端发送更新信息

4. 其他客户端受到信息,在本地端使用玩家ID或其他表示符更新对应玩家位置

注意事项:

玩家之间网络传输存在丢包,即网络数据偶尔发送失败,所以客户端更新的时候可能会出现瞬移,卡顿,颤抖的问题。这里使用客户端渲染最新玩家网络位置的方式为线性靠近最新位置,防止抖动

2. 玩家位置同步实现

        1. 玩家标识符控制本地移动与其他玩家移动

本地端玩家之间输入移动按键移动,其他网络玩家同步服务器所有玩家字典地址,通过玩家ID标识符同步网络地址到本地

代码实现(移动部分)

extends CharacterBody2D

@export var move_speed: 		float 		= 500	# 玩家移动速度
@export var player_move_lerp: 	float     	= 6		# 网络玩家移动平滑值

var player_information: Dictionary		= {}		# 玩家本地杂项数据
var player_position_group: Dictionary	= {}		# 玩家本地服务器玩家位置
var _player_id:			String			= ""		# 玩家标识符ID

func _physics_process(delta: float) -> void:
	#玩家移动主循环
    _sync_information()						# 向主机端发送更新后的数据
	_player_move(delta)						# 本地玩家移动
	_other_player_sync(delta)				# 网络玩家移动同步
	_server_main_player_bind()				# 服务端玩家自己更新自己位置
    _update_server_information_to_loacl()	# 将服务器数据取到本地

#控制端玩家移动	
func _player_move(delta: float) -> void:
	## 玩家移动输入(只允许对应ID玩家控制自己的玩家)
	## 判断玩家ID是否是本地ID
	if	_player_id != str(Server.get_player_id()):
		return 
		
	# 这里就是正常的本地玩家移动	 
	var move_dir = Input.get_vector("left", "right", "up", "down")
	move_dir = move_dir.normalized()
	
	velocity.x = move_dir.x * move_speed
	velocity.y = move_dir.y * move_speed

## 其他网络玩家移动
## 注意这里服务器
func _other_player_sync(delta: float) 		-> void:
		## 平滑插值其他网络玩家移动防抖动
		## 从取到位置字典中获得对应网络玩家的位置
		## 这个脚本由于是通用的,不要判断主机跳过
		## 因为这样会导致其他网络玩家不同步主机位置
		var target_position = player_position_group.get(_player_id, global_position)
		global_position = lerp(global_position, target_position, player_move_lerp * delta)

## Godot对等体玩家列表默认不带本地端
## 对于主机而言,是默认不带主机端,所以要主机自己发送信息
## 更新主机玩家信息
func _server_main_player_bind()				-> void:
    # 判断主机
	if _player_id == "1":
		Server.change_main_player_position(global_position)			# 调用API更改主机位置

# 刷新服务器数据到本地
func _update_server_information_to_loacl() -> void:
	# 临时取出服务器数据
	var player_all_information:		Dictionary		# 这是收到玩家杂项信息的
	var player_all_position: 		Dictionary		# 这是收到服务器玩家位置的
	# 使用工具类刷新临时服务器数据
	NormalUtil.player_server_information_refresh(player_all_information, player_all_position)
	
	## 将临时类数据转换为本地数据
	## 这里用has的原因是因为服务器玩家杂项数据信息的格式是 
	## { "player_id ": 字典 }
	## 此外这里不删除杂项信息的存储主要是因为此工具类第一个变量是传输服务器杂项信息的
	## 当然也可以之间用 {} 替代
	if player_all_information.has(_player_id):
		player_information = player_all_information[_player_id]
		
	# 获得网络玩家位置数据
	player_position_group = player_all_position

# 向服务器发送当前玩家信息
func _sync_information()					-> void:
	if _player_id == str(Server.get_player_id()):
		Server.sync_position(global_position)			# 向服务器发送当前玩家位置
		Server.sync_infomation(player_information)		# 向服务器发送当前玩家杂项数据

        2. 玩家标识符控制玩家杂项数据传输

玩家主要负责的是杂项数据的发送,大部分杂项数据接收使用实际是比如血条等等一类需要玩家杂项数据的节点或者场景接收

这里给出发送和接收的示例代码

# 完整主循环函数
func _physics_process(delta: float) -> void:
	#玩家移动主循环
	_sync_information()						# 向主机端发送更新后的数据
	_player_move(delta)						# 本地玩家移动
	_other_player_sync(delta)				# 网络玩家移动同步
	_server_main_player_bind()				# 服务端玩家自己更新自己位置
	_update_player_information()			# 将本地杂项数据由客户端自己更新
	_update_server_information_to_loacl()	# 将服务器数据取到本地
	move_and_slide()

新增同步杂项数据的实现函数

# (更新杂项数据传输)
# 向服务器发送当前玩家信息
func _sync_information()					-> void:
	if _player_id == str(Server.get_player_id()):
		Server.sync_position(global_position)			# 向服务器发送当前玩家位置
		Server.sync_infomation(player_information)		# 向服务器发送当前玩家杂项数据

# (更新杂项数据传输)
# 更新玩家当前信息状态
func _update_player_information() -> void:
	player_information["player_hp"] 		= player_hp
	player_information["player_max_hp"]		= player_max_hp
	player_information["player_scale_hp"]	= player_hp / player_max_hp

杂项数据传输的原理和位置传输的原理一致,就是多了客户端自己维护传输数据的部分(字典)

        3. 完整玩家代码

这里是两部分合在一起完整的代码

extends CharacterBody2D

@export var player_max_hp: 		float		= 100
@export var player_hp:  		float		= 100
@export var move_speed: 		float 		= 500	# 玩家移动速度
@export var player_move_lerp: 	float     	= 6		# 网络玩家移动平滑值

var player_information: Dictionary		= {}		# 玩家本地杂项数据
var player_position_group: Dictionary	= {}		# 玩家本地服务器玩家位置
var _player_id:			String			= ""		# 玩家标识符ID

func _physics_process(delta: float) -> void:
	#玩家移动主循环
	_sync_information()						# 向主机端发送更新后的数据
	_player_move(delta)						# 本地玩家移动
	_other_player_sync(delta)				# 网络玩家移动同步
	_server_main_player_bind()				# 服务端玩家自己更新自己位置
	_update_player_information()			# 将本地杂项数据由客户端自己更新
	_update_server_information_to_loacl()	# 将服务器数据取到本地
	move_and_slide()
	
#控制端玩家移动	
func _player_move(delta: float) -> void:
	## 玩家移动输入(只允许对应ID玩家控制自己的玩家)
	## 判断玩家ID是否是本地ID
	if	_player_id != str(Server.get_player_id()):
		return 
		
	# 这里就是正常的本地玩家移动	 
	var move_dir = Input.get_vector("left", "right", "up", "down")
	move_dir = move_dir.normalized()
	
	velocity.x = move_dir.x * move_speed
	velocity.y = move_dir.y * move_speed


# 向服务器发送当前玩家信息
func _sync_information()					-> void:
	if _player_id == str(Server.get_player_id()):
		Server.sync_position(global_position)			# 向服务器发送当前玩家位置
		Server.sync_infomation(player_information)		# 向服务器发送当前玩家杂项数据
		
## 其他网络玩家移动
## 注意这里服务器
func _other_player_sync(delta: float) 		-> void:
		## 平滑插值其他网络玩家移动防抖动
		## 从取到位置字典中获得对应网络玩家的位置
		## 这个脚本由于是通用的,不要判断主机跳过
		## 因为这样会导致其他网络玩家不同步主机位置
		var target_position = player_position_group.get(_player_id, global_position)
		global_position = lerp(global_position, target_position, player_move_lerp * delta)

## Godot对等体玩家列表默认不带本地端
## 对于主机而言,是默认不带主机端,所以要主机自己发送信息
## 更新主机玩家信息
func _server_main_player_bind()				-> void:
	if _player_id == "1":
		Server.change_main_player_position(global_position)			# 调用API更改主机位置
		Server.change_main_player_infomation(player_information)
	
# 更新玩家当前信息状态
func _update_player_information() -> void:
	player_information["player_hp"] 		= player_hp
	player_information["player_max_hp"]		= player_max_hp
	player_information["player_scale_hp"]	= player_hp / player_max_hp

# 刷新服务器数据到本地
func _update_server_information_to_loacl() -> void:
	# 临时取出服务器数据
	var player_all_information:		Dictionary		# 这是收到玩家杂项信息的
	var player_all_position: 		Dictionary		# 这是收到服务器玩家位置的
	# 使用工具类刷新临时服务器数据
	NormalUtil.player_server_information_refresh(player_all_information, player_all_position)
	
	## 将临时类数据转换为本地数据
	## 这里用has的原因是因为服务器玩家杂项数据信息的格式是 
	## { "player_id ": 字典 }
	## 此外这里不删除杂项信息的存储主要是因为此工具类第一个变量是传输服务器杂项信息的
	## 当然也可以之间用 {} 替代
	if player_all_information.has(_player_id):
		player_information = player_all_information[_player_id]
		
	# 获得网络玩家位置数据
	player_position_group = player_all_position

# 得到玩家ID
func get_player_id() -> String:
	return _player_id

# 改变玩家ID
func change_player_id(player_id: String) -> void:
	_player_id = player_id

# 得到玩家当前血量
func get_player_current_hp()				-> float:
	return player_hp

3. 运行演示

可以看到玩家位置同步

4. 补充

        本文章是 Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客 中实现部分的补充

这里返回主文章 Godot(4.X): 全局AutoLoad网络管理中心实现-CSDN博客

Logo

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

更多推荐