| name | godot-online-multiplayer |
| description | Godot 4 多人游戏核心模式,包括场景管理、玩家生命周期、死亡复活机制。 |
Godot Online Multiplayer
Godot 4 多人游戏核心模式,涵盖场景管理、玩家生命周期、死亡复活等机制。
When to Use This Skill
- 构建多人在线游戏
- 实现玩家生成与销毁
- 处理多人游戏中的死亡与复活
- 管理游戏流程状态机
- 同步玩家数据
Core Concepts
多人游戏架构
MultiplayerHost (服务器/主机)
├── MultiplayerSpawner (自动生成节点)
├── MultiplayerSynchronizer (状态同步)
└── PlayerManager (玩家管理)
MultiplayerPeers (客户端/对等端)
├── NetworkedMultiplayerENet (网络连接)
└── Player instances (同步的玩家)
1. 场景结构
主游戏场景
# main.tscn 场景结构
# Main (Node)
# ├── GameManager (Autoload)
# ├── MultiplayerSpawner (自动生成玩家)
# ├── World (Node2D)
# │ ├── SpawnPoints (Node)
# │ └── Enemies (Node)
# └── UI (CanvasLayer)
玩家场景
# player.tscn 场景结构
# Player (CharacterBody2D)
# ├── Sprite2D
# ├── CollisionShape2D
# ├── HealthComponent
# ├── HitboxComponent
# ├── NameLabel (Label)
# └── HealthBar (ProgressBar)
2. MultiplayerSpawner 使用
# PlayerSpawner.gd
class_name PlayerSpawner
extends MultiplayerSpawner
## 自动生成玩家节点
## 当对等端连接时自动生成玩家实例
signal player_spawned(player: Node, peer_id: int)
signal player_despawned(player: Node, peer_id: int)
@export var player_scene: PackedScene
func _ready() -> void:
# 设置生成路径
spawn_path = ^"/root/Main/Players"
# 连接信号
spawned.connect(_on_spawned)
despawned.connect(_on_despawned)
func _on_spawned(node: Node) -> void:
var peer_id := multiplayer.get_remote_sender_id()
print("玩家生成: %s (Peer ID: %d)" % [node.name, peer_id])
player_spawned.emit(node, peer_id)
func _on_despawned(node: Node) -> void:
var peer_id := multiplayer.get_remote_sender_id()
print("玩家销毁: %s (Peer ID: %d)" % [node.name, peer_id])
player_despawned.emit(node, peer_id)
3. 生成函数
# spawn_manager.gd
class_name SpawnManager
extends Node
## 管理玩家生成逻辑
@export var player_scene: PackedScene
@export var spawn_points: Array[Marker2D] = []
var _spawn_points_container: Node2D
func _ready() -> void:
_spawn_points_container = $SpawnPoints
func get_spawn_position(peer_id: int) -> Vector2:
"""根据peer_id获取生成位置"""
if spawn_points.is_empty():
return Vector2.ZERO
# 使用 peer_id 哈希选择生成点,实现均衡分布
var index := peer_id % spawn_points.size()
var spawn_point := spawn_points[index]
if spawn_point is Marker2D:
return spawn_point.global_position
return spawn_point.global_position if spawn_point.has_node(".") else Vector2.ZERO
func spawn_player(peer_id: int, parent: Node) -> Node:
"""生成玩家节点"""
var player := player_scene.instantiate()
# 设置玩家名称为 peer_id
player.name = str(peer_id)
# 设置初始位置
player.global_position = get_spawn_position(peer_id)
# 配置玩家
if player.has_method("setup"):
player.setup(peer_id)
parent.add_child(player)
# 只在服务器上设置网络主人
if multiplayer.is_server():
player.set_multiplayer_authority(peer_id)
return player
func despawn_player(peer_id: int, parent: Node) -> void:
"""销毁玩家节点"""
var player := parent.get_node_or_null(str(peer_id))
if player:
player.queue_free()
4. 玩家加入流程
# game_manager.gd (Autoload)
extends Node
signal player_joined(peer_id: int)
signal player_left(peer_id: int)
signal game_started
signal game_ended(winner_id: int)
enum GameState { WAITING, PLAYING, GAME_OVER }
var game_state: GameState = GameState.WAITING
var players: Dictionary = {} # peer_id -> PlayerData
var _min_players: int = 1
var _max_players: int = 4
func _ready() -> void:
# 设置网络回调
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
# 如果是服务器,启动游戏循环
if multiplayer.is_server():
_check_start_game()
func _on_peer_connected(peer_id: int) -> void:
print("玩家连接: %d" % peer_id)
# 创建玩家数据
var player_data := PlayerData.new()
player_data.peer_id = peer_id
player_data.name = "Player_%d" % peer_id
player_data.is_alive = true
player_data.score = 0
players[peer_id] = player_data
# 通知所有客户端有新玩家加入
rpc("_sync_player_joined", peer_id, player_data.to_dict())
player_joined.emit(peer_id)
# 检查是否可以开始游戏
_check_start_game()
func _on_peer_disconnected(peer_id: int) -> void:
print("玩家断开: %d" % peer_id)
# 清理玩家数据
var player_data := players.get(peer_id)
if player_data:
player_data.free()
players.erase(peer_id)
# 通知所有客户端
rpc("_sync_player_left", peer_id)
player_left.emit(peer_id)
# 检查游戏是否结束
if game_state == GameState.PLAYING and players.size() < 2:
_end_game(0) # 剩余玩家获胜
@rpc(call_local = true)
func _sync_player_joined(peer_id: int, data: Dictionary) -> void:
if not players.has(peer_id):
var player_data := PlayerData.new()
player_data.from_dict(data)
players[peer_id] = player_data
@rpc(call_local = true)
func _sync_player_left(peer_id: int) -> void:
var player_data := players.get(peer_id)
if player_data:
player_data.free()
players.erase(peer_id)
func _check_start_game() -> void:
if game_state == GameState.WAITING and players.size() >= _min_players:
start_game()
func start_game() -> void:
game_state = GameState.PLAYING
game_started.emit()
# 重置所有玩家状态
for peer_id in players.keys():
var data := players[peer_id]
data.is_alive = true
data.lives = 3
data.score = 0
rpc("_sync_game_state", GameState.PLAYING)
@rpc(call_local = true)
func _sync_game_state(state: GameState) -> void:
game_state = state
func _end_game(winner_id: int) -> void:
game_state = GameState.GAME_OVER
game_ended.emit(winner_id)
rpc("_sync_game_state", GameState.GAME_OVER)
rpc("_sync_winner", winner_id)
@rpc(call_local = true)
func _sync_winner(winner_id: int) -> void:
pass # UI显示胜利者
# PlayerData.gd
class_name PlayerData
extends RefCounted
var peer_id: int
var name: String
var is_alive: bool
var lives: int = 3
var score: int = 0
func to_dict() -> Dictionary:
return {
"peer_id": peer_id,
"name": name,
"is_alive": is_alive,
"lives": lives,
"score": score
}
func from_dict(data: Dictionary) -> void:
peer_id = data.get("peer_id", 0)
name = data.get("name", "")
is_alive = data.get("is_alive", true)
lives = data.get("lives", 3)
score = data.get("score", 0)
5. 断开处理
# connection_manager.gd
class_name ConnectionManager
extends Node
signal connection_failed
signal connection_succeeded
signal server_disconnected
const DEFAULT_PORT := 8765
const MAX_PLAYERS := 8
func _ready() -> void:
# 显示主菜单UI
pass
func create_server(port: int = DEFAULT_PORT) -> bool:
"""创建服务器"""
var peer := ENetMultiplayerPeer.new()
var error := peer.create_server(port, MAX_PLAYERS)
if error != OK:
push_error("无法创建服务器: %s" % error)
return false
multiplayer.multiplayer_peer = peer
# 连接信号
peer.peer_connected.connect(_on_peer_connected)
peer.peer_disconnected.connect(_on_peer_disconnected)
print("服务器创建成功,端口: %d" % port)
connection_succeeded.emit()
return true
func join_server(ip: String, port: int = DEFAULT_PORT) -> bool:
"""加入服务器"""
var peer := ENetMultiplayerPeer.new()
var error := peer.create_client(ip, port)
if error != OK:
push_error("无法连接服务器: %s" % error)
connection_failed.emit()
return false
multiplayer.multiplayer_peer = peer
# 连接信号
peer.connection_succeeded.connect(_on_connection_succeeded)
peer.connection_failed.connect(_on_connection_failed)
peer.server_disconnected.connect(_on_server_disconnected)
print("正在连接 %s:%d..." % [ip, port])
return true
func _on_peer_connected(peer_id: int) -> void:
print("对等端连接: %d" % peer_id)
func _on_peer_disconnected(peer_id: int) -> void:
print("对等端断开: %d" % peer_id)
# GameManager 处理玩家离开逻辑
func _on_connection_succeeded() -> void:
print("连接成功!")
connection_succeeded.emit()
func _on_connection_failed() -> void:
print("连接失败!")
connection_failed.emit()
func _on_server_disconnected() -> void:
print("服务器断开")
server_disconnected.emit()
# 返回主菜单
get_tree().change_scene_to_file("res://scenes/menu.tscn")
func disconnect_from_server() -> void:
if multiplayer.multiplayer_peer:
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = null
6. 死亡与复活
# player.gd
class_name Player
extends CharacterBody2D
signal died(peer_id: int, killer_id: int)
signal respawned(peer_id: int)
@export var respawn_time: float = 3.0
@export var invincibility_after_spawn: float = 2.0
var peer_id: int = 0
var is_alive: bool = true
var killer_id: int = 0
var _respawn_timer: Timer
var _invincibility_timer: Timer
var _health_component: HealthComponent
func _ready() -> void:
# 获取生命组件
_health_component = $HealthComponent
if _health_component:
_health_component.died.connect(_on_died)
# 创建计时器
_respawn_timer = Timer.new()
_respawn_timer.one_shot = true
_respawn_timer.timeout.connect(_on_respawn_timer_timeout)
add_child(_respawn_timer)
_invincibility_timer = Timer.new()
_invincibility_timer.one_shot = true
_invincibility_timer.timeout.connect(_on_invincibility_timeout)
add_child(_invincibility_timer)
func setup(p_id: int) -> void:
peer_id = p_id
name = str(p_id)
# 设置网络权限
set_multiplayer_authority(p_id)
# 在所有客户端上设置可见性
.rpc("_sync_setup", p_id)
@rpc
func _sync_setup(p_id: int) -> void:
peer_id = p_id
name = str(p_id)
func _on_died(source_peer_id: int) -> void:
if not is_alive:
return
is_alive = false
killer_id = source_peer_id
died.emit(peer_id, killer_id)
# 通知服务器
rpc("_notify_death", peer_id, source_peer_id)
@rpc(call_local = true)
func _notify_death(victim_id: int, killer_id: int) -> void:
died.emit(victim_id, killer_id)
# 服务器处理死亡逻辑
if multiplayer.is_server():
GameManager.on_player_died(victim_id, killer_id)
# 开始复活计时
_start_respawn_timer()
func _start_respawn_timer() -> void:
_respawn_timer.start(respawn_time)
func _on_respawn_timer_timeout() -> void:
if is_networked():
rpc("_request_respawn", peer_id)
else:
_do_respawn()
@rpc
func _request_respawn(p_id: int) -> void:
if multiplayer.is_server():
_do_respawn_for_player(p_id)
func _do_respawn_for_player(p_id: int) -> void:
# 服务器检查是否可以复活
var player_data := GameManager.players.get(p_id)
if player_data and player_data.lives > 0:
rpc("_perform_respawn", p_id)
@rpc(call_local = true)
func _perform_respawn(p_id: int) -> void:
if peer_id != p_id:
return
# 重置状态
is_alive = true
_health_component.current_health = _health_component.max_health
# 移动到生成点
global_position = SpawnManager.get_spawn_position(p_id)
# 无敌时间
_invincibility_timer.start(invincibility_after_spawn)
respawned.emit(peer_id)
func _on_invincibility_timeout() -> void:
# 恢复碰撞
pass
func take_damage(amount: int, source_peer_id: int) -> void:
if not is_alive:
return
if _health_component:
_health_component.take_damage(amount, get_node_or_null(str(source_peer_id)))
7. 游戏结束检查
# game_manager.gd 继续
func on_player_died(victim_id: int, killer_id: int) -> void:
"""处理玩家死亡"""
var victim_data := players.get(victim_id)
var killer_data := players.get(killer_id)
if not victim_data:
return
# 标记死亡
victim_data.is_alive = false
victim_data.lives -= 1
# 给击杀者加分
if killer_data and killer_id != victim_id:
killer_data.score += 100
rpc("_sync_player_scored", killer_id, killer_data.score)
# 检查游戏是否结束
_check_game_over()
# 检查是否需要回合复活
_check_round_respawn(victim_id)
func _check_game_over() -> void:
"""检查是否触发游戏结束"""
var alive_players := players.values().filter(func(p): return p.is_alive)
# 只剩一个玩家
if alive_players.size() <= 1 and game_state == GameState.PLAYING:
if alive_players.is_empty():
_end_game(0) # 平局
else:
_end_game(alive_players[0].peer_id)
func _check_round_respawn(victim_id: int) -> void:
"""检查是否进行回合复活"""
var player_data := players.get(victim_id)
if player_data and player_data.lives > 0:
# 有剩余生命值,准备复活
rpc("_schedule_round_respawn", victim_id, player_data.lives)
@rpc(call_local = true)
func _schedule_round_respawn(peer_id: int, remaining_lives: int) -> void:
# UI显示复活倒计时
pass
@rpc(call_local = true)
func _sync_player_scored(peer_id: int, new_score: int) -> void:
var player_data := players.get(peer_id)
if player_data:
player_data.score = new_score
8. 回合复活
# round_manager.gd
class_name RoundManager
extends Node
signal round_started(round_number: int)
signal round_ended(round_number: int, winner_id: int)
signal all_rounds_ended(final_winner_id: int)
@export var rounds_to_win: int = 3
@export var round_intermission_time: float = 3.0
var current_round: int = 1
var round_state: RoundState = RoundState.INTERMISSION
var player_wins: Dictionary = {} # peer_id -> wins
enum RoundState { INTERMISSION, FIGHTING, ROUND_END }
func _ready() -> void:
GameManager.game_started.connect(_on_game_started)
GameManager.game_ended.connect(_on_game_ended)
func _on_game_started() -> void:
current_round = 1
round_state = RoundState.INTERMISSION
player_wins.clear()
# 初始化所有玩家的胜利数
for peer_id in GameManager.players.keys():
player_wins[peer_id] = 0
_start_round_intermission()
func _start_round_intermission() -> void:
round_state = RoundState.INTERMISSION
rpc("_sync_round_state", RoundState.INTERMISSION, current_round)
# 3秒后开始回合
await get_tree().create_timer(round_intermission_time).timeout
_start_round()
func _start_round() -> void:
round_state = RoundState.FIGHTING
rpc("_sync_round_state", RoundState.FIGHTING, current_round)
round_started.emit(current_round)
# 重置所有玩家位置和状态
rpc("_reset_all_players_for_round")
@rpc(call_local = true)
func _sync_round_state(state: RoundState, round_num: int) -> void:
round_state = state
current_round = round_num
@rpc(call_local = true)
func _reset_all_players_for_round() -> void:
# 重置玩家状态用于新回合
for peer_id_str in GameManager.players.keys():
var peer_id := int(peer_id_str)
var player := get_tree().get_first_node_in_group("players").get_node_or_null(str(peer_id))
if player:
player.global_position = SpawnManager.get_spawn_position(peer_id)
player.is_alive = true
if player.has_method("reset_for_round"):
player.reset_for_round()
func on_player_died_in_round(victim_id: int, killer_id: int) -> void:
"""回合中玩家死亡"""
if round_state != RoundState.FIGHTING:
return
var alive_players := _get_alive_players()
if alive_players.size() <= 1:
_end_round(alive_players[0] if alive_players else 0)
func _get_alive_players() -> Array:
var alive: Array = []
for peer_id in GameManager.players.keys():
var data := GameManager.players[peer_id]
if data.is_alive:
alive.append(peer_id)
return alive
func _end_round(winner_id: int) -> void:
round_state = RoundState.ROUND_END
round_ended.emit(current_round, winner_id)
if winner_id != 0:
player_wins[winner_id] += 1
# 检查是否有人获胜
for peer_id in player_wins.keys():
if player_wins[peer_id] >= rounds_to_win:
_end_game(peer_id)
return
# 下一回合
current_round += 1
_start_round_intermission()
func _end_game(winner_id: int) -> void:
all_rounds_ended.emit(winner_id)
GameManager._end_game(winner_id)
func _on_game_ended(winner_id: int) -> void:
round_state = RoundState.ROUND_END
9. 场景切换
# scene_transition.gd
class_name SceneTransition
extends CanvasLayer
@export var transition_duration: float = 0.5
@export var fade_color: Color = Color.BLACK
var _transition_tween: Tween
var _color_rect: ColorRect
func _ready() -> void:
# 创建过渡用的 ColorRect
_color_rect = ColorRect.new()
_color_rect.color = Color.TRANSPARENT
_color_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(_color_rect)
func fade_to_black() -> void:
"""淡出到黑色"""
if _transition_tween:
_transition_tween.kill()
_transition_tween = create_tween()
_transition_tween.tween_property(_color_rect, "color", fade_color, transition_duration)
await _transition_tween.finished
func fade_from_black() -> void:
"""从黑色淡入"""
if _transition_tween:
_transition_tween.kill()
_color_rect.color = fade_color
_transition_tween = create_tween()
_transition_tween.tween_property(_color_rect, "color", Color.TRANSPARENT, transition_duration)
await _transition_tween.finished
func change_scene(scene_path: String) -> void:
"""带过渡的场景切换"""
await fade_to_black()
get_tree().change_scene_to_file(scene_path)
await fade_from_black()
# 使用示例
func _on_play_pressed() -> void:
var transition := $SceneTransition
await transition.fade_to_black()
get_tree().change_scene_to_file("res://scenes/game.tscn")
await transition.fade_from_black()
10. 游戏流程状态机
# game_state_machine.gd
class_name GameStateMachine
extends Node
## 游戏流程状态机
signal state_changed(from_state: GameStates, to_state: GameStates)
enum GameStates {
MAIN_MENU,
CONNECTING,
WAITING_FOR_PLAYERS,
LOADING,
PLAYING,
PAUSED,
ROUND_END,
GAME_OVER
}
@export var initial_state: GameStates = GameStates.MAIN_MENU
var current_state: GameStates
var previous_state: GameStates
func _ready() -> void:
current_state = initial_state
_enter_state(current_state)
func transition_to(new_state: GameStates, msg: Dictionary = {}) -> void:
if new_state == current_state:
return
previous_state = current_state
var old_state := current_state
current_state = new_state
_exit_state(old_state)
_enter_state(new_state, msg)
state_changed.emit(old_state, new_state)
func _enter_state(state: GameStates, msg: Dictionary = {}) -> void:
match state:
GameStates.MAIN_MENU:
_enter_main_menu()
GameStates.CONNECTING:
_enter_connecting()
GameStates.WAITING_FOR_PLAYERS:
_enter_waiting()
GameStates.LOADING:
_enter_loading()
GameStates.PLAYING:
_enter_playing()
GameStates.PAUSED:
_enter_paused()
GameStates.ROUND_END:
_enter_round_end()
GameStates.GAME_OVER:
_enter_game_over()
func _exit_state(state: GameStates) -> void:
match state:
GameStates.MAIN_MENU:
_exit_main_menu()
GameStates.CONNECTING:
_exit_connecting()
GameStates.WAITING_FOR_PLAYERS:
_exit_waiting()
GameStates.LOADING:
_exit_loading()
GameStates.PLAYING:
_exit_playing()
GameStates.PAUSED:
_exit_paused()
GameStates.ROUND_END:
_exit_round_end()
GameStates.GAME_OVER:
_exit_game_over()
func _enter_main_menu() -> void:
get_tree().paused = false
get_tree().change_scene_to_file("res://scenes/menu.tscn")
func _exit_main_menu() -> void:
pass
func _enter_connecting() -> void:
# 显示连接界面
pass
func _exit_connecting() -> void:
pass
func _enter_waiting() -> void:
# 显示等待玩家界面
GameManager.game_state = GameManager.GameState.WAITING
RoundManager._start_round_intermission()
func _exit_waiting() -> void:
pass
func _enter_loading() -> void:
# 显示加载界面
pass
func _exit_loading() -> void:
pass
func _enter_playing() -> void:
get_tree().paused = false
GameManager.game_state = GameManager.GameState.PLAYING
func _exit_playing() -> void:
pass
func _enter_paused() -> void:
get_tree().paused = true
# 显示暂停菜单
pass
func _exit_paused() -> void:
get_tree().paused = false
func _enter_round_end() -> void:
# 显示回合结束界面
pass
func _exit_round_end() -> void:
pass
func _enter_game_over() -> void:
GameManager.game_state = GameManager.GameState.GAME_OVER
# 显示游戏结束界面
pass
func _exit_game_over() -> void:
pass
func _process(delta: float) -> void:
match current_state:
GameStates.MAIN_MENU:
_update_main_menu(delta)
GameStates.WAITING_FOR_PLAYERS:
_update_waiting(delta)
GameStates.PLAYING:
_update_playing(delta)
func _update_main_menu(_delta: float) -> void:
pass
func _update_waiting(_delta: float) -> void:
# 检查玩家数量
if GameManager.players.size() >= GameManager._min_players:
transition_to(GameStates.LOADING)
func _update_playing(_delta: float) -> void:
pass
11. 数据管理(玩家字典)
# player_data.gd
class_name PlayerData
extends RefCounted
## 玩家数据结构
var peer_id: int = 0
var name: String = ""
var is_alive: bool = true
var lives: int = 3
var score: int = 0
var kills: int = 0
var deaths: int = 0
var ping: int = 0
var last_position: Vector2 = Vector2.ZERO
var custom_data: Dictionary = {}
func _init() -> void:
pass
func to_dict() -> Dictionary:
"""序列化为字典"""
return {
"peer_id": peer_id,
"name": name,
"is_alive": is_alive,
"lives": lives,
"score": score,
"kills": kills,
"deaths": deaths,
"ping": ping,
"last_position": {"x": last_position.x, "y": last_position.y},
"custom_data": custom_data
}
func from_dict(data: Dictionary) -> void:
"""从字典反序列化"""
peer_id = data.get("peer_id", 0)
name = data.get("name", "")
is_alive = data.get("is_alive", true)
lives = data.get("lives", 3)
score = data.get("score", 0)
kills = data.get("kills", 0)
deaths = data.get("deaths", 0)
ping = data.get("ping", 0)
var pos_dict: Dictionary = data.get("last_position", {})
last_position = Vector2(pos_dict.get("x", 0), pos_dict.get("y", 0))
custom_data = data.get("custom_data", {})
func reset() -> void:
"""重置玩家数据"""
is_alive = true
lives = 3
score = 0
kills = 0
deaths = 0
last_position = Vector2.ZERO
func add_kill() -> void:
kills += 1
score += 100
func add_death() -> void:
deaths += 1
lives = maxi(lives - 1, 0)
func update_ping(new_ping: int) -> void:
ping = new_ping
# player_registry.gd
class_name PlayerRegistry
extends Node
## 玩家注册表,管理所有玩家数据
signal player_registered(peer_id: int, data: PlayerData)
signal player_unregistered(peer_id: int)
signal player_updated(peer_id: int, data: PlayerData)
var _players: Dictionary = {} # peer_id -> PlayerData
func _ready() -> void:
# 连接 GameManager 信号
if GameManager:
GameManager.player_joined.connect(_on_player_joined)
GameManager.player_left.connect(_on_player_left)
func register_player(peer_id: int, data: PlayerData) -> void:
_players[peer_id] = data
player_registered.emit(peer_id, data)
func unregister_player(peer_id: int) -> void:
if _players.has(peer_id):
_players.erase(peer_id)
player_unregistered.emit(peer_id)
func get_player(peer_id: int) -> PlayerData:
return _players.get(peer_id)
func get_all_players() -> Array[PlayerData]:
return Array(_players.values(), TYPE_OBJECT, "", null)
func get_alive_players() -> Array[PlayerData]:
return get_all_players().filter(func(p): return p.is_alive)
func get_player_count() -> int:
return _players.size()
func has_player(peer_id: int) -> bool:
return _players.has(peer_id)
func _on_player_joined(peer_id: int) -> void:
var data := PlayerData.new()
data.peer_id = peer_id
data.name = "Player_%d" % peer_id
register_player(peer_id, data)
func _on_player_left(peer_id: int) -> void:
unregister_player(peer_id)
func update_player(peer_id: int, updater: Callable) -> void:
"""使用回调更新玩家数据"""
var data := get_player(peer_id)
if data:
updater.call(data)
player_updated.emit(peer_id, data)