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
}
}
}