feat:增加多角色在线功能
- 增加远程登录角色精灵 - 基于后端接口完成位置同步 - 实现多人在线以及跳转 - 增加个人房间功能
This commit is contained in:
318
scenes/Maps/BaseLevel.gd
Normal file
318
scenes/Maps/BaseLevel.gd
Normal file
@@ -0,0 +1,318 @@
|
||||
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
|
||||
1
scenes/Maps/BaseLevel.gd.uid
Normal file
1
scenes/Maps/BaseLevel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c5ml4722ptwp2
|
||||
@@ -1,8 +1,9 @@
|
||||
extends Area2D
|
||||
|
||||
# 场景名称
|
||||
@export var target_scene_name: String = "room"
|
||||
@export var target_position: Vector2 = Vector2.ZERO # 目标场景的生成位置 (Vector2.ZERO 表示使用默认位置/不设置)
|
||||
@export var target_scene_name: String = ""
|
||||
@export var target_position: Vector2 = Vector2.ZERO # 兼容旧逻辑
|
||||
@export var target_spawn_name: String = "" # 新逻辑:指定目标场景的 Marker2D 名称 (例如 "FromSquare") # 目标场景的生成位置 (Vector2.ZERO 表示使用默认位置/不设置)
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
@@ -23,10 +24,16 @@ func _on_body_entered(body: Node2D) -> void:
|
||||
_teleport_player()
|
||||
|
||||
func _teleport_player() -> void:
|
||||
# 如果设置了目标位置,则传递给 SceneManager
|
||||
if target_position != Vector2.ZERO:
|
||||
if target_scene_name == "":
|
||||
print("Error: Target scene name is empty!")
|
||||
return
|
||||
|
||||
print("Teleporting to scene: ", target_scene_name)
|
||||
|
||||
# 设置参数
|
||||
if target_spawn_name != "":
|
||||
SceneManager.set_next_spawn_name(target_spawn_name)
|
||||
elif target_position != Vector2.ZERO:
|
||||
SceneManager.set_next_scene_position(target_position)
|
||||
|
||||
# 使用 SceneManager 切换场景
|
||||
# 确保 SceneManager 已经注册了相关场景路径
|
||||
SceneManager.change_scene(target_scene_name)
|
||||
1
scenes/Maps/DoorTeleport.gd.uid
Normal file
1
scenes/Maps/DoorTeleport.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b068cnbw3a8wt
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user