在之前的章节中我们实现了敌人与玩家的相互攻击, 碰撞判断, 但是一直未正式处理玩家的死亡事件, 也没有判定游戏失败, 今天先实现机制
看看效果
- 玩家在多次受伤后死亡
- 所有玩家死亡后游戏结束, 目前仅回到主菜单
- 游戏可以再次正常开始

实现过程
玩家死亡
我们之前添加过Player的HurtboxComponent组件, 在生命值耗尽时仅打印信息
现在在生命值耗尽后添加queue_free(), 简单处理Player死亡信号, 死亡就释放节点
但是在添加简单的处理后, 实际运行时发生了报错, 当Client的Player在移动时死亡, 非常容易复现
gdscript
E 0:00:28:334 get_node: Node not found: "Main/YSortRoot/Player219449027/PlayerInputMultiplayerSynchronizerComponent" (relative to "/root").
<C++ Error> Method/function failed. Returning: nullptr
<C++ Source> scene/main/node.cpp:1963 @ get_node()
报错如下:
- Client的Player在移动中, Player节点的authority为1(服务器), 而Player节点下的InputComponent的authority为Client本身
- 数据的同步方向是从authority同步到其他peer, Client的Player移动时就从其InputComponent一直同步到服务器和其他peer
- 但所有Player节点的authority属于服务器, 也只有服务器在检测物理事件, 当服务器检测到Client的Player死亡时, 执行了
queue_free() - 服务器上的Player节点先释放, 之后通过网络同步释放Client上的Player节点, 这中间有网络延时
- 在该延时期间, Client上的InputComponent组件还在尝试向Server同步输入信息, 而Server上的对应节点已经不存在了, 出现报错
该问题暂时标记为问题1, 下次处理
玩家重生
首先在main.gd记录死亡的玩家
gdscript
var died_peers: Array[int] = []
# other codes...
func _ready() -> void:
multiplayer_spawner.spawn_function = func(data):
print("[peer %s] Spawn player: %s, pos: %s" % [multiplayer.get_unique_id(), data.peer_id, player_spawn_marker.global_position])
var player = PLAYER.instantiate() as Player
player.name = "Player%s" % [data.peer_id]
player.input_peer_id = data.peer_id
player.global_position = player_spawn_marker.global_position
if is_multiplayer_authority():
player.died.connect(_on_player_died.bind(data.peer_id))
return player
_peer_ready.rpc_id(1)
if is_multiplayer_authority():
enemy_spawn_component.round_completed.connect(_on_round_completed)
else:
multiplayer.server_disconnected.connect(_on_server_disconnected)
# other codes...
func _on_player_died(peer_id: int) -> void:
died_peers.append(peer_id)
_check_game_over()
player新增died信号, 在死亡时触发, main中创建player时监听
然后在EnemySpawnComponent组件中检测round完成时发出round_completed信号, 依旧在main中监听
之后直接调用spawn函数处理所有死亡的玩家, 即可实现新一轮游戏中所有玩家复活的效果
gdscript
func _on_round_completed() -> void:
for peer_id in died_peers:
multiplayer_spawner.spawn({ "peer_id": peer_id })
died_peers.clear()
游戏失败检测
当所有Player都死亡, 则判定为失败, 在每一个Player的死亡信号触发时进行一次检测
若游戏失败, 则关闭multiplayer_peer, 并回到主菜单 (注意: 这是服务器的逻辑)
gdscript
func _end_game() -> void:
multiplayer.multiplayer_peer = null
get_tree().change_scene_to_file("res://ui/menu/main_menu.tscn")
func _check_game_over() -> void:
# multiplayer.get_peers 返回所有已连接的peer,不包含自身
var all_peers := multiplayer.get_peers()
all_peers.append(multiplayer.get_unique_id())
var is_game_over := true
for peer_id in all_peers:
if not died_peers.has(peer_id):
is_game_over = false
break
if is_game_over:
_end_game()
服务器关闭multiplayer_peer, 实际上会导致已连接的peer收到server_disconnected信号
进行监听和处理即可
在上面的_ready中已经展现, 当Client收到该信号时也调用_end_game, 关闭peer并切换场景
gdscript
multiplayer.server_disconnected.connect(_on_server_disconnected)
# other codes...
func _on_server_disconnected() -> void:
_end_game()
保存, 运行, 效果可以, 但是又出现报错
gdscript
E 0:00:18:010 main.gd:40 @ _end_game(): Removing a CollisionObject node during a physics callback is not allowed and will cause undesired behavior. Remove with call_deferred() instead.
<C++ Source> scene/2d/physics/collision_object_2d.cpp:98 @ _notification()
<Stack Trace> main.gd:40 @ _end_game()
main.gd:53 @ _check_game_over()
main.gd:58 @ _on_player_died()
player.gd:69 @ _on_health_depleted()
health_component.gd:15 @ take_damage()
hurtbox_component.gd:15 @ take_damage()
hurtbox_component.gd:22 @ _on_area_entered()
通过调用栈和报错信息可知, 该错误是物理回调(area_entered)中经过一系列判断触发了_end_game切换场景移除了碰撞体导致
也就是说: Godot中不允许在物理回调中移除碰撞体 , 报错信息中推荐使用call_deferred处理
这里记为问题2, 下次处理