using System; using System.Collections; using TMPro; using UnityEngine; using UnityEngine.UI; namespace Dialogue { /// /// Display mode for the speech bubble text /// public enum TextDisplayMode { Instant, // Display all text at once Typewriter // Display text one character at a time } [AddComponentMenu("AppleHills/Dialogue/Speech Bubble")] public class SpeechBubble : MonoBehaviour { [SerializeField] private TextMeshProUGUI textDisplay; [SerializeField] private Image imageDisplay; // New field for displaying images [SerializeField] private TextDisplayMode displayMode = TextDisplayMode.Typewriter; [SerializeField] private float typewriterSpeed = 0.05f; // Time between characters in seconds [SerializeField] private AudioSource typingSoundSource; [SerializeField] private float typingSoundFrequency = 3; // Play sound every X characters [SerializeField] private bool useRichText = true; // Whether to respect rich text tags [SerializeField] private float dialogueDisplayTime = 1.5f; // Time in seconds to display dialogue before showing prompt [SerializeField] private string dialoguePromptText = ". . ."; // Text to show as a prompt for available dialogue private Coroutine typewriterCoroutine; private Coroutine promptUpdateCoroutine; private string currentFullText = string.Empty; private Sprite currentImage = null; private bool isVisible = false; private DialogueContentType currentContentType = DialogueContentType.Text; private void Awake() { // Ensure we have both components if (textDisplay == null) Debug.LogError("SpeechBubble: TextMeshProUGUI component is not assigned!"); if (imageDisplay == null) Debug.LogError("SpeechBubble: Image component is not assigned!"); } /// /// Show the speech bubble /// public void Show() { gameObject.SetActive(true); isVisible = true; } /// /// Hide the speech bubble /// public void Hide() { gameObject.SetActive(false); isVisible = false; // Stop any ongoing typewriter effect if (typewriterCoroutine != null) { StopCoroutine(typewriterCoroutine); typewriterCoroutine = null; } // Stop any prompt update coroutine if (promptUpdateCoroutine != null) { StopCoroutine(promptUpdateCoroutine); promptUpdateCoroutine = null; } } /// /// Toggle visibility of the speech bubble /// public void Toggle() { if (isVisible) Hide(); else Show(); } /// /// Set the text to display in the speech bubble /// /// Text to display public void SetText(string text) { if (textDisplay == null) { Debug.LogError("SpeechBubble: TextMeshProUGUI component is not assigned!"); return; } currentFullText = text; currentContentType = DialogueContentType.Text; // Stop any existing typewriter effect if (typewriterCoroutine != null) { StopCoroutine(typewriterCoroutine); typewriterCoroutine = null; } // Activate text display, deactivate image display textDisplay.gameObject.SetActive(true); if (imageDisplay != null) { imageDisplay.gameObject.SetActive(false); } // Display text based on the selected mode if (displayMode == TextDisplayMode.Instant) { textDisplay.text = text; } else // Typewriter mode { textDisplay.text = string.Empty; // Clear the text initially typewriterCoroutine = StartCoroutine(TypewriterEffect(text)); } // Make sure the bubble is visible when setting text if (!isVisible) Show(); } /// /// Display a dialogue line and handle prompt visibility afterward /// /// The dialogue line to display /// Whether there are more dialogue lines available public void DisplayDialogueLine(string line, bool hasMoreDialogue) { // Cancel any existing prompt update if (promptUpdateCoroutine != null) { StopCoroutine(promptUpdateCoroutine); promptUpdateCoroutine = null; } // Display the dialogue line if (!string.IsNullOrEmpty(line)) { SetText(line); // After a delay, update the prompt visibility promptUpdateCoroutine = StartCoroutine(UpdatePromptAfterDelay(hasMoreDialogue)); } else { // If no line to display, update prompt visibility immediately UpdatePromptVisibility(hasMoreDialogue); } } /// /// Update the speech bubble to either show a prompt or hide based on dialogue availability /// /// Whether dialogue is available public void UpdatePromptVisibility(bool hasDialogueAvailable) { if (hasDialogueAvailable) { Show(); SetText(dialoguePromptText); } else { Hide(); } } /// /// Coroutine to update the prompt visibility after a delay /// private IEnumerator UpdatePromptAfterDelay(bool hasMoreDialogue) { // Wait for the configured display time yield return new WaitForSeconds(dialogueDisplayTime); // Update the prompt visibility UpdatePromptVisibility(hasMoreDialogue); promptUpdateCoroutine = null; } /// /// Change the display mode /// /// New display mode public void SetDisplayMode(TextDisplayMode mode) { displayMode = mode; // If we're changing modes while text is displayed, refresh it if (!string.IsNullOrEmpty(currentFullText)) { SetText(currentFullText); } } /// /// Skip the typewriter effect and show the full text immediately /// public void SkipTypewriter() { if (typewriterCoroutine != null) { StopCoroutine(typewriterCoroutine); typewriterCoroutine = null; textDisplay.text = currentFullText; } } /// /// Set the speed of the typewriter effect /// /// Characters per second public void SetTypewriterSpeed(float charactersPerSecond) { if (charactersPerSecond <= 0) { Debug.LogError("SpeechBubble: Typewriter speed must be greater than 0!"); return; } typewriterSpeed = 1f / charactersPerSecond; } /// /// Coroutine that gradually reveals text one character at a time /// private IEnumerator TypewriterEffect(string text) { int visibleCount = 0; int characterCount = 0; while (visibleCount < text.Length) { // Skip rich text tags if enabled if (useRichText && visibleCount < text.Length && text[visibleCount] == '<') { // Find the end of the tag int tagEnd = text.IndexOf('>', visibleCount); if (tagEnd != -1) { // Include the entire tag at once visibleCount = tagEnd + 1; textDisplay.text = text.Substring(0, visibleCount); continue; } } // Reveal the next character visibleCount++; characterCount++; textDisplay.text = text.Substring(0, visibleCount); // Play typing sound at specified frequency if (typingSoundSource != null && characterCount % typingSoundFrequency == 0) { typingSoundSource.Play(); } yield return new WaitForSeconds(typewriterSpeed); } typewriterCoroutine = null; } /// /// Set the image to display in the speech bubble /// /// Sprite to display public void SetImage(Sprite sprite) { if (imageDisplay == null) { Debug.LogError("SpeechBubble: Image component is not assigned!"); return; } currentImage = sprite; currentContentType = DialogueContentType.Image; // Activate image display, set the sprite imageDisplay.gameObject.SetActive(true); imageDisplay.sprite = sprite; // Deactivate text display if (textDisplay != null) { textDisplay.gameObject.SetActive(false); } // Make sure the bubble is visible when setting image if (!isVisible) Show(); } /// /// Clear the displayed image /// public void ClearImage() { SetImage(null); } /// /// Set the content of the speech bubble (text or image) /// /// Text content /// Image content public void SetContent(string text, Sprite image) { if (!string.IsNullOrEmpty(text)) { currentContentType = DialogueContentType.Text; SetText(text); } else if (image != null) { currentContentType = DialogueContentType.Image; SetImage(image); } } /// /// Get the current content type of the speech bubble /// /// Current content type public DialogueContentType GetCurrentContentType() { return currentContentType; } /// /// Display dialogue content (text or image) /// /// The dialogue content to display /// Whether there are more dialogue content items available public void DisplayDialogueContent(DialogueContent content, bool hasMoreDialogue) { // Cancel any existing prompt update if (promptUpdateCoroutine != null) { StopCoroutine(promptUpdateCoroutine); promptUpdateCoroutine = null; } if (content == null) { UpdatePromptVisibility(hasMoreDialogue); return; } // Display the content based on its type currentContentType = content.ContentType; if (content.ContentType == DialogueContentType.Text) { // Show text display, hide image display textDisplay.gameObject.SetActive(true); if (imageDisplay != null) imageDisplay.gameObject.SetActive(false); // Display the text DisplayDialogueLine(content.Text, hasMoreDialogue); } else // Image content { // Hide text display, show image display textDisplay.gameObject.SetActive(false); if (imageDisplay != null) imageDisplay.gameObject.SetActive(true); // Set the image SetImage(content.Image); // After a delay, update the prompt visibility promptUpdateCoroutine = StartCoroutine(UpdatePromptAfterDelay(hasMoreDialogue)); } } } }