昨天我们使用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!