系統運作的詳細流程:
玩家靠近 → 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 |
觸發機制通用化 | 可應用於不同場景、不同角色,甚至可被敵人、任務物件重用。 |
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; //是否能重複對話
}
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;
}
}
}
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!");
}
}
}