feat:增加多角色在线功能

- 增加远程登录角色精灵
- 基于后端接口完成位置同步
- 实现多人在线以及跳转
- 增加个人房间功能
This commit is contained in:
2026-01-10 21:26:15 +08:00
parent ce47bd6eeb
commit ed7d89e39d
21 changed files with 973 additions and 39 deletions

View File

@@ -29,7 +29,7 @@ extends RefCounted
# ============ 信号定义 ============
# 登录成功信号
signal login_success(username: String)
signal login_success(username: String, token: String)
# 登录失败信号
signal login_failed(message: String)
@@ -80,6 +80,9 @@ var current_email: String = ""
# 网络请求管理
var active_request_ids: Array = []
# 当前登录用户ID (静态变量,全局访问)
static var current_user_id: String = ""
# ============ 生命周期方法 ============
# 初始化管理器
@@ -431,12 +434,20 @@ func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary)
if result.success:
var username = ""
if data.has("data") and data.data.has("user") and data.data.user.has("username"):
username = data.data.user.username
if data.has("data") and data.data.has("user"):
var user_data = data.data.user
if user_data.has("username"):
username = user_data.username
if user_data.has("id"):
current_user_id = user_data.id
print("AuthManager: Current User ID set to ", current_user_id)
# 延迟发送登录成功信号
await Engine.get_main_loop().create_timer(1.0).timeout
login_success.emit(username)
var token = ""
if data.has("data") and data.data.has("access_token"):
token = data.data.access_token
login_success.emit(username, token)
else:
login_failed.emit(result.message)
@@ -451,11 +462,16 @@ func _on_verification_login_response(success: bool, data: Dictionary, error_info
if result.success:
var username = ""
if data.has("data") and data.data.has("user") and data.data.user.has("username"):
username = data.data.user.username
if data.has("data") and data.data.has("user"):
var user_data = data.data.user
if user_data.has("username"):
username = user_data.username
if user_data.has("id"):
current_user_id = user_data.id
print("AuthManager: Current User ID set to ", current_user_id)
await Engine.get_main_loop().create_timer(1.0).timeout
login_success.emit(username)
login_success.emit(username, data.get("access_token", ""))
else:
login_failed.emit(result.message)

View File

@@ -41,8 +41,13 @@ signal request_failed(request_id: String, error_type: String, message: String)
# ============ 常量定义 ============
# API基础URL - 所有请求的根地址
# [Remote] 正式环境地址 (实际正式项目用此地址)
# [Remote] 正式环境地址 (实际正式项目用此地址)
const API_BASE_URL = "https://whaletownend.xinghangee.icu"
# [Local] 本地调试地址 (本地调试用此地址)
# const API_BASE_URL = "http://localhost:3000"
# 默认请求超时时间(秒)
const DEFAULT_TIMEOUT = 30.0

View File

@@ -37,6 +37,7 @@ signal scene_change_started(scene_name: String)
var current_scene_name: String = "" # 当前场景名称
var is_changing_scene: bool = false # 是否正在切换场景
var _next_scene_position: Variant = null # 下一个场景的初始位置 (Vector2 or null)
var _next_spawn_name: String = "" # 下一个场景的出生点名称 (String)
# 场景路径映射表
# 将场景名称映射到实际的文件路径
@@ -109,15 +110,18 @@ func change_scene(scene_name: String, use_transition: bool = true):
if use_transition:
await show_transition()
# 更新场景名称(在切换之前设置,确保新场景的 _ready 能获取正确的名称)
current_scene_name = scene_name
# 执行场景切换
var error = get_tree().change_scene_to_file(scene_path)
if error != OK:
print("场景切换失败: ", error)
current_scene_name = "" # 恢复为空
is_changing_scene = false
return false
# 更新状态
current_scene_name = scene_name
is_changing_scene = false
scene_changed.emit(scene_name)
@@ -147,6 +151,16 @@ func get_next_scene_position() -> Variant:
_next_scene_position = null
return pos
# 设置下一个场景的出生点名称
func set_next_spawn_name(spawn_name: String) -> void:
_next_spawn_name = spawn_name
# 获取并清除下一个场景的出生点名称
func get_next_spawn_name() -> String:
var name = _next_spawn_name
_next_spawn_name = ""
return name
# ============ 场景注册方法 ============

View File

@@ -0,0 +1,172 @@
extends Node
# ============================================================================
# WebSocketManager.gd - WebSocket连接管理器
# ============================================================================
# 负责与后端 Native WebSocket 服务进行实时通信
#
# 协议文档: new_docs/game_architecture_design.md
# 后端地址: ws://localhost:3000/location-broadcast
# ============================================================================
signal connected_to_server()
signal connection_closed()
signal connection_error()
signal session_joined(data: Dictionary)
signal user_joined(data: Dictionary)
signal user_left(data: Dictionary)
signal position_updated(data: Dictionary)
const WS_URL = "wss://whaletownend.xinghangee.icu/location-broadcast"
const PING_INTERVAL = 25.0 # 秒
var _socket: WebSocketPeer
var _connected: bool = false
var _ping_timer: float = 0.0
var _auth_token: String = ""
func _ready():
_socket = WebSocketPeer.new()
process_mode = Node.PROCESS_MODE_ALWAYS # 保证暂停时也能处理网络
print("WebSocketManager Initialized")
func _process(delta):
_socket.poll()
var state = _socket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
if not _connected:
_on_connected()
# 处理接收到的数据包
while _socket.get_available_packet_count() > 0:
var packet = _socket.get_packet()
_handle_packet(packet)
# 心跳处理
_ping_timer += delta
if _ping_timer >= PING_INTERVAL:
_send_heartbeat()
_ping_timer = 0.0
elif state == WebSocketPeer.STATE_CLOSED:
if _connected:
_on_disconnected()
func connect_to_server():
if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
print("WebSocket 已经是连接状态")
return
print("正在连接 WebSocket: ", WS_URL)
var err = _socket.connect_to_url(WS_URL)
if err != OK:
print("WebSocket 连接请求失败: ", err)
connection_error.emit()
else:
# Godot WebSocket connect is non-blocking, wait for state change in _process
pass
func close_connection():
_socket.close()
func set_auth_token(token: String):
_auth_token = token
# ============ 协议发送 ============
func send_packet(event: String, data: Dictionary):
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
var message = {
"event": event,
"data": data
}
var json_str = JSON.stringify(message)
_socket.put_packet(json_str.to_utf8_buffer())
func join_session(map_id: String, initial_pos: Vector2):
var data = {
"sessionId": map_id,
"initialPosition": {
"x": initial_pos.x,
"y": initial_pos.y,
"mapId": map_id
},
"token": _auth_token
}
print("发送加入会话请求: ", map_id, " mapId Payload: ", data.initialPosition.mapId)
send_packet("join_session", data)
func leave_session(map_id: String):
print("发送离开会话请求: ", map_id)
send_packet("leave_session", {"sessionId": map_id})
func send_position_update(map_id: String, pos: Vector2, anim_data: Dictionary = {}):
var data = {
"x": pos.x,
"y": pos.y,
"mapId": map_id,
"metadata": anim_data
}
# print("发送位置更新: ", map_id)
if map_id == "":
print("WARNING: Sending position update with EMPTY mapId! Pos: ", pos)
send_packet("position_update", data)
func _send_heartbeat():
send_packet("heartbeat", {"timestamp": Time.get_unix_time_from_system()})
# ============ 事件处理 ============
func _on_connected():
_connected = true
print("WebSocket 连接成功!")
connected_to_server.emit()
func _on_disconnected():
_connected = false
var code = _socket.get_close_code()
var reason = _socket.get_close_reason()
print("WebSocket 连接断开. Code: %d, Reason: %s" % [code, reason])
connection_closed.emit()
func _handle_packet(packet: PackedByteArray):
var json_str = packet.get_string_from_utf8()
var json = JSON.new()
var err = json.parse(json_str)
if err != OK:
print("JSON 解析失败: ", json.get_error_message())
return
var message = json.data
if not message is Dictionary or not message.has("event"):
return
var event = message["event"]
var data = message.get("data", {})
if event != "heartbeat_response":
print("WebSocket Rx: ", event) # Debug logs for all events
match event:
"session_joined":
session_joined.emit(data)
print("加入会话成功,当前房间人数: ", data.get("users", []).size())
"user_joined":
user_joined.emit(data)
print("用户加入: ", data.get("userId"))
"user_left":
user_left.emit(data)
print("用户离开: ", data.get("userId"))
"position_update":
print("WebSocket Rx position_update: ", data.get("userId", "unknown"))
position_updated.emit(data)
"heartbeat_response":
pass # 静默处理
"error":
print("WebSocket 错误事件: ", JSON.stringify(data))
_:
print("未处理的 WebSocket 事件: ", event)

View File

@@ -0,0 +1 @@
uid://stpl2jdeqo0d