diff --git a/_Core/managers/ChatManager.gd b/_Core/managers/ChatManager.gd index 9ad2c19..e634109 100644 --- a/_Core/managers/ChatManager.gd +++ b/_Core/managers/ChatManager.gd @@ -233,7 +233,7 @@ func send_chat_message(content: String, scope: String = "local") -> void: # 构建消息数据 var message_data := { - "t": "chat", + "type": "chat", "content": content, "scope": scope } @@ -284,7 +284,7 @@ func update_player_position(x: float, y: float, map_id: String) -> void: return var position_data := { - "t": "position", + "type": "position", "x": x, "y": y, "mapId": map_id diff --git a/_Core/systems/SocketIOClient.gd b/_Core/systems/SocketIOClient.gd deleted file mode 100644 index f71e29f..0000000 --- a/_Core/systems/SocketIOClient.gd +++ /dev/null @@ -1,304 +0,0 @@ -extends Node - -# ============================================================================ -# SocketIOClient.gd - Socket.IO 协议封装 -# ============================================================================ -# 封装 Godot 的 WebSocketPeer,实现简化的 Socket.IO 协议 -# -# 核心职责: -# - WebSocket 连接管理 -# - Socket.IO 消息协议(简化版 JSON 格式) -# - 事件监听器管理 -# - 消息发送/接收 -# -# 注意事项: -# - 后端使用简化版 Socket.IO(纯 JSON,无二进制协议) -# - 发送消息使用 "t" 字段标识事件类型 -# - 所有消息通过 JSON 序列化 -# ============================================================================ - -class_name SocketIOClient - -# ============================================================================ -# 信号定义 -# ============================================================================ - -# 连接成功信号 -signal connected() - -# 连接断开信号 -# 参数: -# clean_close: bool - 是否为正常关闭 -signal disconnected(clean_close: bool) - -# 事件接收信号 -# 参数: -# event_name: String - 事件名称(从 "t" 字段提取) -# data: Dictionary - 事件数据 -signal event_received(event_name: String, data: Dictionary) - -# 错误发生信号 -# 参数: -# error: String - 错误信息 -signal error_occurred(error: String) - -# ============================================================================ -# 常量定义 -# ============================================================================ - -# 连接状态枚举 -enum ConnectionState { - DISCONNECTED, # 未连接 - CONNECTING, # 连接中 - CONNECTED # 已连接 -} - -# ============================================================================ -# 成员变量 -# ============================================================================ - -# WebSocket 客户端 -var _websocket_peer: WebSocketPeer = WebSocketPeer.new() - -# 连接状态 -var _connection_state: ConnectionState = ConnectionState.DISCONNECTED - -# 服务器 URL -var _server_url: String = "" - -# 事件监听器: {event_name: [Callable, ...]} -var _event_listeners: Dictionary = {} - -# ============================================================================ -# 生命周期方法 -# ============================================================================ - -# 初始化 -func _ready() -> void: - print("SocketIOClient 初始化完成") - -# 处理进程 - 轮询 WebSocket 消息 -func _process(_delta: float) -> void: - # 轮询 WebSocket 状态 - _websocket_peer.poll() - - # 检查连接状态变化 - var new_state: ConnectionState = _get_connection_state() - if new_state != _connection_state: - _connection_state = new_state - _on_state_changed(_connection_state) - - # 处理接收到的消息 - _process_incoming_messages() - -# ============================================================================ -# 公共 API - 连接管理 -# ============================================================================ - -# 连接到服务器 -# -# 参数: -# url: String - WebSocket 服务器 URL (ws:// 或 wss://) -# -# 使用示例: -# socket_client.connect_to_server("wss://example.com/game") -func connect_to_server(url: String) -> void: - if _connection_state == ConnectionState.CONNECTED: - push_warning("已经连接到服务器,无需重复连接") - return - - _server_url = url - print("=== SocketIOClient 开始连接 ===") - print("服务器 URL: ", _server_url) - - # 创建 WebSocket 客户端 - _websocket_peer = WebSocketPeer.new() - - # 发起连接 - var error := _websocket_peer.connect_to_url(url) - if error != OK: - push_error("WebSocket 连接失败: %s" % error) - error_occurred.emit("WebSocket 连接失败") - return - - _connection_state = ConnectionState.CONNECTING - print("WebSocket 连接中...") - -# 断开连接 -func disconnect_from_server() -> void: - if _connection_state == ConnectionState.DISCONNECTED: - return - - print("=== SocketIOClient 断开连接 ===") - _websocket_peer.close() - _connection_state = ConnectionState.DISCONNECTED - disconnected.emit(true) - -# 检查 Socket 是否已连接 -# -# 返回值: -# bool - 是否已连接 -func is_socket_connected() -> bool: - return _connection_state == ConnectionState.CONNECTED - -# ============================================================================ -# 公共 API - 事件发送 -# ============================================================================ - -# 发送事件(对应 socket.emit) -# -# 参数: -# event_name: String - 事件名称(如 "login", "chat") -# data: Dictionary - 事件数据 -# -# 使用示例: -# socket_client.emit("login", {"type": "login", "token": "abc123"}) -# socket_client.emit("chat", {"t": "chat", "content": "Hello", "scope": "local"}) -func emit(event_name: String, data: Dictionary) -> void: - if not is_socket_connected(): - push_error("无法发送事件: 未连接到服务器") - error_occurred.emit("未连接到服务器") - return - - # 序列化为 JSON - var json_string := JSON.stringify(data) - if json_string.is_empty(): - push_error("JSON 序列化失败") - error_occurred.emit("JSON 序列化失败") - return - - # 发送数据包 - var packet := json_string.to_utf8_buffer() - var error := _websocket_peer.send(packet) - - if error != OK: - push_error("发送数据包失败: %s" % error) - error_occurred.emit("发送数据包失败") - return - - print("📤 发送事件: ", event_name) - print(" 数据: ", json_string if json_string.length() < 200 else json_string.substr(0, 200) + "...") - -# ============================================================================ -# 公共 API - 事件监听 -# ============================================================================ - -# 添加事件监听器(对应 socket.on) -# -# 参数: -# event_name: String - 事件名称 -# callback: Callable - 回调函数,接收 data: Dictionary 参数 -# -# 使用示例: -# socket_client.add_event_listener("chat_render", func(data): -# print("收到消息: ", data.txt) -# ) -func add_event_listener(event_name: String, callback: Callable) -> void: - if not _event_listeners.has(event_name): - _event_listeners[event_name] = [] - - _event_listeners[event_name].append(callback) - print("注册事件监听器: ", event_name, " -> ", callback) - -# 移除事件监听器 -# -# 参数: -# event_name: String - 事件名称 -# callback: Callable - 要移除的回调函数 -func remove_event_listener(event_name: String, callback: Callable) -> void: - if not _event_listeners.has(event_name): - return - - var listeners: Array = _event_listeners[event_name] - listeners.erase(callback) - - if listeners.is_empty(): - _event_listeners.erase(event_name) - - print("移除事件监听器: ", event_name, " -> ", callback) - -# ============================================================================ -# 内部方法 - 消息处理 -# ============================================================================ - -# 处理接收到的消息 -func _process_incoming_messages() -> void: - # 检查是否有可用数据包 - while _websocket_peer.get_available_packet_count() > 0: - # 接收数据包 - var packet: PackedByteArray = _websocket_peer.get_packet() - - # 解析为字符串 - var json_string: String = packet.get_string_from_utf8() - - # 解析 JSON - var json := JSON.new() - var parse_result := json.parse(json_string) - - if parse_result != OK: - push_error("JSON 解析失败: " + json_string) - error_occurred.emit("JSON 解析失败") - continue - - var data: Dictionary = json.data - - # 提取事件类型(从 "t" 字段) - var event_name: String = data.get("t", "") - if event_name.is_empty(): - # 如果没有 "t" 字段,尝试其他方式识别 - if data.has("type"): - event_name = data["type"] - elif data.has("code"): - event_name = "error" - else: - push_warning("收到未知格式消息: " + json_string) - continue - - print("📨 收到事件: ", event_name) - print(" 数据: ", json_string if json_string.length() < 200 else json_string.substr(0, 200) + "...") - - # 调用事件监听器 - _notify_event_listeners(event_name, data) - - # 发射通用信号 - event_received.emit(event_name, data) - -# 通知事件监听器 -func _notify_event_listeners(event_name: String, data: Dictionary) -> void: - if not _event_listeners.has(event_name): - return - - var listeners: Array = _event_listeners[event_name] - for callback in listeners: - if callback.is_valid(): - callback.call(data) - -# ============================================================================ -# 内部方法 - 状态管理 -# ============================================================================ - -# 获取当前连接状态 -func _get_connection_state() -> ConnectionState: - match _websocket_peer.get_ready_state(): - WebSocketPeer.STATE_CONNECTING: - return ConnectionState.CONNECTING - WebSocketPeer.STATE_OPEN: - return ConnectionState.CONNECTED - WebSocketPeer.STATE_CLOSING: - return ConnectionState.CONNECTING - WebSocketPeer.STATE_CLOSED: - return ConnectionState.DISCONNECTED - _: - return ConnectionState.DISCONNECTED - -# 连接状态变化处理 -func _on_state_changed(new_state: ConnectionState) -> void: - match new_state: - ConnectionState.CONNECTED: - print("✅ WebSocket 连接成功") - connected.emit() - ConnectionState.DISCONNECTED: - print("🔌 WebSocket 连接断开") - disconnected.emit(false) - ConnectionState.CONNECTING: - print("⏳ WebSocket 连接中...") diff --git a/_Core/systems/SocketIOClient.gd.uid b/_Core/systems/SocketIOClient.gd.uid deleted file mode 100644 index 0bac43c..0000000 --- a/_Core/systems/SocketIOClient.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d0b3aiagnuhxx diff --git a/assets/audio/music/Spawn Fixed.mp3 b/assets/audio/music/Spawn Fixed.mp3 new file mode 100644 index 0000000..0a7b794 Binary files /dev/null and b/assets/audio/music/Spawn Fixed.mp3 differ diff --git a/assets/audio/music/Spawn Fixed.mp3.import b/assets/audio/music/Spawn Fixed.mp3.import new file mode 100644 index 0000000..8d15f7f --- /dev/null +++ b/assets/audio/music/Spawn Fixed.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://c5ujr1fj14n58" +path="res://.godot/imported/Spawn Fixed.mp3-cdebcc11ab16736c9b4a0906adccb431.mp3str" + +[deps] + +source_file="res://assets/audio/music/Spawn Fixed.mp3" +dest_files=["res://.godot/imported/Spawn Fixed.mp3-cdebcc11ab16736c9b4a0906adccb431.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/assets/ui/auth/微信图片_20251218232924_983_1425.png b/assets/ui/auth/微信图片_20251218232924_983_1425.png new file mode 100644 index 0000000..df9090b Binary files /dev/null and b/assets/ui/auth/微信图片_20251218232924_983_1425.png differ diff --git a/assets/ui/auth/微信图片_20251218232924_983_1425.png.import b/assets/ui/auth/微信图片_20251218232924_983_1425.png.import new file mode 100644 index 0000000..5b70a86 --- /dev/null +++ b/assets/ui/auth/微信图片_20251218232924_983_1425.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://0pt270gaevbk" +path="res://.godot/imported/微信图片_20251218232924_983_1425.png-0163d5f3fcf9765f1d8d0ec22851e7b0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/ui/auth/微信图片_20251218232924_983_1425.png" +dest_files=["res://.godot/imported/微信图片_20251218232924_983_1425.png-0163d5f3fcf9765f1d8d0ec22851e7b0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/ui/auth/登录背景.png b/assets/ui/auth/登录背景.png new file mode 100644 index 0000000..d52c92c Binary files /dev/null and b/assets/ui/auth/登录背景.png differ diff --git a/assets/ui/auth/登录背景.png.import b/assets/ui/auth/登录背景.png.import new file mode 100644 index 0000000..62181d5 --- /dev/null +++ b/assets/ui/auth/登录背景.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dyma4hpodhdxi" +path="res://.godot/imported/登录背景.png-89ec90e6b5b2b96eba5cf8e1b1d50ed8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/ui/auth/登录背景.png" +dest_files=["res://.godot/imported/登录背景.png-89ec90e6b5b2b96eba5cf8e1b1d50ed8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/ui/auth/输入框.png b/assets/ui/auth/输入框.png new file mode 100644 index 0000000..ab19f97 Binary files /dev/null and b/assets/ui/auth/输入框.png differ diff --git a/assets/ui/auth/输入框.png.import b/assets/ui/auth/输入框.png.import new file mode 100644 index 0000000..1d6595e --- /dev/null +++ b/assets/ui/auth/输入框.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cnrffaqbtw8f5" +path="res://.godot/imported/输入框.png-74076fbd98c6a5dee8485c2e25f4d583.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/ui/auth/输入框.png" +dest_files=["res://.godot/imported/输入框.png-74076fbd98c6a5dee8485c2e25f4d583.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/project.godot b/project.godot index 714704e..57e45d2 100644 --- a/project.godot +++ b/project.godot @@ -33,7 +33,7 @@ gdscript/warnings/treat_warnings_as_errors=false window/size/viewport_width=1920 window/size/viewport_height=1080 window/size/mode=2 -window/stretch/mode="viewport" +window/stretch/mode="canvas_items" window/stretch/aspect="expand" [gui] diff --git a/scenes/MainScene.gd b/scenes/MainScene.gd index bde945c..6eac5bb 100644 --- a/scenes/MainScene.gd +++ b/scenes/MainScene.gd @@ -90,6 +90,7 @@ func _on_login_success(username: String): print("用户 ", username, " 登录成功!") # 连接到聊天服务器(在进入游戏界面之前) + # 注意:token 已在 AuthScene._on_controller_login_success 中设置 print("🔌 开始连接聊天服务器...") ChatManager.connect_to_chat_server() diff --git a/scenes/ui/AuthScene.tscn b/scenes/ui/AuthScene.tscn index 80ae516..8e81345 100644 --- a/scenes/ui/AuthScene.tscn +++ b/scenes/ui/AuthScene.tscn @@ -1,10 +1,14 @@ -[gd_scene load_steps=10 format=3 uid="uid://by7m8snb4xllf"] +[gd_scene load_steps=13 format=3 uid="uid://by7m8snb4xllf"] [ext_resource type="Texture2D" uid="uid://bx17oy8lvaca4" path="res://assets/ui/auth/bg_auth_scene.png" id="1_background"] -[ext_resource type="Texture2D" uid="uid://b6wlro1vixo8a" path="res://assets/ui/auth/清空内部组件的登录框架 (1).png" id="3_26vyf"] [ext_resource type="Script" uid="uid://b514h2wuido0h" path="res://scenes/ui/AuthScene.gd" id="3_script"] +[ext_resource type="Texture2D" uid="uid://dyma4hpodhdxi" path="res://assets/ui/auth/登录背景.png" id="3_wh4n4"] +[ext_resource type="Texture2D" uid="uid://cnrffaqbtw8f5" path="res://assets/ui/auth/输入框.png" id="4_lnw07"] -[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1"] +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_26vyf"] + +[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_cjyup"] +texture = ExtResource("4_lnw07") [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hover"] bg_color = Color(0.3, 0.6, 0.9, 1) @@ -59,6 +63,8 @@ Button/styles/hover = SubResource("StyleBoxFlat_hover") Button/styles/normal = SubResource("StyleBoxFlat_normal") Button/styles/pressed = SubResource("StyleBoxFlat_pressed") +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1"] + [node name="AuthScene" type="Control"] layout_mode = 3 anchors_preset = 15 @@ -81,22 +87,14 @@ texture = ExtResource("1_background") expand_mode = 1 stretch_mode = 6 -[node name="WhaleFrame" type="TextureRect" parent="."] +[node name="ColorRect" type="ColorRect" parent="."] +modulate = Color(0.84313726, 0.92941177, 0.98039216, 0.47058824) layout_mode = 1 -anchors_preset = 8 -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -offset_left = -300.0 -offset_top = -300.0 -offset_right = 300.0 -offset_bottom = 300.0 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -texture = ExtResource("3_26vyf") -expand_mode = 1 -stretch_mode = 5 [node name="CenterContainer" type="CenterContainer" parent="."] layout_mode = 1 @@ -112,35 +110,45 @@ offset_bottom = 236.0 grow_horizontal = 2 grow_vertical = 2 -[node name="LoginPanel" type="Panel" parent="CenterContainer"] -custom_minimum_size = Vector2(350, 400) +[node name="WhaleFrame" type="TextureRect" parent="CenterContainer"] +custom_minimum_size = Vector2(500, 0) layout_mode = 2 -theme_override_styles/panel = SubResource("StyleBoxEmpty_1") +texture = ExtResource("3_wh4n4") +expand_mode = 4 +stretch_mode = 5 + +[node name="LoginPanel" type="Panel" parent="CenterContainer"] +custom_minimum_size = Vector2(300, 0) +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxEmpty_26vyf") [node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/LoginPanel"] layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 30.0 -offset_top = 30.0 -offset_right = -30.0 -offset_bottom = -30.0 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -140.0 +offset_top = -185.5 +offset_right = 140.0 +offset_bottom = 185.5 grow_horizontal = 2 grow_vertical = 2 [node name="TitleLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer"] layout_mode = 2 theme_override_colors/font_color = Color(0, 0, 0, 1) -theme_override_font_sizes/font_size = 24 +theme_override_colors/font_shadow_color = Color(0.011764706, 0.12156863, 0.101960786, 0) +theme_override_font_sizes/font_size = 32 text = "Whaletown" horizontal_alignment = 1 vertical_alignment = 1 [node name="SubtitleLabel" type="Label" parent="CenterContainer/LoginPanel/VBoxContainer"] layout_mode = 2 -theme_override_colors/font_color = Color(0, 0, 0, 1) -theme_override_font_sizes/font_size = 14 +theme_override_colors/font_color = Color(0.53333336, 0.53333336, 0.53333336, 1) +theme_override_font_sizes/font_size = 16 text = "开始你的小镇之旅!" horizontal_alignment = 1 vertical_alignment = 1 @@ -180,10 +188,13 @@ theme_override_font_sizes/font_size = 12 text = "用户名不能为空" horizontal_alignment = 2 -[node name="UsernameInput" type="LineEdit" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm/UsernameContainer"] +[node name="UsernameInput" type="LineEdit" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm"] +custom_minimum_size = Vector2(0, 48) layout_mode = 2 theme_override_colors/font_color = Color(0, 0, 0, 1) theme_override_colors/font_placeholder_color = Color(0.5, 0.5, 0.5, 1) +theme_override_colors/selection_color = Color(0.5, 0.5, 0.5, 1) +theme_override_styles/normal = SubResource("StyleBoxTexture_cjyup") placeholder_text = "用户名/手机/邮箱" [node name="PasswordContainer" type="VBoxContainer" parent="CenterContainer/LoginPanel/VBoxContainer/LoginForm"] @@ -552,6 +563,8 @@ theme = SubResource("Theme_button") text = "返回登录" [node name="ToastContainer" type="Control" parent="."] +modulate = Color(0.84313726, 0.92941177, 0.98039216, 1) +self_modulate = Color(0.84313726, 0.92941177, 0.9843137, 1) layout_mode = 1 anchors_preset = 15 anchor_right = 1.0