개발 낙서장

[Unity 3D] NPC 대화 본문

Unity/Devlog

[Unity 3D] NPC 대화

권승준 2023. 1. 14. 00:41

구현 동기

퀘스트를 만들었으면 NPC와 대화해 수락, 진행, 완료를 할 수 있어야 한다.
특정 조건을 만족하면 자동으로 완료되는 퀘스트들도 있지만 나는 NPC를 통해서만 수락, 완료가 되도록 했다.

구현 내용

대화 UI

우선 NPC에게 말을 걸었을 때 나타나는 UI를 가장 먼저 만들어야 한다.
대화 UI는 이 UI 하나에서 대화, 퀘스트 보기, 수락, 완료 등이 다 진행되도록 구현했다.

지금 와서 보니 조금 난잡하기도 하고 어느 정도 수정이 필요해 보이긴 하지만
일단 기능에는 문제가 없으니...

이렇게 UI를 다 만들었다면 동작하게 할 스크립트가 필요하다.
나는 대화 UI도 간단한 상태 패턴으로 구현했다.

먼저 Enum으로 현재 UI의 상태들을 정의해줬다.

이 UI에 존재하는 모든 버튼을 누르면 호출되는 함수이다.
버튼의 개수가 좀 있어서 각 버튼 별로 호출 함수를 따로 만들기보다는 함수 하나를 호출하는 게 훨씬 낫다고 생각했다.

버튼에 리스너를 등록할 때 각 State에 맞는 int값을 매개변수로 받아 각 상태에 맞게 UI를 세팅하도록 했다.

퀘스트 리스트

퀘스트를 수락하려면 먼저 NPC한테서 어떤 퀘스트를 받을 수 있는지 목록을 봐야 할 것이다.

퀘스트의 진행 정도에 따라 수락 가능, 진행 중, 완료 가능, 수락 불가로 나누게 했고
이를 Dictionary<Quest, int>에 넣어 정렬해 주었다.

Value는 0부터 3까지 차례대로 완료 가능, 진행 중, 수락 가능, 수락 불가로 지정하여 Dictionary에 들어가게 했다.

GC를 방지하기 위해 각 퀘스트는 버튼 프리팹으로 생성해 놓고 오브젝트 풀링 기법을 이용하여 UI에 생성, 활성화, 비활성화되도록 했다.

    public void OnClickQuest()
    {
        _npcDialogue._selectedQuest = _quest.Key;

        _questManager = PlayerController.instance.QuestManager;
        QuestData quest = _questManager.GetQuest(_quest.Key);

        if (quest != null && quest.CanComplete)
        {
            _npcDialogue.SetState((int)NPCDialogueState.QUESTDETAIL);
            return;
        }
        else if(_quest.Value == 3)
        {
            string s = string.Format("<선행 조건>\nLv : {0}\n선행 퀘스트 : {1}", _quest.Key._requiredLevel, _quest.Key._precednetQuest == null ? "없음" : _quest.Key._precednetQuest._title);
            _npcDialogue.SetState((int)NPCDialogueState.DIALOGUE);
            _npcDialogue.SetContentText(s);
            return;
        }

        if (_quest.Key._type == Quest.Type.DIALOGUE)
        {
            DialogueQuest dq = (DialogueQuest)_quest.Key;
            if(_questManager.FindQuest(_quest.Key))
            {
                DialogueQuestData dialogueQuest = quest as DialogueQuestData;
                NPC npc = _npcDialogue._npc;
                if (dq._targetNPC[dialogueQuest._currentIndex] == npc)
                {
                    _npcDialogue.SetQuestDialogue(dq._npcDialogue[dialogueQuest._currentIndex]._sentences);
                    _npcDialogue.SetState((int)NPCDialogueState.QUESTDIALOGUE);
                }
            }
            else
            {
                if (_quest.Key._startNPC == _npcDialogue._npc)
                {
                    _npcDialogue.SetQuestDialogue(_quest.Key._dialogues);
                    _npcDialogue.SetState((int)NPCDialogueState.QUESTDIALOGUE);
                }
            }
        }
        else if (_quest.Key._type == Quest.Type.HUNTING)
            _npcDialogue.SetState((int)NPCDialogueState.QUESTDETAIL);
    }
    public void OnClickQuest()
    {
        _npcDialogue._selectedQuest = _quest.Key;

        _questManager = PlayerController.instance.QuestManager;
        QuestData quest = _questManager.GetQuest(_quest.Key);

        if (quest != null && quest.CanComplete)
        {
            _npcDialogue.SetState((int)NPCDialogueState.QUESTDETAIL);
            return;
        }
        else if(_quest.Value == 3)
        {
            string s = string.Format("<선행 조건>\nLv : {0}\n선행 퀘스트 : {1}", _quest.Key._requiredLevel, _quest.Key._precednetQuest == null ? "없음" : _quest.Key._precednetQuest._title);
            _npcDialogue.SetState((int)NPCDialogueState.DIALOGUE);
            _npcDialogue.SetContentText(s);
            return;
        }

        if (_quest.Key._type == Quest.Type.DIALOGUE)
        {
            DialogueQuest dq = (DialogueQuest)_quest.Key;
            if(_questManager.FindQuest(_quest.Key))
            {
                DialogueQuestData dialogueQuest = quest as DialogueQuestData;
                NPC npc = _npcDialogue._npc;
                if (dq._targetNPC[dialogueQuest._currentIndex] == npc)
                {
                    _npcDialogue.SetQuestDialogue(dq._npcDialogue[dialogueQuest._currentIndex]._sentences);
                    _npcDialogue.SetState((int)NPCDialogueState.QUESTDIALOGUE);
                }
            }
            else
            {
                if (_quest.Key._startNPC == _npcDialogue._npc)
                {
                    _npcDialogue.SetQuestDialogue(_quest.Key._dialogues);
                    _npcDialogue.SetState((int)NPCDialogueState.QUESTDIALOGUE);
                }
            }
        }
        else if (_quest.Key._type == Quest.Type.HUNTING)
            _npcDialogue.SetState((int)NPCDialogueState.QUESTDETAIL);
    }

각 퀘스트 버튼을 눌렀을 때 호출되는 함수이다.

  1. 현재 퀘스트가 완료 가능 혹은 수락 불가 상태인지
  2. 대화 퀘스트일 경우 현재 대화중인 NPC와 내 퀘스트 진행도가 맞는지
  3. 사냥 퀘스트인 경우

이렇게 3단계로 나누었다. 대화 퀘스트 역시 이 UI에서 진행되게 하였기 때문에 따로 조건을 짰다.

퀘스트 디테일

    public void AcceptQuest()
    {
        QuestData questData = null;

        if (_quest._type == Quest.Type.HUNTING)
        {
            questData = new HuntingQuestData(_quest as HuntingQuest);
        }
        else if (_quest._type == Quest.Type.DIALOGUE)
        {
            questData = new DialogueQuestData(_quest as DialogueQuest);
        }
        else
        {
            Debug.Log("!!!Check Quest Type!!!");
            return;
        }
        PlayerController.instance.QuestManager.AcceptQuest(questData);
        questData.OnQuestComplete.AddListener(_npcDialogue._npcAI.CheckQuestState);
        _npcDialogue._npcAI.CheckQuestState();
        _npcDialogue.SetState((int)NPCDialogueState.INIT);
    }

    public void CompleteQuest()
    {
        QuestManager questManager = PlayerController.instance.QuestManager;
        questManager.CompleteQuest(_quest, _npcDialogue._npcAI.CheckQuestState);
        _npcDialogue._npcAI.CheckQuestState();
        _npcDialogue.gameObject.SetActive(false);
        if(_quest._isTimeline)
            TimelineController.instance.PlayFromTimeline(_quest._playableDirector, _quest._timeline);
    }

퀘스트가 완료 가능하거나 수락 불가할 때, 사냥 퀘스트일 때에는 퀘스트 정보를 볼 수 있도록 했다.

구현 후기

처음 UI 구상할 때는 '퀘스트 리스트만 가져와서 플레이어 퀘스트와 비교하면 끝이네?'라고 되게 간단할 거라고 생각했는데 꽤 오래 걸렸던 것 같다.

특히 대화 퀘스트의 존재 때문에 더 까다로웠던 것 같다.

단순히 사냥 퀘스트만 있고 수락, 완료만 누르게 한다면 굉장히 간단했겠지만 대화 진행까지 이 UI에서 하다 보니 npc 비교, 대화 내용, 대화 진행도 등 따져야 할 것들이 많아졌다.

대화 퀘스트의 구현을 포기했다면 훨씬 쉬웠겠지만 사냥 퀘스트 하나로는 뭔가 부족하다고 생각해 그래도 끝까지 구현했다.

Scriptable Object에서 대화 내용을 수정만 해주면 실제 게임 UI에서 바로 나타나는 걸 보니 조금 뿌듯한 것 같기도?

'Unity > Devlog' 카테고리의 다른 글

[Unity 2D] 캐릭터 움직임  (1) 2023.01.23
[Unity 3D] NPC 퀘스트 팝업  (0) 2023.01.16
[Unity 3D] 퀘스트  (2) 2023.01.09
[Unity 3D] Collider의 크기를 넘겨줄 때 주의사항  (0) 2023.01.09
[Unity 3D] 몬스터 체력바 수정  (0) 2023.01.06
Comments