using EasyTalk.Character; using EasyTalk.Controller; using EasyTalk.Nodes.Common; using EasyTalk.Nodes.Tags; using EasyTalk.Settings; using System.Collections; using System.Collections.Generic; #if TEXTMESHPRO_INSTALLED using TMPro; #endif using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; namespace EasyTalk.Display { /// /// This is an implementation of a conversation display which is used to display lines of dialogue to a player in addition to the name of the character who is speaking if desired. /// public class ConversationDisplay : DialoguePanel { #if TEXTMESHPRO_INSTALLED /// /// The text element used to display the character name. /// [Tooltip("The text element to use for displaying character names.")] [SerializeField] private TMP_Text textMeshProCharacterNameText; #endif /// /// The text element used to display the character name. /// [Tooltip("The text element to use for displaying character names.")] [SerializeField] private Text characterNameText; /// /// The background image of the character name display. /// [Tooltip("The background image used for the character name.")] [SerializeField] private Image characterNameBackgroundImage; #if TEXTMESHPRO_INSTALLED /// /// The text element used to display lines of dialogue. /// [Tooltip("The text element to use for displaying lines of dialogue.")] [SerializeField] private TMP_Text textMeshProConvoText; #endif /// /// The text element used to display lines of dialogue. /// [Tooltip("The text element to use for displaying lines of dialogue.")] [SerializeField] private Text convoText; /// /// The display mode of the dialogue text. /// [Tooltip("The way text should be displayed on the Conversation Display." + "\nFULL: Lines of dialogue will be shown in their entirety." + "\nBY_WORD: Lines of dialogue will be shown one word at a time." + "\nBY_CHARACTER: Lines of dialogue will be shown one character at a time.")] [SerializeField] private TextDisplayMode textDisplayMode = TextDisplayMode.FULL; /// /// When the text display mode is set to BY_WORD, this value controls how many words are displayed per second. /// [Tooltip("How many words from a line of dialogue should be displayed per second.")] [SerializeField] private float wordsPerSecond = 5.0f; /// /// When the text display mode is set to BY_CHARACTER, this value controls how many characters are displayed per second. /// [Tooltip("How many characters from a line of dialogue should be displayed per second.")] [SerializeField] private float charactersPerSecond = 20.0f; /// /// The images used to create the conversation display panel. /// [Tooltip("A list of images used in the Conversation Display.")] [NonReorderable] [SerializeField] private List conversationPanelImages = new List(); /// /// Whether the conversation display should be hidden on awake. /// [Tooltip("If set to true, the conversation display will be hidden when Awake() is called on the GameObject.")] [SerializeField] private bool hideOnAwake = true; /// /// The delimiter text to prepend whenever text is appended to the display. /// [SerializeField] private string appendDelimiter = " "; /// /// Listeners for conversation display events which are triggered whenever the character name is updated or the conversation text is updated. /// [Tooltip("A set of Conversation Display Listeners which listen for the character name or conversation text being set a conversation display.")] [NonReorderable] [SerializeField] private List conversationDisplayListeners = new List(); /// /// An event which is triggered when the character name is updated. /// [SerializeField] [HideInInspector] protected UnityEvent onCharacterNameUpdated = new UnityEvent(); /// /// An event which is triggered when the covnersation text is updated. /// [SerializeField] [HideInInspector] protected UnityEvent onConversationTextUpdated = new UnityEvent(); /// /// An event which is triggered whenever the display is cleared out, meaning the conversation text and character name are set to empty strings. /// [SerializeField] [HideInInspector] protected UnityEvent onReset = new UnityEvent(); /// /// An event which is triggered when the conversation text starts being displayed. /// [SerializeField] [HideInInspector] protected UnityEvent onTextDisplayStarted = new UnityEvent(); /// /// An event which is triggered when the conversation text has finished being displayed. /// [SerializeField] [HideInInspector] protected UnityEvent onTextDisplayCompleted = new UnityEvent(); /// /// The current line of dialogue being displayed by the conversation display. /// protected ConversationLine currentLine; /// /// The final target text to display on the conversation display. /// private string targetText = ""; /// /// The coroutine for displaying text over time. /// private Coroutine displayTextCoroutine; /// /// This setting determines how gibberish sounds are played. /// [Tooltip("Determines how gibberish sounds are played as dialogue text is being displayed." + "\nNONE: No gibberish sounsd will be played." + "\nCYCLE_RANDOM: Gibberish audio clips will be randomly selected and played consecutively until the text has finished displaying." + "\nLOOP_SINGLE: A random gibberish audio clip will be selected from the list, and looped until the text has finished displaying." + "\nPLAY_RANDOM_PER_ITEM: Each time a new character or word is displayed, a random gibberish sound will be selected from the list and played.")] [SerializeField] private GibberishMode gibberishMode = GibberishMode.NONE; /// /// The audio source to use when playing gibberish sounds. /// [SerializeField] private AudioSource gibberishAudioSource; /// /// A collection of audio clips to use for playing gibberish audio. /// [NonReorderable] [SerializeField] private List gibberishAudioClips = new List(); /// /// A temporary list of audio clips to use for character gibberish when a character doesn't override the defaults. /// private List defaultGibberishAudioClips = new List(); /// /// A flag which indicates whether the conversation display is currently processing one text item at a time, such as a word or character. /// private bool isPerItemDisplayOngoing = false; /// /// A coroutine for playing random gibberish sounds. /// private Coroutine gibberishCoroutine; /// /// The character library. /// private CharacterLibrary characterLibrary; /// /// Initializes the display and hides it on awake. /// private void Awake() { Init(); InitializeCharacterLibrary(); BackupDefaultGibberish(); if (hideOnAwake) { HideImmediately(); } } /// /// Activates the conversation text. /// public void ShowConversationText() { if (forceStandardText) { if (StandardConvoText != null) { StandardConvoText.gameObject.SetActive(true); } } else { #if TEXTMESHPRO_INSTALLED if (TMPConvoText != null) { TMPConvoText.gameObject.SetActive(true); } #else if (StandardConvoText != null) { StandardConvoText.gameObject.SetActive(true); } #endif } } /// /// Deactivates the conversation text. /// public virtual void HideConversationText() { if (forceStandardText) { if (StandardConvoText != null) { StandardConvoText.gameObject.SetActive(false); } } else { #if TEXTMESHPRO_INSTALLED if (TMPConvoText != null) { TMPConvoText.gameObject.SetActive(false); } #else if (StandardConvoText != null) { StandardConvoText.gameObject.SetActive(false); } #endif } } /// /// Activates the character name text. /// public void ShowCharacterName() { if(forceStandardText) { if (StandardCharacterNameText != null) { StandardCharacterNameText.gameObject.SetActive(true); } } else { #if TEXTMESHPRO_INSTALLED if (TMPCharacterNameText != null) { TMPCharacterNameText.gameObject.SetActive(true); } #else if (StandardCharacterNameText != null) { StandardCharacterNameText.gameObject.SetActive(true); } #endif } } /// /// Deactivates the character name text. /// public virtual void HideCharacterName() { if (forceStandardText) { if (StandardCharacterNameText != null) { StandardCharacterNameText.gameObject.SetActive(false); } } else { #if TEXTMESHPRO_INSTALLED if (TMPCharacterNameText != null) { TMPCharacterNameText.gameObject.SetActive(false); } #else if (StandardCharacterNameText != null) { StandardCharacterNameText.gameObject.SetActive(false); } #endif } } /// /// Sets the value of the conversation display dialogue text element. /// /// The text to use. public void SetConversationText(string text) { if (StandardConvoText != null) { StandardConvoText.text = text; } #if TEXTMESHPRO_INSTALLED if (TMPConvoText != null) { TMPConvoText.text = text; } #endif if (onConversationTextUpdated != null) { onConversationTextUpdated.Invoke(); } foreach(ConversationDisplayListener listener in conversationDisplayListeners) { listener.OnConversationTextUpdated(text); } } /// /// Forces the display to finish displaying its text. /// public void ForceFinish() { //Stop the coroutines and set the text to whatever it's supposed to be. if (displayTextCoroutine != null) { StopCoroutine(displayTextCoroutine); displayTextCoroutine = null; } isPerItemDisplayOngoing = false; StopGibberish(); SetConversationText(targetText); if (onTextDisplayCompleted != null) { onTextDisplayCompleted.Invoke(); } } /// /// Appends the line of dialogue provided to the current text of the conversation display. /// /// The line of dialogue to append to the display. public void AppendText(ConversationLine appendedLine) { if(appendedLine.AutoPlay) { float delay = CalculateAutoDelayForText(appendedLine.Text, appendedLine.PlayTime); displayTextCoroutine = StartCoroutine(AppendTextAsync(appendedLine.Text, delay)); } else { float delay = CalculateDelay(); displayTextCoroutine = StartCoroutine(AppendTextAsync(appendedLine.Text, delay)); } } /// /// Asynchronously appends the text specified to the currently displayed text. /// /// The text to append. /// private IEnumerator AppendTextAsync(string text, float delay = 0.0f) { string appendText = appendDelimiter + text; targetText += appendText; if (onTextDisplayStarted != null) { onTextDisplayStarted.Invoke(); } //Display the text in full immediately. if (TextPresentationMode == TextDisplayMode.FULL) { SetConversationText(appendText); if (onTextDisplayCompleted != null) { onTextDisplayCompleted.Invoke(); } yield break; } //Display the text, one word at a time. if (TextPresentationMode == TextDisplayMode.BY_WORD) { yield return SetConversationTextByWord(appendText, delay, true); } //Display the text, one character at time. else if (TextPresentationMode == TextDisplayMode.BY_CHARACTER) { yield return SetConversationTextByCharacter(appendText, delay, true); } } /// /// Returns the current text shown on the conversation display. /// /// The text currently shown on the display. private string GetConversationText() { if (StandardConvoText != null) { return StandardConvoText.text; } #if TEXTMESHPRO_INSTALLED if (TMPConvoText != null) { return TMPConvoText.text; } #endif return null; } /// /// Displays the dialogue text specified on the conversation display. /// /// The line of dialogue to display. public void SetConversationText(ConversationLine line) { if (displayTextCoroutine != null) { StopCoroutine(displayTextCoroutine); displayTextCoroutine = null; } displayTextCoroutine = StartCoroutine(SetConversationTextAsync(line)); } /// /// Asynchronously sets the conversation display's dialogue text based on the text display mode to the text of the line of dialogue provided. /// /// The line of dialogue to use when setting the displayed dialogue text. /// public IEnumerator SetConversationTextAsync(ConversationLine line) { this.currentLine = line; if (line.AutoPlay) { float delay = CalculateAutoDelayForText(line.Text, line.PlayTime); yield return SetConversationTextAsync(line.Text, delay); } else { float delay = CalculateDelay(); yield return SetConversationTextAsync(line.Text, delay); } } private float CalculateDelay() { float delay = 0.0f; if (TextPresentationMode == TextDisplayMode.BY_WORD) { delay = 1.0f / WordsPerSecond; } else if (TextPresentationMode == TextDisplayMode.BY_CHARACTER) { delay = 1.0f / CharactersPerSecond; } return delay; } private float CalculateAutoDelayForText(string text, float totalPlayTime) { float delay = 0.0f; if (TextPresentationMode == TextDisplayMode.BY_WORD) { string[] words = text.Split(' '); float desiredCompletionTime = WordsPerSecond * words.Length; if (desiredCompletionTime < totalPlayTime) { delay = 1.0f / WordsPerSecond; } else { float timingModifier = totalPlayTime / desiredCompletionTime; delay = WordsPerSecond * timingModifier; } } else if (TextPresentationMode == TextDisplayMode.BY_CHARACTER) { float desiredCompletionTime = CharactersPerSecond * text.Length; if (desiredCompletionTime < totalPlayTime) { delay = 1.0f / CharactersPerSecond; } else { float timingModifier = totalPlayTime / desiredCompletionTime; delay = CharactersPerSecond * timingModifier; } } return delay; } /// /// Asynchronously sets the displayed dialogue text based on the text display mode set on this conversation display. The delay is used to sync the timing of /// the text with the total time alloted to the line of dialogue (applies when the dialogue controller is in AUTOPLAY mode). /// /// The text to set. /// A delay added each time the text is updated when in BY_WORD or BY_CHARACTER mode. /// public IEnumerator SetConversationTextAsync(string text, float delay = 0.0f) { targetText = text; if (onTextDisplayStarted != null) { onTextDisplayStarted.Invoke(); } //If the text is blank, set it to an empty string on the convo display. if (text == null || text.Length == 0) { SetConversationText(""); if (onTextDisplayCompleted != null) { onTextDisplayCompleted.Invoke(); } yield break; } //Display the text in full immediately. if (TextPresentationMode == TextDisplayMode.FULL) { SetConversationText(text); if (onTextDisplayCompleted != null) { onTextDisplayCompleted.Invoke(); } yield break; } //Display the text, one word at a time. if (TextPresentationMode == TextDisplayMode.BY_WORD) { yield return SetConversationTextByWord(text, delay); } //Display the text, one character at time. else if (TextPresentationMode == TextDisplayMode.BY_CHARACTER) { yield return SetConversationTextByCharacter(text, delay); } } /// /// Updates the displayed dialogue text by displaying one word at a time, using a specific delay per word. /// /// The full text to display. /// The delay to use between words. /// Whether the specified text should be appended to the currently displayed text (default false). /// private IEnumerator SetConversationTextByWord(string text, float delay, bool append = false) { //Initialize variables, split text into individual words, and reset the convo display. isPerItemDisplayOngoing = true; int index = 0; string oldString = GetConversationText(); string currentString = ""; if(append) { currentString = oldString; } else { SetConversationText(currentString); } //Find any tags in the text so they can be applied as necessary. List tags = TMPTag.FindTags(text); List tagStack = new List(); int tagIdx = 0; int addedCharCount = 0; if(gibberishMode == GibberishMode.LOOP_SINGLE) { PlayRandomGibberish(true); } else if(gibberishMode == GibberishMode.CYCLE_RANDOM) { CycleRandomGibberish(); } while (index < text.Length) { if (gibberishMode == GibberishMode.PLAY_RANDOM_PER_ITEM) { PlayRandomGibberish(false); } yield return new WaitForSeconds(delay); //Remove any characters which were appended for tags. if (addedCharCount > 0) { currentString = currentString.Substring(0, currentString.Length - addedCharCount); } //Add any tags preceeding the next word to the current string. while (tagIdx < tags.Count && index < text.Length && index == tags[tagIdx].startIdx) { //Increase the index by the tag length and append it to the current string. index += tags[tagIdx].endIdx - tags[tagIdx].startIdx + 1; currentString += tags[tagIdx].fullTagText; //Add the tag to the stack if it's an opening tag. if (!tags[tagIdx].isCloseTag) { tagStack.Add(tags[tagIdx]); } else { //Remove tags from the stack if we're no longer looking for closing tags. for (int i = 0; i < tagStack.Count; i++) { TMPTag tag = tagStack[i]; if (tag.name.Equals(tags[tagIdx].name)) { tagStack.RemoveAt(i); i--; } } } tagIdx++; } //Append the next word to the string. while (index < text.Length && text[index] != '<' && text[index] != ' ') { currentString += text[index]; index++; } if (index < text.Length && text[index] == ' ') { currentString += " "; index++; } //Append closing tags to the string. addedCharCount = 0; foreach (TMPTag tag in tagStack) { string closeTag = ""; currentString += closeTag; addedCharCount += closeTag.Length; } //Display the current string. SetConversationText(currentString); } //Display the final text. if (append) { SetConversationText(oldString + text); } else { SetConversationText(text); } isPerItemDisplayOngoing = false; StopGibberish(); if (onTextDisplayCompleted != null) { onTextDisplayCompleted.Invoke(); } } /// /// Updates the displayed dialogue text by displaying one character at a time, using a specific delay per character. /// /// The full text to display. /// The delay to use between characters. /// Whether the specified text should be appended to the currently displayed text (default false). /// private IEnumerator SetConversationTextByCharacter(string text, float delay, bool append = false) { isPerItemDisplayOngoing = true; int index = 0; string currentString = ""; string oldString = GetConversationText(); if (append) { currentString = oldString + text[0]; } else { SetConversationText(currentString); } //Find any tags in the text so they can be applied as necessary. List tags = TMPTag.FindTags(text); List tagStack = new List(); int tagIdx = 0; int addedCharCount = 0; if (gibberishMode == GibberishMode.LOOP_SINGLE) { PlayRandomGibberish(true); } else if (gibberishMode == GibberishMode.CYCLE_RANDOM) { CycleRandomGibberish(); } while (index < text.Length) { if(gibberishMode == GibberishMode.PLAY_RANDOM_PER_ITEM) { PlayRandomGibberish(false); } yield return new WaitForSeconds(delay); if (addedCharCount > 0) { currentString = currentString.Substring(0, currentString.Length - addedCharCount); } //Add any tags preceeding the next word to the current string. while (tagIdx < tags.Count && index < text.Length && index == tags[tagIdx].startIdx) { //Increase the index by the tag length and append it to the current string. index += tags[tagIdx].endIdx - tags[tagIdx].startIdx + 1; currentString += tags[tagIdx].fullTagText; //Add the tag to the stack if it's an opening tag. if (!tags[tagIdx].isCloseTag) { tagStack.Add(tags[tagIdx]); } else { //Remove tags from thh stack if we're no longer looking for closing tags. for (int i = 0; i < tagStack.Count; i++) { TMPTag tag = tagStack[i]; if (tag.name.Equals(tags[tagIdx].name)) { tagStack.RemoveAt(i); i--; } } } tagIdx++; } if (index < text.Length) { currentString += "" + text[index]; } string tagStackString = ""; addedCharCount = 0; foreach (TMPTag tag in tagStack) { string closeTag = ""; currentString += closeTag; addedCharCount += closeTag.Length; tagStackString += tag.fullTagText + ", "; } SetConversationText(currentString); index++; } if (append) { SetConversationText(oldString + text); } else { SetConversationText(text); } isPerItemDisplayOngoing = false; StopGibberish(); if (onTextDisplayCompleted != null) { onTextDisplayCompleted.Invoke(); } } /// /// Stops any currently playing gibberish audio. /// private void StopGibberish() { if(gibberishCoroutine != null) { StopCoroutine(gibberishCoroutine); gibberishCoroutine = null; } if (gibberishAudioSource != null) { gibberishAudioSource.Stop(); } } /// /// Plays a random gibberish audio clip. /// /// Whether the audio should be looped. /// The AudioClip which is played. private AudioClip PlayRandomGibberish(bool loop) { if (gibberishAudioSource != null && gibberishAudioClips.Count > 0) { AudioClip clip = gibberishAudioClips[Random.Range(0, gibberishAudioClips.Count)]; gibberishAudioSource.clip = clip; gibberishAudioSource.loop = loop; gibberishAudioSource.Play(); return clip; } return null; } /// /// Starts a coroutine to cycle through random gibberish audio clips. After one audio clip finishes, another random one is selected and played. /// private void CycleRandomGibberish() { gibberishCoroutine = StartCoroutine(CycleGibberishAsync()); } /// /// Cycles through gibberish audio files randomly, playing each one until completion before switching to another. /// /// private IEnumerator CycleGibberishAsync() { while (isPerItemDisplayOngoing) { AudioClip clipToPlay = PlayRandomGibberish(false); if (clipToPlay != null) { yield return new WaitForSeconds(clipToPlay.length); } else { break; } } } /// /// Sets the character name text to display the provided character name. /// /// The character name to use on the display. /// The original character name (pre-translation). public void SetCharacterName(string characterName, string sourceName) { StandardCharacterNameText.text = characterName; #if TEXTMESHPRO_INSTALLED TMPCharacterNameText.text = characterName; #endif if (onCharacterNameUpdated != null) { onCharacterNameUpdated.Invoke(); } //Sets up the gibberish audio based on the character who is speaking. SetUpGibberishAudio(sourceName); foreach (ConversationDisplayListener listener in conversationDisplayListeners) { listener.OnCharacterNameUpdated(characterName, sourceName); } } /// /// Replaces the default gibberish audio files of the conversation display with those of the character specified, if the character is set to /// override the default gibberish audio with their own (in the character library). /// /// The name of the character to retrieve gibberish audio clips for. private void SetUpGibberishAudio(string characterName) { if (CharacterLibrary != null) { CharacterDefinition characterDefinition = CharacterLibrary.GetCharacterDefinition(characterName); if (characterDefinition != null && characterDefinition.OverrideDefaultGibberishAudio) { BackupDefaultGibberish(); gibberishAudioClips = characterDefinition.GibberishAudioClips; } else { RestoreDefaultGibberish(); } } else { RestoreDefaultGibberish(); } } /// /// Creates a backuup of the default gibberish audio files so that they can be restored when switching to other characters who /// may not support overriding gibberish audio. /// private void BackupDefaultGibberish() { defaultGibberishAudioClips = gibberishAudioClips; } /// /// Restores the default gibberish audio files. /// private void RestoreDefaultGibberish() { gibberishAudioClips = defaultGibberishAudioClips; } /// /// Resets the conversation display by setting the character name and conversation text to blank values. /// public void Reset() { targetText = ""; SetConversationText(""); SetCharacterName("", ""); if(onReset != null) { onReset.Invoke(); } foreach(ConversationDisplayListener listener in conversationDisplayListeners) { listener.OnReset(); } } /// /// Sets the character library from the Dialogue Registry. /// private void InitializeCharacterLibrary() { if (characterLibrary == null) { DialogueDisplay parentDisplay = DialogueDisplay.GetParentDialogueDisplay(this.gameObject); if (parentDisplay != null) { EasyTalkDialogueSettings settings = parentDisplay.DialogueSettings; if (settings != null && settings.DialogueRegistry != null) { characterLibrary = settings.DialogueRegistry.CharacterLibrary; } } } } /// /// Gets the Character Library. /// private CharacterLibrary CharacterLibrary { get { InitializeCharacterLibrary(); return this.characterLibrary; } } /// /// Gets or sets the standard Unity Text component of the display for the character name. /// public Text StandardCharacterNameText { get { return this.characterNameText; } set { this.characterNameText = value; } } #if TEXTMESHPRO_INSTALLED /// /// Gets or sets the TextMeshPro Text component of the display for the character name. /// public TMP_Text TMPCharacterNameText { get { return textMeshProCharacterNameText; } set { textMeshProCharacterNameText = value; } } #endif /// /// Gets the image used for the character name panel. /// public Image CharacterNameBackgroundImage { get { return characterNameBackgroundImage; } } /// /// Gets or sets the standard Unity Text component of the display. /// public Text StandardConvoText { get { return this.convoText; } set { this.convoText = value; } } #if TEXTMESHPRO_INSTALLED /// /// Gets or sets the TextMeshPro Text component of the display. /// public TMP_Text TMPConvoText { get { return textMeshProConvoText; } set { textMeshProConvoText = value; } } #endif /// /// Gets the List of images used for the conversation display panel. /// public List ConversationPanelImages { get { return conversationPanelImages; } } /// /// Gets or sets the display mode used for dialogue text. /// public TextDisplayMode TextPresentationMode { get { return textDisplayMode; } set { textDisplayMode = value; } } /// /// Gets or sets the number of words per second to display when displaying dialogue in BY_WORD mode. /// public float WordsPerSecond { get { return wordsPerSecond; } set { wordsPerSecond = value; } } /// /// Gets or sets the number of characters per second to display when displaying dialogue in BY_CHARACTER mode. /// public float CharactersPerSecond { get { return charactersPerSecond; } set { charactersPerSecond = value; } } /// /// Gets or sets the List of ConversationDisplayListeners to be called by the Conversation Display. /// public List ConversationDisplayListeners { get { return this.conversationDisplayListeners; } set { this.conversationDisplayListeners = value; } } /// /// Defines possible modes for displaying dialogue text. /// public enum TextDisplayMode { FULL, BY_WORD, BY_CHARACTER } /// /// Translates the conversation text and character name based on the current language set on EasyTalkGameState.Instance.Language. /// /// The Dialogue Controller being used. public virtual void TranslateText(DialogueController controller) { if (controller != null && currentLine != null) { string convoText = currentLine.PreTranslationText; string translatedText = controller.GetNodeHandler().Translate(convoText); if (this.StandardConvoText != null) { this.StandardConvoText.text = translatedText; } #if TEXTMESHPRO_INSTALLED if (this.TMPConvoText != null) { this.TMPConvoText.text = translatedText; } #endif string characterNameText = currentLine.OriginalCharacterName; string translatedCharacterName = controller.GetNodeHandler().Translate(characterNameText); if (this.StandardCharacterNameText != null) { this.StandardCharacterNameText.text = translatedCharacterName; } #if TEXTMESHPRO_INSTALLED if (this.TMPCharacterNameText != null) { this.TMPCharacterNameText.text = translatedCharacterName; } #endif } } private void OnValidate() { #if TEXTMESHPRO_INSTALLED if(this.TMPCharacterNameText == null) { TMP_Text[] tmpTextComponents = GetComponentsInChildren(true); foreach (TMP_Text txt in tmpTextComponents) { if (txt.gameObject.name.Contains("CharacterName") && txt.gameObject.name.Contains("TMP")) { this.TMPCharacterNameText = txt; break; } } } if(this.TMPConvoText == null) { TMP_Text[] tmpTextComponents = GetComponentsInChildren(true); foreach(TMP_Text txt in tmpTextComponents) { if(txt.gameObject.name.Contains("Convo") && txt.gameObject.name.Contains("TMP")) { this.TMPConvoText = txt; break; } } } #endif } } public enum GibberishMode { NONE, CYCLE_RANDOM, LOOP_SINGLE, PLAY_RANDOM_PER_ITEM } }