Godot游戏练习01-第4节-多人控制与玩家位置同步(翻车)

0 阅读4分钟

昨天我们使用MultiplayerSpawner实现了所有peer上的玩家生成, 但是最后碰到两个问题, 今天来解决昨天的问题, 并实现玩家的分开控制与位置同步

玩家生成在一起后弹开的问题

昨天其实已经指出原因了 -- Player场景上的碰撞体发生了碰撞

我们在前期先不处理碰撞, 暂时关闭碰撞, 具体的操作方式如下

在Player场景中的CharacterBody2D节点中的Collision部分, 取消Mask层的默认选中

Layer 的含义是指碰撞体在哪些层, 可以触发其他检测对应层的碰撞体的碰撞事件

Mask 的含义是当前碰撞体检测哪些层的碰撞, 当其他碰撞体的Layer在当前碰撞体的Mask中, 那么其他碰撞体就可以触发当前碰撞体的碰撞事件

我们去掉了Mask层的默认值1, 也就是Player的碰撞体不再检测任何层的碰撞事件, 但是它本身还在第1层(Layer), 如果有其他CollisionObject检测1层的碰撞, 则可以被Player触发

玩家控制

昨天的第二个问题是, 在任意一个窗口中控制玩家移动时, 会同时移动所有的玩家, 控制没有区分

让我们新建一个PlayerInputComponent场景类, 只有一个MultiplayerSychronizer根节点, 用于同步authority对等端(peer)的输入到其他所有对等端

挂载脚本内容如下

class_name PlayerInputMultiplayerSynchronizerComponent
extends MultiplayerSynchronizer

var move_vector : Vector2 = Vector2.ZERO

func _process(_delta: float) -> void:
	if is_multiplayer_authority():
		move_vector = Input.get_vector("move_left", "move_right", "move_up", "move_down")

配置root_path属性为根节点(同步属性的相对参考节点), 在需要同步的属性(replication)中添加脚本中的move_vector变量

在Player场景下添加该Component节点(场景实例化), 获取引用, 在ready回调中设置组件节点的authority, 在process中使用输入组件提供的move_vector

注意: 这里仅设置了Player场景中的输入组件(PlayerInputMultiplayerSychronizer)的authority, Player本身未设置, 未设置的节点默认authority都是1, 也就是默认节点都由服务器管理

脚本中的input_peer_id由外部输入, 我们在main.gd中生成Player时处理 player.input_peer_id = data.peer_id

func _ready() -> void:
	multiplayer_spawner.spawn_function = func(data: Dictionary):
		var player = PLAYER.instantiate()
		player.name = "Player%s" % [data.peer_id]
		player.input_peer_id = data.peer_id
		return player
	_create_player.rpc_id(1)


@rpc("any_peer", "call_local", "reliable")
func _create_player() -> void:
	var sender_id := multiplayer.get_remote_sender_id()
	multiplayer_spawner.spawn({ "peer_id" : sender_id })

注意: 对节点设置调用set_multiplayer_authority时, 对应的节点authority信息不会自动同步到其余peer上, 想要所有peer保持相同的authority设置, 必须开发者自己保证, 这里我们在spawn_function中设置, 而spawn_function会在所有peer上自动调用, 保证了同步

以下为set_multiplayer_authority函数的官方文档说明

Warning: This does not automatically replicate the new authority to other peers. It is the developer's responsibility to do so. You may replicate the new authority's information using MultiplayerSpawner.spawn_function, an RPC, or a MultiplayerSynchronizer. Furthermore, the parent's authority does not propagate to newly added children.

玩家位置同步

这里我们只做使用MultiplayerSychronizer做简单的位置同步, 暂时不考虑插值/预测等概念

在Player场景中再新增一个MultiplayerSychronizer节点, 添加global_position作为同步属性

看看效果

我们暂时在Debug中开启3个实例, 看看同步效果

不开不知道, 一开吓一跳, 当场翻车了!

调试时又碰到一个问题, 在Godot4.6.1版本中, 提示不应该在ready回调中改变节点的authority值, 应该在enter_tree回调中设置

这是教程之外的问题, 这里有些纠结的点, 在enter_tree回调中, 我们的@onready还未生效, 也就是还拿不到输入组件的引用, 我们试试enter_tree中动态创建输入组件

var player_input_multiplayer_synchronizer_component: PlayerInputMultiplayerSynchronizerComponent

func _enter_tree() -> void:
	player_input_multiplayer_synchronizer_component = PlayerInputMultiplayerSynchronizerComponent.new()
	player_input_multiplayer_synchronizer_component.set_multiplayer_authority(input_peer_id)
	player_input_multiplayer_synchronizer_component.name = "PlayerInputMultiplayerSynchronizer"
	add_child(player_input_multiplayer_synchronizer_component)

但是紧接着, 我又发现一个问题...

当客户端peer加入服务器后, 报错数字狂飙, 一分钟就飙到10000+了, 压迫力拉满

我又确认了一遍, 按照教程实现的没错, 但是效果不一样, 可能Godot版本升级后, 有一些特性发生了变化

排查之后发现, 当客户端peer加入服务器后, 服务器之前生成的服务器的Player未被同步生成到客户端peer中

我们在spawn_function中添加打印, 并调试

[peer 1] Spawn player: 1
[peer 1] Spawn player: 1892877996
[peer 1892877996] Spawn player: 1892877996
--- Debugging process stopped ---

第一行: 第一个实例点击host, 创建服务器, 并创建了自己的Player实例(peer_id为1)

第二行: 客户端peer加入服务器, 在服务端创建了属于客户端peer的Player实例(peer_id为1892877996)

第三行: 客户端peer加入服务器后, 在客户端创建了属于客户端peer的Player实例(peer_id为1892877996)

然后就没了! 客户端只有一个Player实例, 缺少属于服务端的Player实例!

真是翻车的一天, 但是今天太晚了, 幸好我们找出了问题, 明天来解决这个bug!