extends Node # ============================================================================ # LocationManager.gd - 位置同步管理器 # ============================================================================ # 负责与后端 WebSocket 服务进行位置同步和多人会话管理 # # 协议文档: new_docs/game_architecture_design.md # 后端地址默认值: wss://whaletownend.xinghangee.icu/game # 可通过以下方式覆盖: # 1) 环境变量 WHALETOWN_LOCATION_WS_URL # 2) Config/game_config.json 或 config/game_config.json 中的 # network.location_ws_url / network.game_ws_url # ============================================================================ 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 DEFAULT_WS_URL: String = "wss://whaletownend.xinghangee.icu/game" const WS_URL_ENV_KEY: String = "WHALETOWN_LOCATION_WS_URL" const PING_INTERVAL = 25.0 # 秒 var _socket: WebSocketPeer var _connected: bool = false var _ping_timer: float = 0.0 var _auth_token: String = "" var _connection_error_reported: bool = false var _is_connecting: bool = false var _ws_url: String = DEFAULT_WS_URL func _ready(): _socket = WebSocketPeer.new() process_mode = Node.PROCESS_MODE_ALWAYS # 保证暂停时也能处理网络 _ws_url = _resolve_ws_url() func _process(delta): _socket.poll() var state = _socket.get_ready_state() if state == WebSocketPeer.STATE_OPEN: _connection_error_reported = false _is_connecting = false 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() elif _is_connecting and not _connection_error_reported: var close_code := _socket.get_close_code() var close_reason := _socket.get_close_reason() push_warning( "LocationManager: WebSocket 握手失败,close_code=%d, reason=%s" % [close_code, close_reason] ) connection_error.emit() _connection_error_reported = true _is_connecting = false func connect_to_server(): var state: WebSocketPeer.State = _socket.get_ready_state() if state == WebSocketPeer.STATE_OPEN or state == WebSocketPeer.STATE_CONNECTING: return _connection_error_reported = false _is_connecting = true var err = _socket.connect_to_url(_ws_url) if err != OK: push_error("LocationManager: WebSocket 连接请求失败,url=%s, 错误码: %d" % [_ws_url, err]) connection_error.emit() _connection_error_reported = true _is_connecting = false else: # Godot WebSocket connect is non-blocking, wait for state change in _process pass func _resolve_ws_url() -> String: var env_url: String = OS.get_environment(WS_URL_ENV_KEY).strip_edges() if not env_url.is_empty(): return env_url for config_path in ["res://Config/game_config.json", "res://config/game_config.json"]: var config_url: String = _load_ws_url_from_config(config_path) if not config_url.is_empty(): return config_url return DEFAULT_WS_URL func _load_ws_url_from_config(config_path: String) -> String: if not FileAccess.file_exists(config_path): return "" var content: String = FileAccess.get_file_as_string(config_path) if content.is_empty(): return "" var json := JSON.new() if json.parse(content) != OK: push_warning("LocationManager: 读取配置失败 %s - %s" % [config_path, json.get_error_message()]) return "" var data_variant: Variant = json.data if not (data_variant is Dictionary): return "" var root: Dictionary = data_variant var network_variant: Variant = root.get("network", {}) if not (network_variant is Dictionary): return "" var network_config: Dictionary = network_variant var ws_url: String = str(network_config.get("location_ws_url", network_config.get("game_ws_url", ""))).strip_edges() return ws_url 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 } send_packet("join_session", data) func leave_session(map_id: String): 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 } if map_id == "": push_warning("LocationManager: position_update 的 map_id 为空") send_packet("position_update", data) func _send_heartbeat(): send_packet("heartbeat", {"timestamp": Time.get_unix_time_from_system()}) # ============ 事件处理 ============ func _on_connected(): _connected = true connected_to_server.emit() func _on_disconnected(): _connected = false 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: push_error("LocationManager: JSON 解析失败 - %s" % 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", {}) match event: "session_joined": session_joined.emit(data) "user_joined": user_joined.emit(data) "user_left": user_left.emit(data) "position_update": position_updated.emit(data) "heartbeat_response": pass # 静默处理 "error": push_error("LocationManager: WebSocket 错误事件 - %s" % JSON.stringify(data)) _: push_warning("LocationManager: 未处理的 WebSocket 事件 %s" % event)