@@ -1,7 +1,7 @@
extends Control
# ============================================================================
# ChatUI.gd - 聊天界面控制器(Enter 显示/隐藏版本 )
# ChatUI.gd - 聊天界面控制器(T 唤起 / Enter 发送 )
# ============================================================================
# 聊天系统的用户界面控制器
#
@@ -9,8 +9,10 @@ extends Control
# - 显示聊天消息历史
# - 处理用户输入
# - 显示连接状态
# - Enter 显示/隐藏 + 点击外部取消输入状态 + 5秒自动隐藏
# - 只有按 Enter 才会取消倒计时
# - T 唤起聊天框, Enter 发送消息
# - 点击聊天框外部隐藏聊天框
# - 失焦后 5 秒自动隐藏
# - 显示/隐藏带 0.5s 过渡动画
# - Call Down: 通过 EventSystem 订阅聊天事件
#
# 使用方式:
@@ -46,6 +48,8 @@ extends Control
# 聊天消息场景
@ onready var chat_message_scene : PackedScene = preload ( " res://scenes/prefabs/ui/ChatMessage.tscn " )
const CHAT_TRANSITION_DURATION : float = 0.5
# ============================================================================
# 成员变量
# ============================================================================
@@ -59,6 +63,9 @@ var _hide_timer: Timer = null
# 是否在输入中(输入时不隐藏)
var _is_typing : bool = false
# 显示/隐藏过渡动画
var _transition_tween : Tween = null
# 当前用户名
var _current_username : String = " "
@@ -69,7 +76,7 @@ var _current_username: String = ""
# 准备就绪
func _ready ( ) - > void :
# 初始隐藏聊天框
hide_chat ( )
hide_chat ( true )
# 创建隐藏计时器
_create_hide_timer ( )
@@ -80,6 +87,9 @@ func _ready() -> void:
# 连接 UI 信号
_connect_ui_signals ( )
# 尽可能保持回车提交后输入框继续编辑状态( Godot 4.6+ 支持该属性)
_enable_keep_editing_on_submit ( )
# 清理
func _exit_tree ( ) - > void :
# 取消事件订阅
@@ -93,30 +103,48 @@ func _exit_tree() -> void:
if _hide_timer :
_hide_timer . queue_free ( )
if is_instance_valid ( _transition_tween ) :
_transition_tween . kill ( )
_transition_tween = null
# ============================================================================
# 输入处理
# ============================================================================
# 处理全局输入
func _input ( event : InputEvent ) - > void :
# 检查是否按下 Enter 键
if event is InputEventKey and event . pressed and not event . echo and ( event . keycode == KEY_ENTER or event . keycode == KEY_KP_ENTER ) :
if not ( event is InputEventKey ) :
return
var key_event : = event as InputEventKey
if not key_event . pressed or key_event . echo :
return
# T 键用于唤起聊天(输入框聚焦时不拦截)
if key_event . keycode == KEY_T and not chat_input . has_focus ( ) :
_handle_t_pressed ( )
return
# Enter 键用于发送聊天
if key_event . keycode == KEY_ENTER or key_event . keycode == KEY_KP_ENTER :
_handle_enter_pressed ( )
# 处理 T 键按下
func _handle_t_pressed ( ) - > void :
if not _is_chat_visible :
show_chat ( )
# 延迟聚焦,避免同一输入事件周期冲突
call_deferred ( " _grab_input_focus " )
# 处理 Enter 键按下
func _handle_enter_pressed ( ) - > void :
# 如果 聊天框未显示,显示它
# 聊天框未显示时不处理 Enter
if not _is_chat_visible :
show_chat ( )
# 使用 call_deferred 避免在同一个事件周期内触发 LineEdit 的 text_submitted 信号
call_deferred ( " _grab_input_focus " )
return
# 如果聊天框已显示且输入框有焦点,检查输入框内容
# 输入框有焦点时,发送逻辑交给 LineEdit 的 text_submitted 统一处理
if chat_input . has_focus ( ) :
# 如果输入框有内容,发送消息
if not chat_input . text . is_empty ( ) :
_on_send_button_pressed ( )
return
# 如果聊天框已显示但输入框无焦点,重新聚焦(取消倒计时)
@@ -136,11 +164,12 @@ func _gui_input(event: InputEvent) -> void:
# 处理点击聊天框外部区域
func _handle_click_outside ( ) - > void :
if not _is_chat_visible :
return
# 检查点击是否在聊天面板外部
if not chat_panel . get_global_rect ( ) . has_point ( get_global_mouse_position ( ) ) :
# 延迟释放输入框焦点,避免事件冲突
if chat_input . has_focus ( ) :
call_deferred ( " _release_input_focus " )
hide_chat ( )
# 延迟释放输入框焦点(由 call_deferred 调用)
func _release_input_focus ( ) - > void :
@@ -152,20 +181,32 @@ func _release_input_focus() -> void:
# ============================================================================
# 显示聊天框
func show_chat ( ) - > void :
func show_chat ( immediate : bool = false ) - > void :
_is_chat_visible = true
if is_instance_valid ( chat_panel ) :
chat_panel . show ( )
# 停止隐藏计时器
_stop_hide_timer ( )
if not is_instance_valid ( chat_panel ) :
return
_stop_transition_tween ( )
chat_panel . show ( )
if immediate :
chat_panel . modulate . a = 1.0
return
chat_panel . modulate . a = 0.0
_transition_tween = create_tween ( )
_transition_tween . set_trans ( Tween . TRANS_SINE ) . set_ease ( Tween . EASE_OUT )
_transition_tween . tween_property ( chat_panel , " modulate:a " , 1.0 , CHAT_TRANSITION_DURATION )
_transition_tween . finished . connect ( _on_show_transition_finished )
# 隐藏聊天框
func hide_chat ( ) - > void :
func hide_chat ( immediate : bool = false ) - > void :
_is_chat_visible = false
_is_typing = false
if is_instance_valid ( chat_panel ) :
chat_panel . hide ( )
if is_instance_valid ( chat_input ) and chat_input . has_focus ( ) :
chat_input . release_focus ( )
@@ -173,6 +214,25 @@ func hide_chat() -> void:
# 停止隐藏计时器
_stop_hide_timer ( )
if not is_instance_valid ( chat_panel ) :
return
_stop_transition_tween ( )
if immediate :
chat_panel . hide ( )
chat_panel . modulate . a = 1.0
return
if not chat_panel . visible :
chat_panel . modulate . a = 1.0
return
_transition_tween = create_tween ( )
_transition_tween . set_trans ( Tween . TRANS_SINE ) . set_ease ( Tween . EASE_IN )
_transition_tween . tween_property ( chat_panel , " modulate:a " , 0.0 , CHAT_TRANSITION_DURATION )
_transition_tween . finished . connect ( _on_hide_transition_finished )
# 创建隐藏计时器
func _create_hide_timer ( ) - > void :
_hide_timer = Timer . new ( )
@@ -202,6 +262,20 @@ func _stop_hide_timer() -> void:
func _on_hide_timeout ( ) - > void :
hide_chat ( )
func _on_show_transition_finished ( ) - > void :
_transition_tween = null
func _on_hide_transition_finished ( ) - > void :
_transition_tween = null
if not _is_chat_visible and is_instance_valid ( chat_panel ) :
chat_panel . hide ( )
chat_panel . modulate . a = 1.0
func _stop_transition_tween ( ) - > void :
if is_instance_valid ( _transition_tween ) :
_transition_tween . kill ( )
_transition_tween = null
# ============================================================================
# UI 事件处理
# ============================================================================
@@ -243,12 +317,30 @@ func _on_send_button_pressed() -> void:
# 清空输入框
chat_input . clear ( )
# 重新聚焦输入框
# 发送后延迟重新聚焦,避免被 LineEdit 的提交事件在同一帧内抢走焦点
call_deferred ( " _focus_input_after_send " )
func _focus_input_after_send ( ) - > void :
if not _is_chat_visible :
return
if not is_instance_valid ( chat_input ) :
return
chat_input . grab_focus ( )
# 聊天输入提交(回车键)处理
func _on_chat_input_submitted ( text : String ) - > void :
func _on_chat_input_submitted ( _ text: String ) - > void :
_on_send_button_pressed ( )
# 即便提交的是空串,也保持输入焦点,便于连续输入
call_deferred ( " _focus_input_after_send " )
func _enable_keep_editing_on_submit ( ) - > void :
if not is_instance_valid ( chat_input ) :
return
for property in chat_input . get_property_list ( ) :
if property . get ( " name " , " " ) == " keep_editing_on_text_submit " :
chat_input . set ( " keep_editing_on_text_submit " , true )
return
# ============================================================================
# 事件订阅( Call Down)
@@ -290,10 +382,11 @@ func _on_chat_error(data: Dictionary) -> void:
var error_code : String = data . get ( " error_code " , " " )
var message : String = data . get ( " message " , " " )
push_error ( " ChatUI: [ %s ] %s " % [ error_code , message ] )
# 聊天发送失败通常是业务校验(频率/内容)导致,不应按引擎级错误处理。
push_warning ( " ChatUI: [ %s ] %s " % [ error_code , message ] )
# 处理连接状态变化
func _on_connection_state_changed ( data : Dictionary ) - > void :
func _on_connection_state_changed ( _ data: Dictionary ) - > void :
# 连接状态变化处理( 当前不更新UI)
pass
@@ -319,7 +412,6 @@ func add_message_to_history(from_user: String, content: String, timestamp: float
# 每条消息用一行容器包起来,方便左右对齐且不挤在一起
var row : = HBoxContainer . new ( )
row . layout_mode = 2 # 让 VBoxContainer 接管布局,否则会重叠在同一位置
row . size_flags_horizontal = Control . SIZE_EXPAND_FILL
row . size_flags_vertical = Control . SIZE_SHRINK_BEGIN
row . alignment = BoxContainer . ALIGNMENT_END if is_self else BoxContainer . ALIGNMENT_BEGIN