昨天我们探究了多人游戏中的peer_connected信号触发规则, 今天继续开发, 包括场景的生成, 每个peer的Player同步生成
场景的切换
先预加载main场景
const MAIN : PackedScene = preload("uid://yubvfldj7w73")
新版本的Godot更推荐使用uid来表述资源, 按住Ctrl将场景拖入脚本中, 将自动生成上述语句
在ready中我们将对MultiplayerAPI的peer_connected事件监听修改为connected_to_server事件监听, 这个事件仅在Client成功连接到服务器后触发, 仅在客户端peer上触发
multiplayer.connected_to_server.connect(_on_connected_to_server)
当客户端成功连接服务器后, 加载main场景
func _on_connected_to_server() -> void:
get_tree().change_scene_to_packed(MAIN)
我们的服务器不是单纯服务器, 它是Host服务器, 也就是它既充当一个玩家的peer也充当服务器, 它也需要加载main场景, 服务器不用等待, 在创建服务成功后就可以加载
func _on_host_pressed() -> void:
var server_peer := ENetMultiplayerPeer.new()
server_peer.create_server(PORT)
multiplayer.multiplayer_peer = server_peer
get_tree().change_scene_to_packed(MAIN)
各peer的Player同步
当peer连接到服务器后, 所有peer都已经加载了main场景, 此时需要在所有peer上创建对应的Player, 在多人游戏中我们使用MultiplayerSpawner在所有peer上同步生成节点
删除main.tscn场景中的Player子节点, 添加一个MultiplayerSpawner子节点, 将其SpawnPath设置为Main节点

当一个Client加入服务器后, 在同步生成Player节点之前, 需要确保Client的场景加载完毕, 因此我们在ready之后才调用_create_player, 在服务器上调用MultiplayerSpawner进行Player的同步生成
extends Node2D
const PLAYER = preload("uid://dgstmloeo60yy")
@onready var multiplayer_spawner: MultiplayerSpawner = $MultiplayerSpawner
func _ready() -> void:
multiplayer_spawner.spawn_function = func(data: Dictionary):
var player = PLAYER.instantiate()
player.name = "Player%s" % [data.player_id]
return player
_create_player.rpc_id(1)
@rpc("any_peer", "call_local", "reliable")
func _create_player() -> void:
var id := multiplayer.get_remote_sender_id()
multiplayer_spawner.spawn({ "player_id" : id })
这里有几个点需要注意:
-
MultiplayerAPI的
spawn_function指定之后, 当authority(节点的拥有者, 这里就是服务端)调用MultiplayerAPI的spawn函数之后,spawn_function会在所有的peer上对等调用,spawn函数的传入参数会被当做spawn_function调用时的传入参数**
spawn**仅在authority上调用一次, **spawn_function**会自动在所有peer上调用所以我们用
_create_player.rpc_id(1)这种RPC远程调用的方式, 让创建player的逻辑仅在服务端(authority)运行在Godot编辑器中, 按住Ctrl点击对应的成员, 会直接跳转到详细的文档, 可以很方便查看比如
multiplayer,spawn等成员的具体说明 -
Godot节点上的rpc调用要求所有peer上对应节点的路径必须相同, 因此我们在创建player对象时, 都显式指定了player节点的名称
-
@rpc的三个参数含义需要弄明白, 特别是call_local, 因为host服务器自己ready之后也需要调用_create_player创建自身的player节点 -
spawn_function需要返回一个没有加入任何场景树的节点, 需要小心Method called on all peers when a custom spawn() is requested by the authority. Will receive the data parameter, and should return a Node that is not in the scene tree.
Note: The returned node should not be added to the scene with Node.add_child(). This is done automatically.
客户端加入后的问题
为方便调试, 我们调整Debug中的多实例数量为2
运行起来之后, 仅启动服务器时, 一切正常, 玩家正常创建, 可以正常移动
但是客户端加入后, 发现玩家对象"闪现"一下之后消失了
并且如果服务器启动时不移动玩家, 客户端加入后, 两个窗口的玩家都会闪现一下消失

通过Remote调试, 查看场景树, Player节点都还在, 但是位置都跑了很远
原来是碰撞生效了, 在第二个玩家生成的那一刻, 两个玩家碰撞盒叠在一起, 被一下子排斥开来, 飞出屏幕了, 设置一下碰撞层级就可以正常运行啦
紧接着又发现一个问题! 好像只有一个玩家, 但仔细查看Remote场景树, 发现其实是两个玩家叠一起了, 按下移动键时, 两者一起移动了, 看起来好像只有一个, 我们下次来解决该问题