26.05.26(NPC 대사 메니져)

최완용·2026년 5월 26일

1. NPC 대사 관리 Script

//SO
using UnityEngine;

[CreateAssetMenu(fileName = "DialogueData", menuName = "Dialogue/Dialogue Data")]
public class DialogueData : ScriptableObject
{
    [Header("대화 이름")]
    [SerializeField] private string speakerName;

    [Header("대사 목록")]
    [TextArea(2, 4)]
    [SerializeField] private string[] lines;

    public string SpeakerName => speakerName;
    public string[] Lines => lines;
}
//NPC에 맞는 SO 가져오기
using UnityEngine;

public class NPCDialogue : MonoBehaviour
{
    [Header("NPC 대사 데이터")]
    [SerializeField] private DialogueData dialogueData;

    public DialogueData DialogueData => dialogueData;

    public void StartDialogue()
    {
        if (dialogueData == null)
        {
            Debug.LogWarning($"[NPCDialogue] {gameObject.name}에 DialogueData가 없습니다.");
            return;
        }

        DialogueManager.Instance.StartDialogue(dialogueData);

        Debug.Log($"[NPCDialogue] {gameObject.name} 대화 시작 요청");
    }
}
// NPC 대사 메니저
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.Events;

public class DialogueManager : MonoBehaviour
{
    public static DialogueManager Instance { get; private set; }

    [Header("UI")]
    [SerializeField] private GameObject dialoguePanel;
    [SerializeField] private TMP_Text speakerNameText;
    [SerializeField] private TMP_Text dialogueText;

    [Header("타이핑 설정")]
    [SerializeField] private float typingSpeed = 0.04f;

    [Header("대화 종료 이벤트")]
    [SerializeField] private UnityEvent onDialogueEnd;

    private DialogueData currentDialogueData;
    private int currentIndex;
    private bool isTyping;
    private string currentLine;
    private Coroutine typingCoroutine;

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Debug.LogWarning("[DialogueManager] 중복된 DialogueManager가 제거되었습니다.");
            Destroy(gameObject);
            return;
        }

        Instance = this;
    }

    public void StartDialogue(DialogueData dialogueData)
    {
        if (dialogueData == null)
        {
            Debug.LogWarning("[DialogueManager] 전달받은 DialogueData가 없습니다.");
            return;
        }

        if (dialogueData.Lines == null || dialogueData.Lines.Length == 0)
        {
            Debug.LogWarning("[DialogueManager] 출력할 대사가 없습니다.");
            return;
        }

        currentDialogueData = dialogueData;
        currentIndex = 0;

        dialoguePanel.SetActive(true);
        speakerNameText.text = currentDialogueData.SpeakerName;

        ShowCurrentLine();

        Debug.Log($"[DialogueManager] 대화 시작: {currentDialogueData.SpeakerName}");
    }

    public void OnClickNext()
    {
        if (currentDialogueData == null)
        {
            return;
        }

        if (isTyping)
        {
            CompleteCurrentLine();
            return;
        }

        MoveNextLine();
    }

    private void ShowCurrentLine()
    {
        currentLine = currentDialogueData.Lines[currentIndex];

        if (typingCoroutine != null)
        {
            StopCoroutine(typingCoroutine);
        }

        typingCoroutine = StartCoroutine(TypeLine(currentLine));
    }

    private IEnumerator TypeLine(string line)
    {
        isTyping = true;
        dialogueText.text = string.Empty;

        foreach (char letter in line)
        {
            dialogueText.text += letter;
            yield return new WaitForSeconds(typingSpeed);
        }

        isTyping = false;
        typingCoroutine = null;

        Debug.Log($"[DialogueManager] 문장 출력 완료: {line}");
    }

    private void CompleteCurrentLine()
    {
        if (typingCoroutine != null)
        {
            StopCoroutine(typingCoroutine);
            typingCoroutine = null;
        }

        dialogueText.text = currentLine;
        isTyping = false;

        Debug.Log("[DialogueManager] 현재 문장 즉시 출력");
    }

    private void MoveNextLine()
    {
        currentIndex++;

        if (currentIndex >= currentDialogueData.Lines.Length)
        {
            EndDialogue();
            return;
        }

        ShowCurrentLine();

        Debug.Log($"[DialogueManager] 다음 문장 이동: {currentIndex}");
    }

    private void EndDialogue()
    {
        dialogueText.text = string.Empty;
        speakerNameText.text = string.Empty;
        dialoguePanel.SetActive(false);

        Debug.Log("[DialogueManager] 대화 종료");

        currentDialogueData = null;
        currentIndex = 0;

        onDialogueEnd?.Invoke();
    }
}

//NPC 상호작용 키
using UnityEngine;

public class PlayerInteraction : MonoBehaviour
{
    private NPCDialogue currentNPC;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.F))
        {
            TryTalk();
        }
    }

    private void TryTalk()
    {
        if (currentNPC == null)
        {
            Debug.Log("[PlayerInteraction] 대화 가능한 NPC가 없습니다.");
            return;
        }

        currentNPC.StartDialogue();
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.TryGetComponent(out NPCDialogue npcDialogue))
        {
            currentNPC = npcDialogue;
            Debug.Log($"[PlayerInteraction] 대화 가능 NPC 감지: {other.name}");
        }
    }

    private void OnTriggerExit2D(Collider2D other)
    {
        if (other.TryGetComponent(out NPCDialogue npcDialogue) && currentNPC == npcDialogue)
        {
            currentNPC = null;
            Debug.Log($"[PlayerInteraction] 대화 가능 NPC 해제: {other.name}");
        }
    }
}

2. 사용법

1. SO를  생성한다.
2. NPCDialogue을 NPC에 넣는다.
3. NPC대사에 해당하는 SO를 넣는다.
3. DialogueManager를 빈 오브젝트에 넣은 후 Text 들을 연결한다.

0개의 댓글