Create a simple dialogue authoring system, tied into our items (#10)
- Editor dialogue graph - Asset importer for processing the graph into runtime data - DialogueComponent that steers the dialogue interactions - DialogueCanbas with a scalable speech bubble to display everything - Brief README overview of the system Co-authored-by: AlexanderT <alexander@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #10
This commit is contained in:
627
Assets/Scripts/Dialogue/DialogueComponent.cs
Normal file
627
Assets/Scripts/Dialogue/DialogueComponent.cs
Normal file
@@ -0,0 +1,627 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Core;
|
||||
using Interactions;
|
||||
using UnityEngine;
|
||||
using PuzzleS;
|
||||
|
||||
namespace Dialogue
|
||||
{
|
||||
[AddComponentMenu("Apple Hills/Dialogue/Dialogue Component")]
|
||||
public class DialogueComponent : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private RuntimeDialogueGraph dialogueGraph;
|
||||
|
||||
private RuntimeDialogueNode currentNode;
|
||||
private int currentLineIndex;
|
||||
private bool initialized = false;
|
||||
private SpeechBubble speechBubble;
|
||||
|
||||
// Flag to track when a condition has been met but dialogue hasn't advanced yet
|
||||
private bool _conditionSatisfiedPendingAdvance = false;
|
||||
|
||||
// Track the current slot state for WaitOnSlot nodes
|
||||
private ItemSlotState _currentSlotState = ItemSlotState.None;
|
||||
private PickupItemData _lastSlottedItem;
|
||||
|
||||
// Properties
|
||||
public bool IsActive { get; private set; }
|
||||
public bool IsCompleted { get; private set; }
|
||||
public string CurrentSpeakerName => dialogueGraph?.speakerName;
|
||||
|
||||
// Event for UI updates if needed
|
||||
public event Action<string> OnDialogueChanged;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Register for global events
|
||||
if (PuzzleManager.Instance != null)
|
||||
PuzzleManager.Instance.OnStepCompleted += OnAnyPuzzleStepCompleted;
|
||||
|
||||
if (ItemManager.Instance != null)
|
||||
{
|
||||
ItemManager.Instance.OnItemPickedUp += OnAnyItemPickedUp;
|
||||
ItemManager.Instance.OnCorrectItemSlotted += OnAnyItemSlotted;
|
||||
ItemManager.Instance.OnIncorrectItemSlotted += OnAnyIncorrectItemSlotted;
|
||||
ItemManager.Instance.OnForbiddenItemSlotted += OnAnyForbiddenItemSlotted;
|
||||
ItemManager.Instance.OnItemSlotCleared += OnAnyItemSlotCleared;
|
||||
ItemManager.Instance.OnItemsCombined += OnAnyItemsCombined;
|
||||
}
|
||||
|
||||
speechBubble = GetComponentInChildren<SpeechBubble>();
|
||||
|
||||
if (speechBubble == null)
|
||||
{
|
||||
Debug.LogError("SpeechBubble component is missing on Dialogue Component");
|
||||
}
|
||||
|
||||
// Auto-start the dialogue
|
||||
// StartDialogue();
|
||||
|
||||
var interactable = GetComponent<Interactable>();
|
||||
if (interactable != null)
|
||||
{
|
||||
interactable.characterArrived.AddListener(OnCharacterArrived);
|
||||
}
|
||||
|
||||
// Update bubble visibility based on whether we have lines
|
||||
if (speechBubble != null)
|
||||
{
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCharacterArrived()
|
||||
{
|
||||
if (speechBubble == null || ! HasAnyLines()) return;
|
||||
|
||||
AdvanceDialogueState();
|
||||
|
||||
// Get the current dialogue line
|
||||
string line = GetCurrentDialogueLine();
|
||||
|
||||
// Display the line with the new method that handles timed updates
|
||||
speechBubble.DisplayDialogueLine(line, HasAnyLines());
|
||||
|
||||
// Advance dialogue state for next interaction
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Unregister from events
|
||||
if (PuzzleManager.Instance != null)
|
||||
PuzzleManager.Instance.OnStepCompleted -= OnAnyPuzzleStepCompleted;
|
||||
|
||||
if (ItemManager.Instance != null)
|
||||
{
|
||||
ItemManager.Instance.OnItemPickedUp -= OnAnyItemPickedUp;
|
||||
ItemManager.Instance.OnCorrectItemSlotted -= OnAnyItemSlotted;
|
||||
ItemManager.Instance.OnIncorrectItemSlotted -= OnAnyIncorrectItemSlotted;
|
||||
ItemManager.Instance.OnForbiddenItemSlotted -= OnAnyForbiddenItemSlotted;
|
||||
ItemManager.Instance.OnItemSlotCleared -= OnAnyItemSlotCleared;
|
||||
ItemManager.Instance.OnItemsCombined -= OnAnyItemsCombined;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the dialogue from the beginning
|
||||
/// </summary>
|
||||
public void StartDialogue()
|
||||
{
|
||||
if (dialogueGraph == null)
|
||||
{
|
||||
Debug.LogError("DialogueComponent: No dialogue graph assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
IsActive = true;
|
||||
IsCompleted = false;
|
||||
currentLineIndex = 0;
|
||||
initialized = true;
|
||||
|
||||
// Set to entry node
|
||||
currentNode = dialogueGraph.GetNodeByID(dialogueGraph.entryNodeID);
|
||||
|
||||
// Process the node
|
||||
ProcessCurrentNode();
|
||||
|
||||
// Update bubble visibility based on whether we have lines
|
||||
if (speechBubble != null)
|
||||
{
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current dialogue line and advance to the next line or node if appropriate
|
||||
/// Each call represents one interaction with the NPC
|
||||
/// </summary>
|
||||
public string GetCurrentDialogueLine()
|
||||
{
|
||||
// Initialize if needed
|
||||
if (!initialized)
|
||||
{
|
||||
StartDialogue();
|
||||
}
|
||||
|
||||
if (!IsActive || IsCompleted || currentNode == null)
|
||||
return string.Empty;
|
||||
|
||||
// For WaitOnSlot nodes, use the appropriate line type based on slot state
|
||||
if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot)
|
||||
{
|
||||
// Choose the appropriate line collection based on the current slot state
|
||||
List<string> linesForState = currentNode.dialogueLines; // Default lines
|
||||
|
||||
switch (_currentSlotState)
|
||||
{
|
||||
case ItemSlotState.Incorrect:
|
||||
// Use incorrect item lines if available, otherwise fall back to default lines
|
||||
if (currentNode.incorrectItemLines != null && currentNode.incorrectItemLines.Count > 0)
|
||||
linesForState = currentNode.incorrectItemLines;
|
||||
break;
|
||||
|
||||
case ItemSlotState.Forbidden:
|
||||
// Use forbidden item lines if available, otherwise fall back to default lines
|
||||
if (currentNode.forbiddenItemLines != null && currentNode.forbiddenItemLines.Count > 0)
|
||||
linesForState = currentNode.forbiddenItemLines;
|
||||
break;
|
||||
|
||||
// For None or Correct state, use the default lines
|
||||
default:
|
||||
linesForState = currentNode.dialogueLines;
|
||||
break;
|
||||
}
|
||||
|
||||
// If we have lines for this state, return the current one
|
||||
if (linesForState != null && linesForState.Count > 0)
|
||||
{
|
||||
// Make sure index is within bounds
|
||||
int index = Mathf.Clamp(currentLineIndex, 0, linesForState.Count - 1);
|
||||
return linesForState[index];
|
||||
}
|
||||
}
|
||||
|
||||
// For other node types, use the default dialogueLines
|
||||
if (currentNode.dialogueLines == null || currentNode.dialogueLines.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
// Get current line
|
||||
string currentLine = string.Empty;
|
||||
if (currentLineIndex >= 0 && currentLineIndex < currentNode.dialogueLines.Count)
|
||||
{
|
||||
currentLine = currentNode.dialogueLines[currentLineIndex];
|
||||
}
|
||||
|
||||
return currentLine;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advance dialogue state for the next interaction
|
||||
/// </summary>
|
||||
private void AdvanceDialogueState()
|
||||
{
|
||||
if (!IsActive || IsCompleted || currentNode == null)
|
||||
return;
|
||||
|
||||
// If the condition was satisfied earlier, move to the next node immediately
|
||||
if (_conditionSatisfiedPendingAdvance)
|
||||
{
|
||||
_conditionSatisfiedPendingAdvance = false; // Reset flag
|
||||
MoveToNextNode();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have more lines in the current node, advance to the next line
|
||||
if (currentLineIndex < currentNode.dialogueLines.Count - 1)
|
||||
{
|
||||
currentLineIndex++;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we should loop through lines, reset the index
|
||||
if (currentNode.loopThroughLines && currentNode.dialogueLines.Count > 0)
|
||||
{
|
||||
currentLineIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're at a node that doesn't have a next node, we're done
|
||||
if (string.IsNullOrEmpty(currentNode.nextNodeID))
|
||||
{
|
||||
IsActive = false;
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to the next node only if no conditions to wait for
|
||||
if (!IsWaitingForCondition())
|
||||
{
|
||||
MoveToNextNode();
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveToNextNode()
|
||||
{
|
||||
Debug.Log("MoveToNextNode");
|
||||
|
||||
// If there's no next node, complete the dialogue
|
||||
if (string.IsNullOrEmpty(currentNode.nextNodeID))
|
||||
{
|
||||
IsActive = false;
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to the next node
|
||||
currentNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID);
|
||||
currentLineIndex = 0;
|
||||
|
||||
// Process the new node
|
||||
ProcessCurrentNode();
|
||||
}
|
||||
|
||||
private void ProcessCurrentNode()
|
||||
{
|
||||
if (currentNode == null)
|
||||
{
|
||||
Debug.LogError("DialogueComponent: Current node is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle different node types
|
||||
switch (currentNode.nodeType)
|
||||
{
|
||||
case RuntimeDialogueNodeType.Dialogue:
|
||||
// Regular dialogue node, nothing special to do
|
||||
break;
|
||||
|
||||
case RuntimeDialogueNodeType.WaitOnPuzzleStep:
|
||||
// Check if the puzzle step is already completed
|
||||
if (IsPuzzleStepComplete(currentNode.puzzleStepID))
|
||||
{
|
||||
// If it's already complete, move past this node automatically
|
||||
MoveToNextNode();
|
||||
}
|
||||
break;
|
||||
|
||||
case RuntimeDialogueNodeType.WaitOnPickup:
|
||||
// Check if the item is already picked up
|
||||
if (IsItemPickedUp(currentNode.pickupItemID))
|
||||
{
|
||||
// If it's already picked up, move past this node automatically
|
||||
MoveToNextNode();
|
||||
}
|
||||
break;
|
||||
|
||||
case RuntimeDialogueNodeType.WaitOnSlot:
|
||||
// Check if the item is already slotted
|
||||
if (IsItemSlotted(currentNode.slotItemID))
|
||||
{
|
||||
// If it's already slotted, move past this node automatically
|
||||
MoveToNextNode();
|
||||
}
|
||||
break;
|
||||
|
||||
case RuntimeDialogueNodeType.WaitOnCombination:
|
||||
// Check if the result item is already created through combination
|
||||
if (IsResultItemCreated(currentNode.combinationResultItemID))
|
||||
{
|
||||
// If it's already created, move past this node automatically
|
||||
MoveToNextNode();
|
||||
}
|
||||
break;
|
||||
|
||||
case RuntimeDialogueNodeType.End:
|
||||
// End node, complete the dialogue
|
||||
IsActive = false;
|
||||
IsCompleted = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.LogError($"DialogueComponent: Unknown node type {currentNode.nodeType}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Global event handlers
|
||||
private void OnAnyPuzzleStepCompleted(PuzzleStepSO step)
|
||||
{
|
||||
// Only react if we're active and waiting on a puzzle step
|
||||
if (!IsActive || IsCompleted || currentNode == null ||
|
||||
currentNode.nodeType != RuntimeDialogueNodeType.WaitOnPuzzleStep)
|
||||
return;
|
||||
|
||||
// Check if this is the step we're waiting for
|
||||
if (step.stepId == currentNode.puzzleStepID)
|
||||
{
|
||||
// Instead of immediately moving to the next node, set the flag
|
||||
_conditionSatisfiedPendingAdvance = true;
|
||||
|
||||
// Update bubble visibility after state change to show interaction prompt
|
||||
if (speechBubble != null)
|
||||
{
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnyItemPickedUp(PickupItemData item)
|
||||
{
|
||||
// Only react if we're active and waiting on an item pickup
|
||||
if (!IsActive || IsCompleted || currentNode == null ||
|
||||
currentNode.nodeType != RuntimeDialogueNodeType.WaitOnPickup)
|
||||
return;
|
||||
|
||||
// Check if this is the item we're waiting for
|
||||
if (item.itemId == currentNode.pickupItemID)
|
||||
{
|
||||
// Instead of immediately moving to the next node, set the flag
|
||||
_conditionSatisfiedPendingAdvance = true;
|
||||
|
||||
// Update bubble visibility after state change to show interaction prompt
|
||||
if (speechBubble != null)
|
||||
{
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnyItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
|
||||
{
|
||||
Debug.Log("[DialogueComponent] OnAnyItemSlotted");
|
||||
|
||||
// Only react if we're active and waiting on a slot
|
||||
if (!IsActive || IsCompleted || currentNode == null ||
|
||||
currentNode.nodeType != RuntimeDialogueNodeType.WaitOnSlot)
|
||||
return;
|
||||
|
||||
// Check if this is the slot we're waiting for
|
||||
if (slotDefinition.itemId == currentNode.slotItemID)
|
||||
{
|
||||
// Instead of immediately moving to the next node, set the flag
|
||||
_conditionSatisfiedPendingAdvance = true;
|
||||
|
||||
// Update bubble visibility after state change to show interaction prompt
|
||||
if (speechBubble != null)
|
||||
{
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnyItemsCombined(PickupItemData itemA, PickupItemData itemB, PickupItemData resultItem)
|
||||
{
|
||||
// Only react if we're active and waiting on a combination
|
||||
if (!IsActive || IsCompleted || currentNode == null ||
|
||||
currentNode.nodeType != RuntimeDialogueNodeType.WaitOnCombination)
|
||||
return;
|
||||
|
||||
// Check if this is the result item we're waiting for
|
||||
if (resultItem.itemId == currentNode.combinationResultItemID)
|
||||
{
|
||||
// Instead of immediately moving to the next node, set the flag
|
||||
_conditionSatisfiedPendingAdvance = true;
|
||||
|
||||
// Update bubble visibility after state change to show interaction prompt
|
||||
if (speechBubble != null)
|
||||
{
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnyIncorrectItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
|
||||
{
|
||||
// Update the slot state for displaying the correct dialogue lines
|
||||
if (!IsActive || IsCompleted || currentNode == null)
|
||||
return;
|
||||
|
||||
// Only update state if we're actively waiting on this slot
|
||||
if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot &&
|
||||
slotDefinition.itemId == currentNode.slotItemID)
|
||||
{
|
||||
_currentSlotState = ItemSlotState.Incorrect;
|
||||
_lastSlottedItem = slottedItem;
|
||||
|
||||
// Trigger dialogue update
|
||||
if (speechBubble != null)
|
||||
{
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnyForbiddenItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
|
||||
{
|
||||
// Update the slot state for displaying the correct dialogue lines
|
||||
if (!IsActive || IsCompleted || currentNode == null)
|
||||
return;
|
||||
|
||||
// Only update state if we're actively waiting on this slot
|
||||
if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot &&
|
||||
slotDefinition.itemId == currentNode.slotItemID)
|
||||
{
|
||||
_currentSlotState = ItemSlotState.Forbidden;
|
||||
_lastSlottedItem = slottedItem;
|
||||
|
||||
// Trigger dialogue update
|
||||
if (speechBubble != null)
|
||||
{
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnyItemSlotCleared(PickupItemData removedItem)
|
||||
{
|
||||
// Update the slot state when an item is removed
|
||||
if (!IsActive || IsCompleted || currentNode == null)
|
||||
return;
|
||||
|
||||
// Reset slot state if we were tracking this item
|
||||
if (_lastSlottedItem != null && _lastSlottedItem == removedItem)
|
||||
{
|
||||
_currentSlotState = ItemSlotState.None;
|
||||
_lastSlottedItem = null;
|
||||
|
||||
// Trigger dialogue update
|
||||
if (speechBubble != null)
|
||||
{
|
||||
speechBubble.UpdatePromptVisibility(HasAnyLines());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private bool IsWaitingForCondition()
|
||||
{
|
||||
if (currentNode == null) return false;
|
||||
|
||||
switch (currentNode.nodeType)
|
||||
{
|
||||
case RuntimeDialogueNodeType.WaitOnPuzzleStep:
|
||||
return !IsPuzzleStepComplete(currentNode.puzzleStepID);
|
||||
case RuntimeDialogueNodeType.WaitOnPickup:
|
||||
return !IsItemPickedUp(currentNode.pickupItemID);
|
||||
case RuntimeDialogueNodeType.WaitOnSlot:
|
||||
return !IsItemSlotted(currentNode.slotItemID);
|
||||
case RuntimeDialogueNodeType.WaitOnCombination:
|
||||
return !IsResultItemCreated(currentNode.combinationResultItemID);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPuzzleStepComplete(string stepID)
|
||||
{
|
||||
return PuzzleManager.Instance != null &&
|
||||
PuzzleManager.Instance.IsPuzzleStepCompleted(stepID);
|
||||
}
|
||||
|
||||
private bool IsItemPickedUp(string itemID)
|
||||
{
|
||||
if (ItemManager.Instance == null) return false;
|
||||
|
||||
// Check all pickups for the given ID
|
||||
foreach (var pickup in ItemManager.Instance.Pickups)
|
||||
{
|
||||
if (pickup.isPickedUp && pickup.itemData != null &&
|
||||
pickup.itemData.itemId == itemID)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsItemSlotted(string slotID)
|
||||
{
|
||||
if (ItemManager.Instance == null) return false;
|
||||
|
||||
// Check if any slot with this ID has the correct item
|
||||
foreach (var slot in ItemManager.Instance.ItemSlots)
|
||||
{
|
||||
if (slot.itemData != null && slot.itemData.itemId == slotID &&
|
||||
slot.CurrentSlottedState == ItemSlotState.Correct)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsResultItemCreated(string resultItemId)
|
||||
{
|
||||
if (ItemManager.Instance == null) return false;
|
||||
|
||||
// Use the ItemManager's tracking of items created through combination
|
||||
return ItemManager.Instance.WasItemCreatedThroughCombination(resultItemId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the dialogue component has any lines available to serve
|
||||
/// </summary>
|
||||
/// <returns>True if there are lines available, false otherwise</returns>
|
||||
public bool HasAnyLines()
|
||||
{
|
||||
if (!initialized)
|
||||
{
|
||||
// If not initialized yet but has a dialogue graph, it will have lines when initialized
|
||||
return dialogueGraph != null;
|
||||
}
|
||||
|
||||
// No lines if dialogue is not active or is completed
|
||||
if (!IsActive || IsCompleted || currentNode == null)
|
||||
return false;
|
||||
|
||||
// Special case: if condition has been satisfied but not yet advanced, we should show lines
|
||||
if (_conditionSatisfiedPendingAdvance && !string.IsNullOrEmpty(currentNode.nextNodeID))
|
||||
{
|
||||
// Check if the next node would have lines
|
||||
RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID);
|
||||
return nextNode != null && (nextNode.dialogueLines.Count > 0 || nextNode.nodeType != RuntimeDialogueNodeType.End);
|
||||
}
|
||||
|
||||
// For WaitOnSlot nodes, check for lines based on current slot state
|
||||
if (currentNode.nodeType == RuntimeDialogueNodeType.WaitOnSlot)
|
||||
{
|
||||
// Choose the appropriate line collection based on the current slot state
|
||||
List<string> linesForState = currentNode.dialogueLines; // Default lines
|
||||
|
||||
switch (_currentSlotState)
|
||||
{
|
||||
case ItemSlotState.Incorrect:
|
||||
// Use incorrect item lines if available, otherwise fall back to default lines
|
||||
if (currentNode.incorrectItemLines != null && currentNode.incorrectItemLines.Count > 0)
|
||||
linesForState = currentNode.incorrectItemLines;
|
||||
break;
|
||||
|
||||
case ItemSlotState.Forbidden:
|
||||
// Use forbidden item lines if available, otherwise fall back to default lines
|
||||
if (currentNode.forbiddenItemLines != null && currentNode.forbiddenItemLines.Count > 0)
|
||||
linesForState = currentNode.forbiddenItemLines;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if we have any lines for the current state
|
||||
if (linesForState != null && linesForState.Count > 0)
|
||||
{
|
||||
// If we're not at the end of the lines or we loop through them
|
||||
if (currentLineIndex < linesForState.Count - 1 || currentNode.loopThroughLines)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// For other node types, use the standard check
|
||||
else if (currentNode.dialogueLines.Count > 0)
|
||||
{
|
||||
// If we're not at the end of the lines or we loop through them
|
||||
if (currentLineIndex < currentNode.dialogueLines.Count - 1 || currentNode.loopThroughLines)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we're at the end of lines but not waiting for a condition and have a next node
|
||||
if (!IsWaitingForCondition() && !string.IsNullOrEmpty(currentNode.nextNodeID))
|
||||
{
|
||||
// We need to check if the next node would have lines
|
||||
RuntimeDialogueNode nextNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID);
|
||||
return nextNode != null && (nextNode.dialogueLines.Count > 0 || nextNode.nodeType != RuntimeDialogueNodeType.End);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Editor functionality
|
||||
public void SetDialogueGraph(RuntimeDialogueGraph graph)
|
||||
{
|
||||
dialogueGraph = graph;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Dialogue/DialogueComponent.cs.meta
Normal file
3
Assets/Scripts/Dialogue/DialogueComponent.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25bbad45f1fa4183b30ad76c62256fd6
|
||||
timeCreated: 1758891211
|
||||
55
Assets/Scripts/Dialogue/RuntimeDialogueGraph.cs
Normal file
55
Assets/Scripts/Dialogue/RuntimeDialogueGraph.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Dialogue
|
||||
{
|
||||
[Serializable]
|
||||
public enum RuntimeDialogueNodeType
|
||||
{
|
||||
Dialogue,
|
||||
WaitOnPuzzleStep,
|
||||
WaitOnPickup,
|
||||
WaitOnSlot,
|
||||
WaitOnCombination,
|
||||
End
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class RuntimeDialogueGraph : ScriptableObject
|
||||
{
|
||||
public string entryNodeID;
|
||||
public string speakerName;
|
||||
public List<RuntimeDialogueNode> allNodes = new List<RuntimeDialogueNode>();
|
||||
|
||||
// Helper method to find a node by ID
|
||||
public RuntimeDialogueNode GetNodeByID(string id)
|
||||
{
|
||||
return allNodes.Find(n => n.nodeID == id);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class RuntimeDialogueNode
|
||||
{
|
||||
public string nodeID;
|
||||
public RuntimeDialogueNodeType nodeType;
|
||||
public string nextNodeID;
|
||||
|
||||
// Basic dialogue
|
||||
public List<string> dialogueLines = new List<string>();
|
||||
public bool loopThroughLines;
|
||||
|
||||
// Conditional nodes
|
||||
public string puzzleStepID; // For WaitOnPuzzleStep
|
||||
public string pickupItemID; // For WaitOnPickup
|
||||
public string slotItemID; // For WaitOnSlot
|
||||
public string combinationResultItemID; // For WaitOnCombination
|
||||
|
||||
// For WaitOnSlot - different responses
|
||||
public List<string> incorrectItemLines = new List<string>();
|
||||
public bool loopThroughIncorrectLines;
|
||||
public List<string> forbiddenItemLines = new List<string>();
|
||||
public bool loopThroughForbiddenLines;
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Dialogue/RuntimeDialogueGraph.cs.meta
Normal file
3
Assets/Scripts/Dialogue/RuntimeDialogueGraph.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c3be3596532450a923c31dfe0ed4aa9
|
||||
timeCreated: 1758871423
|
||||
263
Assets/Scripts/Dialogue/SpeechBubble.cs
Normal file
263
Assets/Scripts/Dialogue/SpeechBubble.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
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 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 bool isVisible = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
// Stop any existing typewriter effect
|
||||
if (typewriterCoroutine != null)
|
||||
{
|
||||
StopCoroutine(typewriterCoroutine);
|
||||
typewriterCoroutine = null;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Dialogue/SpeechBubble.cs.meta
Normal file
3
Assets/Scripts/Dialogue/SpeechBubble.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb3605ae81a54d2689504e0cd456ac27
|
||||
timeCreated: 1758973942
|
||||
1
Assets/Scripts/Dialogue/SpeechBubbleController.cs
Normal file
1
Assets/Scripts/Dialogue/SpeechBubbleController.cs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
3
Assets/Scripts/Dialogue/SpeechBubbleController.cs.meta
Normal file
3
Assets/Scripts/Dialogue/SpeechBubbleController.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 877344c7a0014922bc3a2a469e03792d
|
||||
timeCreated: 1759050622
|
||||
Reference in New Issue
Block a user