TH1/Unity/Assets/Scripts/TH1_UI/View/Common/UIChatAreaMono.cs

441 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using Logic.Audio;
using Logic.Multilingual;
using TH1_Logic.Chat;
using TH1_Logic.Net;
using TH1_Logic.Steam;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace TH1_UI.View.Common
{
/// <summary>
/// 通用聊天区域,由需要聊天功能的 UI 动态挂载。
/// 拉取模式:订阅 ChatManager.OnChatUpdated对比 _displayedCount 增量显示新消息。
/// </summary>
public class UIChatAreaMono : MonoBehaviour
{
// ---- 对外事件 ----
public Action<string> OnSendMessage;
// ---- 静态访问,供 InputLogic 查询/操作输入框 ----
public static bool IsInputFieldFocused => _instance != null
&& _instance._inputAreaVisible
&& _instance.TextInputArea != null
&& _instance.TextInputArea.isFocused;
public static bool IsInputAreaVisible => _instance != null
&& _instance._inputAreaVisible;
public static bool IsActive => _instance != null && _instance._initialized;
public static void HideInputArea()
{
if (_instance != null)
_instance.SetInputAreaVisible(false);
}
public static void ShowInputArea()
{
if (_instance != null)
_instance.SetInputAreaVisible(true);
}
public static void FocusInputField()
{
if (_instance != null && _instance.TextInputArea != null)
_instance.TextInputArea.ActivateInputField();
}
/// <summary>
/// Enter键专用有文字就发送没文字就关闭。
/// </summary>
public static void SendOrClose()
{
if (_instance == null) return;
if (_instance.TextInputArea == null) return;
string text = _instance.TextInputArea.text;
if (!string.IsNullOrWhiteSpace(text))
{
_instance.OnSendMessage?.Invoke(text.Trim());
_instance.TextInputArea.text = string.Empty;
_instance.TextInputArea.ActivateInputField();
}
else
{
_instance.SetInputAreaVisible(false);
}
}
private static UIChatAreaMono _instance;
// ---- Inspector 绑定 ----
[Header("ChatButton - 控制输入区显隐")]
public Button ChatButton;
[Header("ChatShowArea - 常驻消息显示区")]
public RectTransform ChatShowArea;
public GameObject ChatPrefab; // NetChat_ChatPrefab
[Header("ChatInputArea - 输入区")]
public GameObject ChatInputArea;
public TMP_InputField TextInputArea;
public Button SendButton;
[Header("DocArea - 历史聊天追溯")]
public Button DocButton;
public GameObject DocArea;
public TMP_Text DocText;
[Header("配置")]
public int MaxChatCount = 50;
public float MessageLifetime = 10f;
public int DocRecentCount = 50;
// ---- 内部数据 ----
private readonly List<ChatRowEntry> _chatRows = new List<ChatRowEntry>();
private bool _inputAreaVisible;
private bool _initialized;
private struct ChatRowEntry
{
public GameObject Go;
public float SpawnTime;
}
// ------------------------------------------------------------------
// Lifecycle
// ------------------------------------------------------------------
private void OnDestroy()
{
Shutdown();
if (_instance == this) _instance = null;
}
private void Update()
{
if (!_initialized || _chatRows.Count == 0) return;
// 从头部检查过期消息
float now = Time.time;
while (_chatRows.Count > 0)
{
var entry = _chatRows[0];
if (now - entry.SpawnTime < MessageLifetime) break;
_chatRows.RemoveAt(0);
if (entry.Go != null)
Destroy(entry.Go);
}
}
/// <summary>
/// 外部调用的初始化,跟随 NetInfo 一起触发。
/// ChatButton + ChatShowArea 显示并保持活跃InputArea 隐藏。
/// </summary>
public void Init()
{
if (_initialized) return;
_initialized = true;
_instance = this;
Debug.Log($"[ChatArea] Init: ChatPrefab={ChatPrefab != null}, ChatShowArea={ChatShowArea != null}, ChatButton={ChatButton != null}");
// 标记现有消息为已显示,不回显历史
foreach (var item in ChatManager.Instance.ChatItems)
item.AlreadyUIShow = true;
// 确保 ChatButton 和 ChatShowArea 可见
if (ChatButton != null)
ChatButton.gameObject.SetActive(true);
if (ChatShowArea != null)
ChatShowArea.gameObject.SetActive(true);
// 隐藏输入区
SetInputAreaVisible(false);
// 默认隐藏历史追溯区
SetDocAreaVisible(false);
// 设置输入框placeholder多语言文本
// 必须通过 SetUIText 设置 MultilingualTextMono 的 ID
// 否则 OnEnable 时 MultilingualTextMono 会用旧 ID 覆盖 text
if (TextInputArea != null && TextInputArea.placeholder is TextMeshProUGUI placeholderTmp)
MultilingualManager.Instance.SetUIText(placeholderTmp, Table.Instance.TextDataAssets.NetChatPlaceHolder);
// 绑定按钮事件
if (ChatButton != null)
ChatButton.onClick.AddListener(ToggleInputArea);
if (SendButton != null)
SendButton.onClick.AddListener(OnSendButtonClick);
if (TextInputArea != null)
TextInputArea.onSubmit.AddListener(OnInputSubmit);
if (DocButton != null)
DocButton.onClick.AddListener(ToggleDocArea);
// 订阅 ChatManager 变更回调
ChatManager.Instance.OnChatUpdated += RefreshChatContent;
}
/// <summary>
/// 关闭并清理,可由外部或 OnDestroy 调用。
/// </summary>
public void Shutdown()
{
if (!_initialized) return;
_initialized = false;
// 取消订阅
ChatManager.Instance.OnChatUpdated -= RefreshChatContent;
if (ChatButton != null)
ChatButton.onClick.RemoveListener(ToggleInputArea);
if (SendButton != null)
SendButton.onClick.RemoveListener(OnSendButtonClick);
if (TextInputArea != null)
TextInputArea.onSubmit.RemoveListener(OnInputSubmit);
if (DocButton != null)
DocButton.onClick.RemoveListener(ToggleDocArea);
ClearAll();
}
// ------------------------------------------------------------------
// Public API
// ------------------------------------------------------------------
/// <summary>
/// 添加一条系统消息不经过ChatManager直接显示
/// </summary>
public void AddSystemMessage(string message)
{
var data = new ChatMessageData
{
SenderName = string.Empty,
Message = message,
IsSelf = false,
IsSystem = true,
Timestamp = DateTime.Now
};
CreateChatRow(data);
}
/// <summary>
/// 清空所有已显示的消息行
/// </summary>
public void ClearAll()
{
for (int i = _chatRows.Count - 1; i >= 0; i--)
{
if (_chatRows[i].Go != null)
Destroy(_chatRows[i].Go);
}
_chatRows.Clear();
}
// ------------------------------------------------------------------
// Core: 拉取 ChatManager 增量刷新
// ------------------------------------------------------------------
private void RefreshChatBox()
{
if (!_initialized)
{
Debug.Log("[ChatArea] RefreshChatBox skipped: not initialized");
return;
}
var items = ChatManager.Instance.ChatItems;
var lobby = LobbyManager.Instance.Lobby;
if (lobby == null)
{
Debug.LogWarning("[ChatArea] RefreshChatBox skipped: lobby is null");
return;
}
ulong selfId = lobby.GetSelfMemberId();
int newCount = 0;
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
if (item.AlreadyUIShow) continue;
item.AlreadyUIShow = true;
newCount++;
// 解析发言者名称
var memberInfo = lobby.GetMemberInfo(item.SenderId);
string senderName = memberInfo?.Name;
if (string.IsNullOrEmpty(senderName))
senderName = $"玩家{item.SenderId}";
bool isSelf = item.SenderId == selfId;
var data = new ChatMessageData
{
SenderName = senderName,
Message = item.Message,
IsSelf = isSelf,
IsSystem = false,
Timestamp = DateTime.Now
};
CreateChatRow(data);
}
if (newCount > 0)
AudioManager.Instance.PlayAudio("SFX/UI_message");
Debug.Log($"[ChatArea] RefreshChatBox: {newCount} new messages, total items={items.Count}, rows={_chatRows.Count}");
}
private void RefreshChatContent()
{
RefreshChatBox();
if (DocArea != null && DocArea.activeSelf)
RefreshDocText();
}
// ------------------------------------------------------------------
// Internal
// ------------------------------------------------------------------
private void ToggleInputArea()
{
SetInputAreaVisible(!_inputAreaVisible);
}
private void SetInputAreaVisible(bool visible)
{
_inputAreaVisible = visible;
if (ChatInputArea != null)
ChatInputArea.SetActive(visible);
if (visible && TextInputArea != null)
TextInputArea.ActivateInputField();
}
private void ToggleDocArea()
{
if (DocArea == null) return;
bool willShow = !DocArea.activeSelf;
SetDocAreaVisible(willShow);
}
private void SetDocAreaVisible(bool visible)
{
if (DocArea != null)
DocArea.SetActive(visible);
if (visible)
RefreshDocText();
}
private void RefreshDocText()
{
if (DocText == null) return;
var items = ChatManager.Instance.ChatItems;
var lobby = LobbyManager.Instance?.Lobby;
int total = items.Count;
int start = Mathf.Max(0, total - DocRecentCount);
var sb = new System.Text.StringBuilder();
for (int i = start; i < total; i++)
{
var item = items[i];
string senderName = null;
if (lobby != null)
{
var memberInfo = lobby.GetMemberInfo(item.SenderId);
senderName = memberInfo?.Name;
}
if (string.IsNullOrEmpty(senderName))
senderName = $"玩家{item.SenderId}";
if (sb.Length > 0) sb.Append('\n');
sb.Append(FilterNameText(senderName)).Append(':').Append(FilterChatText(item.Message));
}
DocText.text = sb.ToString();
}
private void OnSendButtonClick()
{
TrySendMessage();
}
private void OnInputSubmit(string _)
{
TrySendMessage();
}
private void TrySendMessage()
{
if (TextInputArea == null) return;
string text = TextInputArea.text;
if (string.IsNullOrWhiteSpace(text)) return;
OnSendMessage?.Invoke(text.Trim());
TextInputArea.text = string.Empty;
TextInputArea.ActivateInputField();
}
private void CreateChatRow(ChatMessageData data)
{
if (ChatPrefab == null || ChatShowArea == null)
{
Debug.LogWarning($"[ChatArea] CreateChatRow failed: ChatPrefab={ChatPrefab != null}, ChatShowArea={ChatShowArea != null}");
return;
}
// 超出上限,移除最早的消息
while (_chatRows.Count >= MaxChatCount)
{
var oldest = _chatRows[0];
_chatRows.RemoveAt(0);
if (oldest.Go != null)
Destroy(oldest.Go);
}
var go = Instantiate(ChatPrefab, ChatShowArea);
// 拼接显示文本: "发言者:内容" 或系统消息直接显示内容
string displayMessage = FilterChatText(data.Message);
string displaySenderName = FilterNameText(data.SenderName);
string displayText = data.IsSystem
? displayMessage
: $"{displaySenderName}:{displayMessage}";
// 优先查找子物体上的 TMP其次根物体
var tmp = go.GetComponentInChildren<TMP_Text>();
if (tmp != null)
{
tmp.text = displayText;
}
_chatRows.Add(new ChatRowEntry { Go = go, SpawnTime = Time.time });
}
private static string FilterChatText(string text)
{
return BannedWordFilter.Filter(text, BannedTextContext.Chat);
}
private static string FilterNameText(string text)
{
return BannedWordFilter.Filter(text, BannedTextContext.Name);
}
}
/// <summary>
/// 单条聊天消息数据
/// </summary>
public class ChatMessageData
{
public string SenderName;
public string Message;
public bool IsSelf;
public bool IsSystem;
public DateTime Timestamp;
}
}