426 lines
14 KiB
C#
426 lines
14 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using XWorld.Framework.Protocol;
|
|
using UObject = UnityEngine.Object;
|
|
|
|
namespace XGame
|
|
{
|
|
public sealed class WorldChatModule : MonoBehaviour
|
|
{
|
|
private const string ChatPrefabPath = "Assets/Game/Art/UI/Prefab/UI_CommonChat.prefab";
|
|
private const int MaxMessages = 80;
|
|
private const int MaxInputLength = 200;
|
|
|
|
private GameObject view;
|
|
private GameObject panelCollapsed;
|
|
private GameObject panelChat;
|
|
private Transform contentMessages;
|
|
private GameObject otherTemplate;
|
|
private GameObject selfTemplate;
|
|
private TMP_InputField inputField;
|
|
private TMP_Text inputPlaceholder;
|
|
private TMP_Text inputValue;
|
|
private TMP_Text collapsedPreviewText;
|
|
private ScrollRect scrollMessages;
|
|
private bool opening;
|
|
private int openVersion;
|
|
private int localPlayerId;
|
|
private string collapsedDefaultText;
|
|
private string lastCollapsedPreview;
|
|
private readonly Queue<GameObject> messageRows = new Queue<GameObject>();
|
|
|
|
public Action<string> OnSendWorldChat;
|
|
|
|
public bool IsOpen => view != null;
|
|
|
|
public void Initialize(int playerId, Action<string> sendWorldChat)
|
|
{
|
|
localPlayerId = playerId;
|
|
OnSendWorldChat = sendWorldChat;
|
|
}
|
|
|
|
public IEnumerator OpenAsync()
|
|
{
|
|
if (view != null || opening)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
opening = true;
|
|
int version = ++openVersion;
|
|
UObject loaded = null;
|
|
yield return XResLoader.coLoadRes(ChatPrefabPath, typeof(GameObject), obj => loaded = obj);
|
|
opening = false;
|
|
|
|
if (version != openVersion)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
GameObject prefab = loaded as GameObject;
|
|
if (prefab == null)
|
|
{
|
|
Debug.LogError("[WorldChatModule] load chat prefab failed: " + ChatPrefabPath);
|
|
yield break;
|
|
}
|
|
|
|
view = Instantiate(prefab);
|
|
UIManager.AttachTo(UILayer.Normal, view);
|
|
CacheControls();
|
|
BindEvents();
|
|
Collapse();
|
|
}
|
|
|
|
public void Close()
|
|
{
|
|
openVersion++;
|
|
if (view != null)
|
|
{
|
|
Destroy(view);
|
|
view = null;
|
|
}
|
|
messageRows.Clear();
|
|
panelCollapsed = null;
|
|
panelChat = null;
|
|
contentMessages = null;
|
|
otherTemplate = null;
|
|
selfTemplate = null;
|
|
inputField = null;
|
|
inputPlaceholder = null;
|
|
inputValue = null;
|
|
collapsedPreviewText = null;
|
|
scrollMessages = null;
|
|
collapsedDefaultText = null;
|
|
lastCollapsedPreview = null;
|
|
opening = false;
|
|
}
|
|
|
|
public void HandleWorldChatMessage(WorldChatMessageMsg message)
|
|
{
|
|
if (message == null)
|
|
{
|
|
return;
|
|
}
|
|
if (view == null)
|
|
{
|
|
XWCoroutine.StartCo(OpenThenAppend(message));
|
|
return;
|
|
}
|
|
AppendMessage(message);
|
|
}
|
|
|
|
private IEnumerator OpenThenAppend(WorldChatMessageMsg message)
|
|
{
|
|
yield return OpenAsync();
|
|
AppendMessage(message);
|
|
}
|
|
|
|
private void CacheControls()
|
|
{
|
|
if (view == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
panelCollapsed = FindGameObject("Panel_Collapsed");
|
|
panelChat = FindGameObject("Panel_Chat");
|
|
contentMessages = FindTransform("Content_Messages");
|
|
otherTemplate = FindGameObject("Item_MessageOther_Template");
|
|
selfTemplate = FindGameObject("Item_MessageSelf_Template");
|
|
scrollMessages = FindComponent<ScrollRect>("Scroll_Messages");
|
|
inputPlaceholder = FindComponent<TMP_Text>("Txt_InputPlaceholder");
|
|
inputValue = FindComponent<TMP_Text>("Txt_InputValue");
|
|
collapsedPreviewText = FindComponent<TMP_Text>("Txt_CollapsedPlaceholder");
|
|
if (collapsedPreviewText != null && string.IsNullOrEmpty(collapsedDefaultText))
|
|
{
|
|
collapsedDefaultText = collapsedPreviewText.text;
|
|
}
|
|
RefreshCollapsedPreview();
|
|
EnsureInputField();
|
|
}
|
|
|
|
private void BindEvents()
|
|
{
|
|
BindButton("Btn_CollapsedInput", Expand);
|
|
BindButton("Btn_Close", Collapse);
|
|
BindButton("Btn_Send", SendCurrentInput);
|
|
BindButton("Btn_ClearInput", ClearInput);
|
|
BindButton("InputHitArea", FocusInput);
|
|
if (inputField != null)
|
|
{
|
|
inputField.onSubmit.RemoveListener(OnInputSubmitted);
|
|
inputField.onSubmit.AddListener(OnInputSubmitted);
|
|
inputField.onEndEdit.RemoveListener(OnInputEndEdit);
|
|
inputField.onEndEdit.AddListener(OnInputEndEdit);
|
|
}
|
|
}
|
|
|
|
private void EnsureInputField()
|
|
{
|
|
Transform hitArea = FindTransform("InputHitArea");
|
|
if (hitArea == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
inputField = hitArea.GetComponent<TMP_InputField>();
|
|
if (inputField == null)
|
|
{
|
|
inputField = hitArea.gameObject.AddComponent<TMP_InputField>();
|
|
}
|
|
|
|
Button button = hitArea.GetComponent<Button>();
|
|
if (button != null)
|
|
{
|
|
button.onClick.RemoveAllListeners();
|
|
button.onClick.AddListener(FocusInput);
|
|
}
|
|
|
|
TextMeshProUGUI textComponent = EnsureInputText(hitArea, "Txt_InputValue", ref inputValue, string.Empty, "#23384CFF");
|
|
TextMeshProUGUI placeholderComponent = EnsureInputText(hitArea, "Txt_InputPlaceholder", ref inputPlaceholder, "输入聊天内容...", "#7A8DA1FF");
|
|
if (textComponent == null)
|
|
{
|
|
Debug.LogError("[WorldChatModule] cannot create input text component.");
|
|
inputField = null;
|
|
return;
|
|
}
|
|
|
|
inputField.textComponent = textComponent;
|
|
inputField.placeholder = placeholderComponent;
|
|
inputField.targetGraphic = hitArea.GetComponent<Image>();
|
|
inputField.characterLimit = MaxInputLength;
|
|
inputField.lineType = TMP_InputField.LineType.SingleLine;
|
|
inputField.contentType = TMP_InputField.ContentType.Standard;
|
|
inputField.onValueChanged.RemoveListener(OnInputValueChanged);
|
|
inputField.onValueChanged.AddListener(OnInputValueChanged);
|
|
OnInputValueChanged(inputField.text);
|
|
}
|
|
|
|
private TextMeshProUGUI EnsureInputText(Transform parent, string nodeName, ref TMP_Text cached, string defaultText, string color)
|
|
{
|
|
bool created = false;
|
|
TextMeshProUGUI text = cached as TextMeshProUGUI;
|
|
if (text == null)
|
|
{
|
|
Transform existing = FindTransform(nodeName);
|
|
text = existing == null ? null : existing.GetComponent<TextMeshProUGUI>();
|
|
}
|
|
if (text == null)
|
|
{
|
|
GameObject go = new GameObject(nodeName, typeof(RectTransform), typeof(TextMeshProUGUI));
|
|
go.transform.SetParent(parent, false);
|
|
RectTransform rt = go.GetComponent<RectTransform>();
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.pivot = new Vector2(0.5f, 0.5f);
|
|
rt.offsetMin = new Vector2(76f, 0f);
|
|
rt.offsetMax = new Vector2(-72f, 0f);
|
|
text = go.GetComponent<TextMeshProUGUI>();
|
|
created = true;
|
|
text.fontSize = 28f;
|
|
text.alignment = TextAlignmentOptions.Left;
|
|
text.raycastTarget = false;
|
|
if (ColorUtility.TryParseHtmlString(color, out Color parsed))
|
|
{
|
|
text.color = parsed;
|
|
}
|
|
}
|
|
if (created)
|
|
{
|
|
text.text = defaultText ?? string.Empty;
|
|
}
|
|
cached = text;
|
|
return text;
|
|
}
|
|
|
|
private void Expand()
|
|
{
|
|
if (panelCollapsed != null) panelCollapsed.SetActive(false);
|
|
if (panelChat != null) panelChat.SetActive(true);
|
|
FocusInput();
|
|
}
|
|
|
|
private void Collapse()
|
|
{
|
|
if (panelCollapsed != null) panelCollapsed.SetActive(true);
|
|
if (panelChat != null) panelChat.SetActive(false);
|
|
}
|
|
|
|
private void FocusInput()
|
|
{
|
|
if (inputField == null)
|
|
{
|
|
return;
|
|
}
|
|
inputField.Select();
|
|
inputField.ActivateInputField();
|
|
}
|
|
|
|
private void ClearInput()
|
|
{
|
|
if (inputField != null)
|
|
{
|
|
inputField.text = string.Empty;
|
|
}
|
|
}
|
|
|
|
private void OnInputSubmitted(string text)
|
|
{
|
|
SendCurrentInput();
|
|
}
|
|
|
|
private void OnInputEndEdit(string text)
|
|
{
|
|
SendCurrentInput();
|
|
}
|
|
|
|
private void SendCurrentInput()
|
|
{
|
|
string text = inputField == null ? string.Empty : inputField.text;
|
|
text = (text ?? string.Empty).Trim();
|
|
if (text.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
if (text.Length > MaxInputLength)
|
|
{
|
|
text = text.Substring(0, MaxInputLength);
|
|
}
|
|
Debug.Log("[WorldChatModule] send world chat length=" + text.Length);
|
|
OnSendWorldChat?.Invoke(text);
|
|
ClearInput();
|
|
FocusInput();
|
|
}
|
|
|
|
private void OnInputValueChanged(string text)
|
|
{
|
|
if (inputPlaceholder != null)
|
|
{
|
|
inputPlaceholder.gameObject.SetActive(string.IsNullOrEmpty(text));
|
|
}
|
|
}
|
|
|
|
private void AppendMessage(WorldChatMessageMsg message)
|
|
{
|
|
CacheControls();
|
|
if (contentMessages == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool isSelf = message.PlayerId == localPlayerId;
|
|
GameObject template = isSelf ? selfTemplate : otherTemplate;
|
|
if (template == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GameObject row = Instantiate(template, contentMessages);
|
|
row.name = isSelf ? "Item_MessageSelf" : "Item_MessageOther";
|
|
row.SetActive(true);
|
|
if (isSelf)
|
|
{
|
|
SetChildText(row.transform, "Txt_SelfMessage", message.Text);
|
|
}
|
|
else
|
|
{
|
|
SetChildText(row.transform, "Txt_OtherName", string.IsNullOrEmpty(message.PlayerName) ? "玩家" : message.PlayerName);
|
|
SetChildText(row.transform, "Txt_OtherMessage", message.Text);
|
|
}
|
|
|
|
messageRows.Enqueue(row);
|
|
while (messageRows.Count > MaxMessages)
|
|
{
|
|
GameObject old = messageRows.Dequeue();
|
|
if (old != null)
|
|
{
|
|
Destroy(old);
|
|
}
|
|
}
|
|
|
|
if (scrollMessages != null)
|
|
{
|
|
StartCoroutine(ScrollToBottomNextFrame());
|
|
}
|
|
SetCollapsedPreview(message, isSelf);
|
|
}
|
|
|
|
private void SetCollapsedPreview(WorldChatMessageMsg message, bool isSelf)
|
|
{
|
|
string name = isSelf ? "\u6211" : message.PlayerName;
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
name = "\u73A9\u5BB6";
|
|
}
|
|
lastCollapsedPreview = name + ": " + (message.Text ?? string.Empty);
|
|
RefreshCollapsedPreview();
|
|
}
|
|
|
|
private void RefreshCollapsedPreview()
|
|
{
|
|
if (collapsedPreviewText != null)
|
|
{
|
|
collapsedPreviewText.text = string.IsNullOrEmpty(lastCollapsedPreview)
|
|
? (collapsedDefaultText ?? string.Empty)
|
|
: lastCollapsedPreview;
|
|
}
|
|
}
|
|
|
|
private IEnumerator ScrollToBottomNextFrame()
|
|
{
|
|
yield return null;
|
|
Canvas.ForceUpdateCanvases();
|
|
if (scrollMessages != null)
|
|
{
|
|
scrollMessages.verticalNormalizedPosition = 0f;
|
|
}
|
|
}
|
|
|
|
private void BindButton(string nodeName, UnityEngine.Events.UnityAction action)
|
|
{
|
|
Button button = FindComponent<Button>(nodeName);
|
|
if (button == null)
|
|
{
|
|
return;
|
|
}
|
|
button.onClick.RemoveAllListeners();
|
|
button.onClick.AddListener(action);
|
|
}
|
|
|
|
private GameObject FindGameObject(string nodeName)
|
|
{
|
|
Transform t = FindTransform(nodeName);
|
|
return t == null ? null : t.gameObject;
|
|
}
|
|
|
|
private Transform FindTransform(string nodeName)
|
|
{
|
|
return view == null ? null : view.transform.FindTransform(nodeName);
|
|
}
|
|
|
|
private T FindComponent<T>(string nodeName) where T : Component
|
|
{
|
|
Transform t = FindTransform(nodeName);
|
|
return t == null ? null : t.GetComponent<T>();
|
|
}
|
|
|
|
private static void SetChildText(Transform root, string childName, string text)
|
|
{
|
|
Transform t = root.FindTransform(childName);
|
|
if (t == null)
|
|
{
|
|
return;
|
|
}
|
|
TMP_Text label = t.GetComponent<TMP_Text>();
|
|
if (label != null)
|
|
{
|
|
label.text = text ?? string.Empty;
|
|
}
|
|
}
|
|
}
|
|
}
|