using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UnityEngine.UI; using ColorUtility = UnityEngine.ColorUtility; namespace Convai.Scripts.Runtime.UI { /// /// Manages the chat UI for displaying messages from characters and the player. /// public class ChatBoxUI : ChatUIBase { private const int MAX_MESSAGES = 25; [SerializeField] private GameObject _playerMessageObject, _characterMessageObject; private readonly List _messageList = new(); private GameObject _chatPanel; private ScrollRect _chatScrollRect; private Speaker _currentSpeaker; private bool _isFirstMessage = true; private bool _isNewMessage; /// /// Initializes the chat UI with the specified prefab. /// /// The UI prefab to instantiate. public override void Initialize(GameObject uiPrefab) { UIInstance = Instantiate(uiPrefab); _chatPanel = UIInstance.transform.GetChild(0).GetChild(0).GetChild(0).GetChild(0).gameObject; _chatScrollRect = UIInstance.transform.GetChild(0).GetChild(0).GetComponent(); _isFirstMessage = true; _currentSpeaker = Speaker.Player; } /// /// Sends a message as a character. /// /// The name of the character. /// The message text. /// The color of the character's text. public override void SendCharacterText(string charName, string text, Color characterTextColor) { BroadcastCharacterDialogue(charName, text, characterTextColor); } /// /// Sends a message as the player. /// /// The name of the player. /// The message text. /// The color of the player's text. public override void SendPlayerText(string playerName, string text, Color playerTextColor) { BroadcastPlayerDialogue(playerName, text, playerTextColor); } /// /// Clears all messages from the UI. /// public void ClearUI() { foreach (Message message in _messageList) { Destroy(message.SenderTextObject.gameObject); Destroy(message.MessageTextObject.gameObject); } _messageList.Clear(); } // Helper methods and private functions are below. These are not part of the public API // and are used internally by the ChatBoxUI class to manage chat messages. /// /// Broadcasts a dialogue message from a character. /// /// Name of the character. /// Text of the dialogue message. /// Color of the character's text. private void BroadcastCharacterDialogue(string characterName, string text, Color characterTextColor) { string trimmedText = text.Trim(); if (_currentSpeaker != Speaker.Character || _isFirstMessage) { _isFirstMessage = false; _currentSpeaker = Speaker.Character; HandleNewCharacterMessage(characterName, trimmedText, characterTextColor); } else { AppendToExistingMessage(trimmedText); } _currentSpeaker = Speaker.Character; ScrollToBottom(); } /// /// Handles a new dialogue message from a character. /// /// Name of the character. /// Text of the dialogue message. /// Color of the character's text. private void HandleNewCharacterMessage(string characterName, string text, Color characterTextColor) { if (_messageList.Count >= MAX_MESSAGES) DestroyOldestMessage(); CreateNewMessage(text, characterName, characterTextColor, false); } /// /// Broadcasts a dialogue message from a player. /// /// Name of the player. /// Text of the dialogue message. /// Color of the player's text. private void BroadcastPlayerDialogue(string playerName, string text, Color playerTextColor) { string trimmedText = text.Trim(); if (_currentSpeaker != Speaker.Player || !_messageList.Any()) { _currentSpeaker = Speaker.Player; HandleNewPlayerMessage(playerName, trimmedText, playerTextColor); } else { ReplaceExistingPlayerMessage(playerName, trimmedText, playerTextColor); } _currentSpeaker = Speaker.Player; ScrollToBottom(); } /// /// Handles a new dialogue message from a player. /// /// Name of the player. /// Text of the dialogue message. /// Color of the player's text. private void HandleNewPlayerMessage(string playerName, string text, Color playerTextColor) { if (_messageList.Count >= MAX_MESSAGES) DestroyOldestMessage(); CreateNewMessage(text, playerName, playerTextColor, true); } /// /// Replaces an existing player message with a new one. /// /// Name of the player. /// New text of the dialogue message. /// Color of the player's text. private void ReplaceExistingPlayerMessage(string playerName, string text, Color playerTextColor) { Message lastMessage = _messageList[^1]; lastMessage.MessageTextObject.text = text; lastMessage.SenderTextObject.text = FormatSpeakerName(playerName, playerTextColor); // RTL Update done due to text arriving after create message // Once for every message if (text != "" && _isNewMessage) { lastMessage.RTLUpdate(); _isNewMessage = false; } } /// /// Appends a text to the existing message. /// /// Text which needs to append to the existing message. private void AppendToExistingMessage(string text) { if (_messageList.Count > 0) { Message lastMessage = _messageList[^1]; lastMessage.MessageTextObject.text += " " + text; if (text != "" && _isNewMessage) { lastMessage.RTLUpdate(); _isNewMessage = false; } } } /// /// private void ScrollToBottom() { LayoutRebuilder.ForceRebuildLayoutImmediate(_chatPanel.GetComponent()); Canvas.ForceUpdateCanvases(); _chatScrollRect.verticalNormalizedPosition = 0f; } /// /// Formats the speaker name /// /// Name of the speaker. /// Color of the speaker's text. /// Formatted speaker's name with color tag. private static string FormatSpeakerName(string speakerName, Color speakerColor) { string speakerColorHtml = ColorUtility.ToHtmlStringRGB(speakerColor); return $"{speakerName}"; } /// /// Destroys the oldest message in the chat UI. /// private void DestroyOldestMessage() { Destroy(_messageList[0].SenderTextObject.gameObject); Destroy(_messageList[0].MessageTextObject.gameObject); _messageList.RemoveAt(0); } /// /// Creates a new dialogue message. /// /// Text of the dialogue message. /// Name of the speaker. /// Color of the speaker's text. /// Flag to check if the speaker is a player. private void CreateNewMessage(string text, string speakerName, Color speakerColor, bool isSpeakerPlayer) { _isNewMessage = true; GameObject messageInstance = isSpeakerPlayer ? Instantiate(_playerMessageObject, _chatPanel.transform) : Instantiate(_characterMessageObject, _chatPanel.transform); messageInstance.SetActive(true); Transform container = messageInstance.transform.Find("Container"); Transform senderBox = container.Find("SenderBox"); Message newMessage = new() { SenderTextObject = senderBox.Find("Sender").GetComponent(), MessageTextObject = container.Find("Message").GetComponent() }; newMessage.SenderTextObject.text = FormatSpeakerName(speakerName, speakerColor); newMessage.MessageTextObject.text = text; _messageList.Add(newMessage); } /// /// Enumeration of the possible speakers. /// private enum Speaker { Player, Character } } }