Files
whale-town-front/scenes/Maps/BaseLevel.gd
xiangwang ed7d89e39d feat:增加多角色在线功能
- 增加远程登录角色精灵
- 基于后端接口完成位置同步
- 实现多人在线以及跳转
- 增加个人房间功能
2026-01-10 21:26:15 +08:00

319 lines
11 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name BaseLevel
extends Node2D
# 基础关卡脚本
# 负责处理通用的关卡逻辑,如玩家生成
@onready var spawn_points = $SpawnPoints if has_node("SpawnPoints") else null
@onready var players_container = $Objects/Players if has_node("Objects/Players") else self
func _ready():
# 延时一帧确保所有子节点就绪
call_deferred("_spawn_player")
# 连接到多人会话
# 获取当前场景名字作为 Session ID
_current_session_id = SceneManager.get_current_scene_name()
if _current_session_id == "":
_current_session_id = "square" # Fallback for direct run
# 如果是私人场景,生成唯一的 Session ID (e.g., room_123)
if _current_session_id in PRIVATE_SCENES:
_current_session_id = _current_session_id + "_" + str(AuthManager.current_user_id)
print("BaseLevel: 进入私密房间实例: ", _current_session_id)
print("BaseLevel: Preparing to join session: ", _current_session_id)
# 如果 WebSocket 已连接,直接加入
if WebSocketManager._socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
_join_session_with_player(_current_session_id)
else:
# 否则等待连接成功信号
WebSocketManager.connected_to_server.connect(func(): _join_session_with_player(_current_session_id))
# 连接远程玩家相关信号
WebSocketManager.session_joined.connect(_on_session_joined)
WebSocketManager.user_joined.connect(_on_user_joined)
WebSocketManager.user_left.connect(_on_user_left)
WebSocketManager.position_updated.connect(_on_position_updated)
# Debug: Simulate fake player to verify rendering
# get_tree().create_timer(2.0).timeout.connect(_debug_fake_activity)
func _exit_tree():
# 场景卸载时不需要再做操作,交给 DoorTeleport 或其他切换逻辑显示处理
pass
func _debug_fake_activity():
print("BaseLevel: Starting fake player simulation")
var fake_id = "fake_ghost"
_on_user_joined({"user": {"userId": fake_id, "username": "Ghost"}, "position": {"x": 500, "y": 500}})
for i in range(20):
await get_tree().create_timer(0.5).timeout
var new_pos = Vector2(500 + i * 20, 500 + i * 10)
_on_position_updated({"userId": fake_id, "position": {"x": new_pos.x, "y": new_pos.y}})
print("BaseLevel: Ghost moved to ", new_pos)
var remote_players: Dictionary = {} # userId -> RemotePlayer instance
var remote_player_scene = preload("res://scenes/characters/remote_player.tscn")
var _player_spawned: bool = false # Track if local player has been spawned
var _local_player: Node = null # Reference to the local player node
var _position_update_timer: float = 0.0 # Throttle timer for position updates
var _current_session_id: String = "" # Cache session ID to avoid race condition on exit
const POSITION_UPDATE_INTERVAL: float = 0.125 # Send at most 8 times per second
const PRIVATE_SCENES = ["room"] # List of scenes that should be private instances
func _on_session_joined(data: Dictionary):
# 对账远程玩家列表,使用 position.mapId 过滤
if not data.has("users"):
return
var current_map = _current_session_id
if current_map == "":
current_map = SceneManager.get_current_scene_name()
if current_map == "":
current_map = "square"
print("BaseLevel: 同步会话 - 当前场景: ", current_map, ", 收到用户数: ", data.users.size())
# 1. 收集服务器返回的、且 mapId 匹配当前场景的用户ID
var valid_user_ids: Array = []
var valid_users: Dictionary = {} # userId -> user data
for user in data.users:
if not user.has("userId") or str(user.userId) == str(AuthManager.current_user_id):
continue
# 检查 position.mapId 是否匹配当前场景
var user_map_id = ""
if user.has("position") and user.position != null:
if typeof(user.position) == TYPE_DICTIONARY and user.position.has("mapId"):
user_map_id = str(user.position.mapId)
print("BaseLevel: 用户 ", user.userId, " 的位置数据: ", user.position)
else:
print("BaseLevel: 用户 ", user.userId, " 没有位置数据")
print("BaseLevel: 用户 ", user.userId, " mapId=", user_map_id, ", 当前场景=", current_map)
if user_map_id == current_map:
var uid = str(user.userId)
valid_user_ids.append(uid)
valid_users[uid] = user
print("BaseLevel: 添加有效用户: ", uid)
else:
# mapId 不匹配,这是"幽灵玩家"
print("BaseLevel: 跳过 mapId 不匹配的玩家: ", user.get("userId"), " (mapId=", user_map_id, ", expected=", current_map, ")")
# 2. 清理幽灵:移除本地有但不在有效列表中的玩家
var local_user_ids = remote_players.keys()
for user_id in local_user_ids:
if str(user_id) not in valid_user_ids:
print("BaseLevel: 清理幽灵玩家: ", user_id)
_remove_remote_player(user_id)
# 3. 添加或更新有效的玩家
for uid in valid_user_ids:
var user = valid_users[uid]
if remote_players.has(uid):
# 已存在,更新位置
_update_remote_player_position(user)
else:
# 不存在,创建新玩家
print("BaseLevel: 创建远程玩家: ", uid)
_add_remote_player(user)
func _on_user_joined(data: Dictionary):
var user = data.get("user", {})
if user.has("userId"):
print("BaseLevel: 新玩家加入: ", user.userId)
if user.userId == AuthManager.current_user_id:
return
# 将 position 数据合并到 user 字典中,以便 _add_remote_player 统一处理
if data.has("position"):
user["position"] = data.position
_add_remote_player(user)
func _on_user_left(data: Dictionary):
var user_id = data.get("userId")
if user_id:
print("BaseLevel: 玩家离开: ", user_id)
_remove_remote_player(user_id)
func _on_position_updated(data: Dictionary):
var user_id = data.get("userId")
if user_id and remote_players.has(user_id):
var player = remote_players[user_id]
# 数据可能直接是位置(扁平)或者包含在 position 字段中
# 根据后端协议: { userId:..., position: {x,y...}, ... }
var pos_data = data.get("position", {})
if pos_data.is_empty():
pos_data = data # 兼容扁平格式
# print("BaseLevel: 收到位置更新: ", user_id, " Data: ", pos_data) # Debug log
# 检查 mapId 是否匹配当前场景
if pos_data.has("mapId") and str(pos_data.mapId) != "":
var current_map = _current_session_id
if current_map == "":
current_map = "square"
if str(pos_data.mapId) != current_map:
print("BaseLevel: 收到异地位置更新,移除幽灵玩家: ", user_id, " (在该玩家在 ", pos_data.mapId, ")")
_remove_remote_player(user_id)
return
if player.has_method("update_position") and pos_data.has("x") and pos_data.has("y"):
player.update_position(Vector2(pos_data.x, pos_data.y))
func _add_remote_player(user_data: Dictionary):
var user_id = str(user_data.get("userId", ""))
if user_id == "":
return
# 防止重复创建
if remote_players.has(user_id):
return
var remote_player = remote_player_scene.instantiate()
# 使用统一的 setup 方法
if remote_player.has_method("setup"):
remote_player.setup(user_data)
else:
# Fallback: 手动设置属性 (如果脚本没更新)
remote_player.position = Vector2.ZERO
if user_data.has("position"):
var p = user_data.position
if p.has("x") and p.has("y"):
remote_player.position = Vector2(p.x, p.y)
# 添加到场景玩家容器
if has_node("Objects/Players"):
$Objects/Players.add_child(remote_player)
else:
add_child(remote_player)
remote_players[user_id] = remote_player
print("BaseLevel: 已创建远程玩家: ", user_id)
func _remove_remote_player(user_id):
var uid = str(user_id)
if remote_players.has(uid):
var player = remote_players[uid]
if is_instance_valid(player):
player.queue_free()
remote_players.erase(uid)
func _update_remote_player_position(user: Dictionary):
var user_id = str(user.get("userId", ""))
var player = remote_players.get(user_id)
if not player or not is_instance_valid(player):
return
if user.has("position"):
var pos = user.position
if pos.has("x") and pos.has("y") and player.has_method("update_position"):
player.update_position(Vector2(pos.x, pos.y))
func _join_session_with_player(session_id: String):
# 检查是否有Token如果没有则等待
if WebSocketManager._auth_token == "":
print("BaseLevel: Token not ready, waiting...")
# 轮询等待Token就绪 (简单重试机制)
await get_tree().create_timer(0.5).timeout
_join_session_with_player(session_id)
return
# 等待玩家生成完毕
if not _player_spawned or not _local_player:
await get_tree().process_frame
_join_session_with_player(session_id)
return
var pos = _local_player.global_position if is_instance_valid(_local_player) else Vector2.ZERO
WebSocketManager.join_session(session_id, pos)
# 强制广播一次位置更新,确保旧房间的玩家立即收到 "已切换地图" 的通知
# 这能解决"需要移动两步幽灵才消失"的问题
await get_tree().create_timer(0.1).timeout
WebSocketManager.send_position_update(session_id, pos)
func _process(delta):
# 发送位置更新 (节流机制)
if not _player_spawned or not _local_player:
return # Wait for player to be spawned
if WebSocketManager._socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
return # WebSocket not connected
if not is_instance_valid(_local_player):
return # Player was freed
# 检查 velocity 属性
if not "velocity" in _local_player:
return
# 只有在移动时才更新计时器和发送
if _local_player.velocity.length() > 0:
_position_update_timer += delta
if _position_update_timer >= POSITION_UPDATE_INTERVAL:
_position_update_timer = 0.0
# 使用 _current_session_id 确保有正确的 fallback
var map_id = _current_session_id if _current_session_id != "" else "square"
WebSocketManager.send_position_update(map_id, _local_player.global_position)
func _spawn_player():
# 1. 确定出生位置
var spawn_pos = Vector2.ZERO
var spawn_name = SceneManager.get_next_spawn_name()
print("BaseLevel: Checking spawn point for name: '", spawn_name, "'")
# 查找逻辑:优先查找名为 spawn_name 的节点,其次找 DefaultSpawn
var target_node_name = spawn_name if spawn_name != "" else "DefaultSpawn"
var marker_node = null
# 策略 A: 在 SpawnPoints 容器中查找
if has_node("SpawnPoints"):
marker_node = $SpawnPoints.get_node_or_null(target_node_name)
# 策略 B: 如果没找到,在当前节点(根节点)下查找
if marker_node == null:
marker_node = get_node_or_null(target_node_name)
# 如果找到了标记点,使用其位置
if marker_node:
spawn_pos = marker_node.global_position
print("BaseLevel: Found spawn marker '", target_node_name, "' at ", spawn_pos)
else:
# 策略 C: 检查 SceneManager 是否有备用坐标 (兼容旧逻辑)
var pos_param = SceneManager.get_next_scene_position()
if pos_param != null:
spawn_pos = pos_param
print("BaseLevel: Using explicit position from SceneManager: ", spawn_pos)
else:
print("BaseLevel: Warning - Could not find marker '", target_node_name, "' and no explicit position set. Using (0,0)")
# 2. 实例化玩家
var player_scene = preload("res://scenes/characters/player.tscn")
var player = player_scene.instantiate()
player.global_position = spawn_pos
# 添加到场景
# 如果有Objects/Players容器则添加进去否则直接添加到当前节点
if has_node("Objects/Players"):
$Objects/Players.add_child(player)
else:
add_child(player)
print("BaseLevel: Player spawned at ", player.global_position)
_local_player = player # Save reference
_player_spawned = true