Files
AppleHillsProduction/Assets/Scripts/Dialogue/SpeechBubble.cs

430 lines
15 KiB
C#
Raw Normal View History

using System;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Dialogue
{
/// <summary>
/// Display mode for the speech bubble text
/// </summary>
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;
2025-10-17 15:35:25 +02:00
[SerializeField] private Image imageDisplay; // For displaying images in dialogue
[SerializeField] private Image dialoguePromptImage; // NEW: Reference to the dialogue prompt image
[SerializeField] private GameObject dialogueBubble; // NEW: Reference to the dialogue bubble container
[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
private Coroutine typewriterCoroutine;
private Coroutine promptUpdateCoroutine;
private string currentFullText = string.Empty;
private Sprite currentImage = null;
private bool isVisible = false;
private DialogueContentType currentContentType = DialogueContentType.Text;
2025-10-17 15:35:25 +02:00
private bool isPromptVisible = false; // Track if we're showing the prompt or dialogue
private void Awake()
{
2025-10-17 15:35:25 +02:00
// Ensure we have the required components
if (textDisplay == null)
Debug.LogError("SpeechBubble: TextMeshProUGUI component is not assigned!");
if (imageDisplay == null)
2025-10-17 15:35:25 +02:00
Debug.LogError("SpeechBubble: Image component for dialogue is not assigned!");
if (dialoguePromptImage == null)
Debug.LogError("SpeechBubble: Dialogue prompt image is not assigned!");
if (dialogueBubble == null)
Debug.LogError("SpeechBubble: Dialogue bubble container is not assigned!");
}
/// <summary>
/// Show the speech bubble
/// </summary>
public void Show()
{
2025-10-17 15:35:25 +02:00
// If we're showing the prompt, we only activate the prompt image
if (isPromptVisible)
{
dialogueBubble.SetActive(false);
dialoguePromptImage.gameObject.SetActive(true);
}
else // Otherwise, show the dialogue bubble
{
dialogueBubble.SetActive(true);
dialoguePromptImage.gameObject.SetActive(false);
}
isVisible = true;
}
/// <summary>
2025-10-17 15:35:25 +02:00
/// Hide the speech bubble and prompt
/// </summary>
public void Hide()
{
2025-10-17 15:35:25 +02:00
dialogueBubble.SetActive(false);
dialoguePromptImage.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;
}
}
/// <summary>
/// Toggle visibility of the speech bubble
/// </summary>
public void Toggle()
{
if (isVisible)
Hide();
else
Show();
}
/// <summary>
/// Set the text to display in the speech bubble
/// </summary>
/// <param name="text">Text to display</param>
public void SetText(string text)
{
if (textDisplay == null)
{
Debug.LogError("SpeechBubble: TextMeshProUGUI component is not assigned!");
return;
}
currentFullText = text;
currentContentType = DialogueContentType.Text;
2025-10-17 15:35:25 +02:00
isPromptVisible = false; // We're showing dialogue, not a prompt
// Stop any existing typewriter effect
if (typewriterCoroutine != null)
{
StopCoroutine(typewriterCoroutine);
typewriterCoroutine = null;
}
2025-10-17 15:35:25 +02:00
// Activate text display, deactivate image display within the dialogue bubble
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();
}
/// <summary>
/// Display a dialogue line and handle prompt visibility afterward
/// </summary>
/// <param name="line">The dialogue line to display</param>
/// <param name="hasMoreDialogue">Whether there are more dialogue lines available</param>
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))
{
2025-10-17 15:35:25 +02:00
isPromptVisible = false; // We're showing dialogue content
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);
}
}
/// <summary>
2025-10-17 15:35:25 +02:00
/// Update to either show the dialogue prompt image or hide based on dialogue availability
/// </summary>
/// <param name="hasDialogueAvailable">Whether dialogue is available</param>
public void UpdatePromptVisibility(bool hasDialogueAvailable)
{
if (hasDialogueAvailable)
{
2025-10-17 15:35:25 +02:00
isPromptVisible = true; // We're showing the prompt, not dialogue
// Hide dialogue bubble, show prompt image
dialogueBubble.SetActive(false);
dialoguePromptImage.gameObject.SetActive(true);
isVisible = true;
}
else
{
2025-10-17 15:35:25 +02:00
Hide(); // Hide both bubble and prompt
}
}
/// <summary>
/// Coroutine to update the prompt visibility after a delay
/// </summary>
private IEnumerator UpdatePromptAfterDelay(bool hasMoreDialogue)
{
// Wait for the configured display time
yield return new WaitForSeconds(dialogueDisplayTime);
// Update the prompt visibility
UpdatePromptVisibility(hasMoreDialogue);
promptUpdateCoroutine = null;
}
/// <summary>
/// Change the display mode
/// </summary>
/// <param name="mode">New display mode</param>
public void SetDisplayMode(TextDisplayMode mode)
{
displayMode = mode;
// If we're changing modes while text is displayed, refresh it
if (!string.IsNullOrEmpty(currentFullText))
{
SetText(currentFullText);
}
}
/// <summary>
/// Skip the typewriter effect and show the full text immediately
/// </summary>
public void SkipTypewriter()
{
if (typewriterCoroutine != null)
{
StopCoroutine(typewriterCoroutine);
typewriterCoroutine = null;
textDisplay.text = currentFullText;
}
}
/// <summary>
/// Set the speed of the typewriter effect
/// </summary>
/// <param name="charactersPerSecond">Characters per second</param>
public void SetTypewriterSpeed(float charactersPerSecond)
{
if (charactersPerSecond <= 0)
{
Debug.LogError("SpeechBubble: Typewriter speed must be greater than 0!");
return;
}
typewriterSpeed = 1f / charactersPerSecond;
}
/// <summary>
/// Coroutine that gradually reveals text one character at a time
/// </summary>
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;
}
/// <summary>
/// Set the image to display in the speech bubble
/// </summary>
/// <param name="sprite">Sprite to display</param>
public void SetImage(Sprite sprite)
{
if (imageDisplay == null)
{
Debug.LogError("SpeechBubble: Image component is not assigned!");
return;
}
currentImage = sprite;
currentContentType = DialogueContentType.Image;
2025-10-17 15:35:25 +02:00
isPromptVisible = false; // We're showing dialogue content, not a prompt
// 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();
}
/// <summary>
/// Clear the displayed image
/// </summary>
public void ClearImage()
{
SetImage(null);
}
/// <summary>
/// Set the content of the speech bubble (text or image)
/// </summary>
/// <param name="text">Text content</param>
/// <param name="image">Image content</param>
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);
}
}
/// <summary>
/// Get the current content type of the speech bubble
/// </summary>
/// <returns>Current content type</returns>
public DialogueContentType GetCurrentContentType()
{
return currentContentType;
}
/// <summary>
/// Display dialogue content (text or image)
/// </summary>
/// <param name="content">The dialogue content to display</param>
/// <param name="hasMoreDialogue">Whether there are more dialogue content items available</param>
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
2025-10-17 15:35:25 +02:00
isPromptVisible = false; // We're showing dialogue content
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);
2025-10-17 15:35:25 +02:00
// Show dialogue bubble, hide prompt
dialogueBubble.SetActive(true);
dialoguePromptImage.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);
2025-10-17 15:35:25 +02:00
// Show dialogue bubble, hide prompt
dialogueBubble.SetActive(true);
dialoguePromptImage.gameObject.SetActive(false);
// Set the image
SetImage(content.Image);
// After a delay, update the prompt visibility
promptUpdateCoroutine = StartCoroutine(UpdatePromptAfterDelay(hasMoreDialogue));
}
}
}
}