393 lines
13 KiB
C#
393 lines
13 KiB
C#
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("Apple Hills/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!");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Show the speech bubble
|
|
/// </summary>
|
|
public void Show()
|
|
{
|
|
gameObject.SetActive(true);
|
|
isVisible = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hide the speech bubble
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
|
|
// 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();
|
|
}
|
|
|
|
/// <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))
|
|
{
|
|
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>
|
|
/// Update the speech bubble to either show a prompt or hide based on dialogue availability
|
|
/// </summary>
|
|
/// <param name="hasDialogueAvailable">Whether dialogue is available</param>
|
|
public void UpdatePromptVisibility(bool hasDialogueAvailable)
|
|
{
|
|
if (hasDialogueAvailable)
|
|
{
|
|
Show();
|
|
SetText(dialoguePromptText);
|
|
}
|
|
else
|
|
{
|
|
Hide();
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
|
|
// 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
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|