系統運作的詳細流程:

玩家靠近 → NPC偵測 → 顯示E提示 ↓ 玩家按E → DialogueNPC.StartDialogue(dialogue) NPC啟動對話流程,呼叫UI ↓ DialogueUI.Instance.StartDialogue(dialogue) 開啟對話框 ↓ DialogueUI 讀取 DialogueData → 顯示角色名稱、句子 ↓ 玩家按下繼續鍵 → DialogueUI.DisplayNextSentence() 播放下一句 ↓ 全部播放完 → DialogueUI.EndDialogue()

程式腳本 原則 好處
DialogueData 資料與邏輯分離 可被 ScriptableObject 化,不需改程式即可新增 NPC 對話。
DialogueUI 單例 UI 管理 避免場上出現多個 UI 控制器,讓各 NPC 呼叫簡單統一。
DialogueNPC 觸發機制通用化 可應用於不同場景、不同角色,甚至可被敵人、任務物件重用。

Data

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class DialogueData
{
    
    [Header("對話內容")]
    [TextArea(3, 10)]
    public string[] dialogueLines; // 對話內容
    
    [Header("說話者資訊")]
    public string speakerName = "村民"; // NPC的名字
    public Sprite speakerAvatar; // NPC的圖片
    
    [Header("對話設定")]
    public bool canRepeat = true; //是否能重複對話
   
}

DialogueUI

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class DialogueUI : MonoBehaviour
{
    [Header("UI Components")] // 對話框包含的元素
    public Text nameText;          // NPC的名字
    public Text dialogueText;      // 對話內容  
    public Button continueButton;  // 繼續對話的按鈕
    public Image avatarImage;      // NPC 頭像顯示
    
    [Header("Typewriter Effect")] //打字效果
    public float typingSpeed = 0.05f;
    
    public static DialogueUI Instance { get; private set; }
    
    private DialogueData currentDialogue;
    private int currentLineIndex;
    private bool isTyping;
    private Coroutine typingCoroutine;
    
    private void Awake()
    {
        // 設置單例Instance
        if (Instance == null)
        {
            Instance = this;
            Debug.Log("DialogueUI Instance 設置成功");
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    private void Start()
    {
        InitializeUI();
    }
    
    private void InitializeUI()
    {
        // 初始隱藏對話面板(保持物件啟用但內容隱藏)
        gameObject.SetActive(false);
            
        // 設置按鈕監聽
        if (continueButton != null)
        {
            continueButton.onClick.RemoveAllListeners(); // 清除重複監聽器
            continueButton.onClick.AddListener(OnContinueClicked);
        }
            
        Debug.Log("DialogueUI 初始化完成,對話面板已隱藏");
    }
    
    public void StartDialogue(DialogueData dialogue)
    {
        if (dialogue == null || dialogue.dialogueLines.Length == 0) //檢查有無對話內容
        {
            Debug.LogWarning("對話數據為空!");
            return;
        }
        
        // 顯示對話面板
        gameObject.SetActive(true);
        
        currentDialogue = dialogue;
        currentLineIndex = 0;
        
        // 檢查有無名稱
        if (nameText != null)
        {
            nameText.text = dialogue.speakerName;
            Debug.Log($"設置說話者名稱: {dialogue.speakerName}");
        }
        else
        {
            Debug.LogError("NameText 未設置!");
        }
        
        // 設置 NPC 頭像
        if (avatarImage != null)
        {
            if (dialogue.speakerAvatar != null)
            {
                avatarImage.sprite = dialogue.speakerAvatar;
                avatarImage.enabled = true;
                Debug.Log("顯示 NPC 頭像");
            }
            else
            {
                avatarImage.enabled = false; // 沒圖片就隱藏
                Debug.Log("NPC 無頭像,已隱藏 Image 元件");
            }
        }
        else
        {
            Debug.LogWarning("AvatarImage 未綁定到 UI!");
        }
        
        // 暫停遊戲
        // Time.timeScale = 0f;
        
        Debug.Log("顯示對話面板,開始對話");
        
        // 開始顯示第一行對話
        DisplayCurrentLine();
    }
    
    private void DisplayCurrentLine()
    {
        if (currentDialogue == null || currentLineIndex >= currentDialogue.dialogueLines.Length)
        {
            EndDialogue();
            return;
        }
        
        string currentLine = currentDialogue.dialogueLines[currentLineIndex];
        Debug.Log($"顯示對話: {currentLine}");
        
        // 停止之前的打字效果
        if (typingCoroutine != null)
        {
            StopCoroutine(typingCoroutine);
        }
        
        // 開始打字效果
        typingCoroutine = StartCoroutine(TypeText(currentLine));
    }
    
    private IEnumerator TypeText(string text)
    {
        isTyping = true;
        
        if (dialogueText != null)
        {
            dialogueText.text = "";
        }
        else
        {
            Debug.LogError("DialogueText 未設置!");
            yield break;
        }
        
        // 隱藏繼續按鈕直到打字完成
        if (continueButton != null)
            continueButton.gameObject.SetActive(false);
        
        foreach (char letter in text.ToCharArray())
        {
            dialogueText.text += letter;
            yield return new WaitForSecondsRealtime(typingSpeed);
        }
        
        isTyping = false;
        
        // 顯示繼續按鈕
        if (continueButton != null)
            continueButton.gameObject.SetActive(true);
    }
    
    public void OnContinueClicked()
    {
        Debug.Log("按下繼續按鈕");
        
        if (isTyping)
        {
            // 如果正在打字,立即完成
            if (typingCoroutine != null)
            {
                StopCoroutine(typingCoroutine);
            }
            
            if (dialogueText != null && currentDialogue != null && currentLineIndex < currentDialogue.dialogueLines.Length)
            {
                dialogueText.text = currentDialogue.dialogueLines[currentLineIndex];
            }
            
            isTyping = false;
            
            if (continueButton != null)
                continueButton.gameObject.SetActive(true);
        }
        else
        {
            // 前往下一行
            currentLineIndex++;
            DisplayCurrentLine();
        }
    }
    
    private void EndDialogue()
    {
        Debug.Log("結束對話,隱藏對話面板");
        
        // 隱藏對話面板
        gameObject.SetActive(false);
    }
    
    private void OnDestroy()
    {
        if (Instance == this)
        {
            Instance = null;
        }
    }
}

DialogueNPC

using UnityEngine;

public class DialogueNPC : MonoBehaviour
{
    [Header("對話設定")]
    public DialogueData dialogue; //對話內容
    public GameObject interactionPrompt; //提示
    
    [Header("互動設定")]
    public KeyCode interactionKey = KeyCode.E;
    public string playerTag = "Player";
    
    private bool playerInRange = false; //偵測玩家是否進入對話範圍
    
    private void Start()
    {
        if (interactionPrompt != null)
            interactionPrompt.SetActive(false);
        
        Debug.Log($"{gameObject.name} DialogueNPC 初始化完成");
    }
    
    private void Update()
    {
        if (playerInRange && Input.GetKeyDown(interactionKey))
        {
            StartDialogue();
        }
    }
    
    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag(playerTag))
        {
            playerInRange = true;
            ShowInteractionPrompt();
            Debug.Log("玩家進入對話範圍");
        }
    }
    
    private void OnTriggerExit2D(Collider2D other)
    {
        if (other.CompareTag(playerTag))
        {
            playerInRange = false;
            HideInteractionPrompt();
            Debug.Log("玩家離開對話範圍");
        }
    }
    
    private void ShowInteractionPrompt()
    {
        if (interactionPrompt != null)
        {
            interactionPrompt.SetActive(true);
            Debug.Log("顯示互動提示");
        }
    }
    
    private void HideInteractionPrompt()
    {
        if (interactionPrompt != null)
        {
            interactionPrompt.SetActive(false);
        }
    }
    
    public void StartDialogue()
    {
        if (dialogue == null)
        {
            Debug.LogWarning($"{gameObject.name} 沒有設定對話內容!");
            return;
        }
        
        Debug.Log("開始對話");
        
        HideInteractionPrompt();
        
        if (DialogueUI.Instance != null)
        {
            DialogueUI.Instance.StartDialogue(dialogue);
        }
        else
        {
            Debug.LogError("找不到 DialogueUI Instance!");
        }
    }
}