Unity 2D RPG - 5. 퀘스트 시스템 구현하기

2023. 8. 14. 05:03C# & Unity 공부

퀘스트 대화

 

퀘스트를 시스템을 만들어 볼건데, 이를 또 관리해줄 매니저를 만들어 보자.

Create Empty를 통해 QuestManager 오브젝트와 C# Script인 QuestManager을 만들고 오브젝트에 넣어주자.

 

그리고 저번에 오브젝트 데이터 관련 스크립트 ObjData.cs를 만들었었다.

이번에는 퀘스트와 관련된 데이터 스크립트 QuestData.cs를 만들자.

 

이번에는 ObjData처럼 오브젝트에다가 넣을 것이 아니다.

코드에서 불러서 쓸 것이다

 

QuestData의 MonoBehaviour을 지울 것이다.

그럼 ' using UnityEngine; '도 필요하지 않을 것이다.

 

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

public class QuestData
{
   
}

 

QuestData.cs을 우선 이런 꼴로 나타내 주면 된다.

그리고 QuestData를 구동하는 변수는 아래와 같이 두 가지이다.

 

public class QuestData
{
    public string questName;
    //퀘스트 타이틀 문자열
    public int[] npcId;
    //퀘스트와 연관되어 있는 npc Id를 저장하는 int형 배열
}

 

다음은 QuestManager.cs에 필요한 변수들이다.

 

public int questId;
//지금 진행중인 퀘스트 아이디

Dictionary<int, QuestData> questList;
//앞에 TalkManager에서 대사 관리했던 것처럼
//퀘스트 데이터를 저장할 Dictionary 변수 생성

 

다음은 Awake()를 통해 자동으로 초기화를 하고 데이터를 불러오는 함수를 실행하게 하자.

 

void Awake()
{
    questList = new Dictionary<int, QuestData>();
    GenerateData();
}

void GenerateData()
{
    questList.Add(10, new QuestData("마을 사람들과 대화하기.", new int[] { 1000, 2000 }));
    //왼쪽이 퀘스트 ID, 오른쪽에는 그 ID에 대한 questName과 Npc ID가 들어있다.
    //int[]처럼 사이즈를 안 적어준 이유는 이미 크기가 정해졌기 때문

}

 

이때 QuestData를 사용하기 위해선 생성자 함수를 만들어줘야 한다.

생성자 함수는 QuestData.cs에 만들어 주자.

 

public QuestData(string name,int[] npc)      //구조체 생성을 위해 매개변수 생성자 작성
{
    questName = name;
    npcId = npc;    
}

 

그리고 QuestTalkIndex를 반환하는 함수를 만들자.

QuestTalkIndex에 대해서는 나중에 제대로 알아보도록 하자.

 

public int GetQuestTalkIndex(int id)
{
    return questId;
}
// NPC ID를 받고 퀘스트 번호를 반환하는 함수 생성
//QuestManager.cs에 작성할 것

 

이제 이 함수를 사용할 것인데, 어디에 쓸거냐가 문제다.

GameManager에다가 사용하겠다.

QuestManager 변수를 생성하자.

 

public QuestManager questManager;

 

Talk()함수를 고쳐보자.

 

void Talk(int id, bool isNpc)
{
    //Set Talk Data
    int questTalkIndex = questManager.GetQuestTalkIndex(id);
    string talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex);

    //End Talk
    if(talkData==null)
    {
        isAction = false;
        talkIndex = 0;
        return;
    }

    //Continue Talk
    if(isNpc)
   {
         talkText.text = talkData.Split(':')[0];

         portraitImg.sprite = talkManager.GetPortrait(id, int.Parse(talkData.Split(':')[1]));
         portraitImg.color = new Color(1, 1, 1, 1);
    }

    else
    {
         talkText.text = talkData;

         portraitImg.color = new Color(1, 1, 1, 0);
    }

    isAction = true;
    talkIndex++;
}

 

앞에 talkdata를 불러오는 과정을 수정하였다.

그런데 TalkManager.cs를 보면 questIndex가 10인데 1000+10의 key가 들어있는 정보가 없다.

그래서 TalkManager.cs에서 QuestTalkData(퀘스트 번호 + NPC ID에 해당하는 대화 데이터)를 추가해주자.

 

//Quest Talk Data - TalkManager.cs -> GenerateData()에 들어갈 것
talkData.Add(1000 + 10, new string[] { "어서 와:0", "이 마을에 놀라운 전설이 있다는데:1", "오른쪽 호수 쪽에 루도가 알려줄꺼야.:2" });

 

이러고 우선 저장하여 잘 실행되는 지 확인해 보자.

 

QuestManager 오브젝트의 Inspector 창안에 Quest Id에다가 10을 넣어주고, 오브젝트를 GameManager 안에다가 넣어주자.

 

 

 

실행하게 된다면

 

 

1차적으로 잘 작동하는 것을 확인할 수 있다.

 

 

다음으로는 NPC B의 퀘스트 대사도 써주기로 하자.

 

talkData.Add(2000 + 10, new string[] { "여어..:1",
                                       "이 호수의 전설을 들으러 온거야?:0",
                                       "그럼 일 좀 하나 해주면 좋을텐데...:1",
                                       "내 집 근처에 떨어진 동전 좀 주워줬으면 해:0"});

 

실행되면 대사는 잘 뜨긴 하지만,

문제는 스토리상이라면 NPC A의 말을 먼저 듣고 B의 말이 실행이 되어야 할텐데,

바로 NPC B로 스킵을 할 수가 있다.

 

퀘스트 순서를 만들어 보도록 하겠다.

 

 public int questActionIndex;
//퀘스트 대화순서 변수 생성

public int GetQuestTalkIndex(int id)
{
    return questId + questActionIndex;
}
    // NPC ID를 받고 퀘스트 번호를 반환하는 함수 생성

 

변수를 하나 만들어 주었고, 함수를 수정하였다.

GetQuestTalkIndex가 quest 번호와 quest 대화순서를 더하여 quest 대화 Id를 출력한다.

그리고 대화 진행을 위해 퀘스트 대화 순서를 올리는 함수를 만들겠다.

 

 public void CheckQuest()		//QuestManager.cs에 작성할 것
{
    questActionIndex++;
}

 

그러면 이 함수는 대화가 끝났을 때 실행해야 한다.

 

//End Talk
if(talkData==null)
{
        isAction = false;
        talkIndex = 0;
        questManager.CheckQuest();
        return;
}

 

Npc A에게 갔다가 Npc B로 가는 게 맞으므로 Npc A와의 대화가 끝난 후에 CheckQuest를 통하여

questActionIndex가 1 올라갈 것이다.

GetQuestTalkIndex에서 10+1이 되므로 코드를 다음과 같이 수정한다.

 

 talkData.Add(2000 + 11, new string[] { "여어..:1",
                                        "이 호수의 전설을 들으러 온거야?:0",
                                        "그럼 일 좀 하나 해주면 좋을텐데...:1",
                                        "내 집 근처에 떨어진 동전 좀 주워줬으면 해:0"});

 

그럼 순서에 맞게 대화를 했을 때만, 퀘스트 대화 순서를 올리도록 작성해보자.

 

public void CheckQuest(int id)
{
    if (id == questList[questId].npcId[questActionIndex]) 
        questActionIndex++;
}

 

코드의 뜻을 살펴보자

if문 안의 뜻은, 현재 말하고 있는 대상의 Id가 Dictionary인 questlist의 키와 연결시켜,

QuestData를 불러오고, '.' 을 사용하여 안의 npcId 배열 중 questActionIndex번째 원소와 같으면 하나 증가시키는 뜻이다.

 

Quest Action Index가 0이다.

 

Quest Action Index가 2이다.

잘 실행되는 것을 확인할 수 있다.

 

퀘스트 진행

 

현재 퀘스트가 하나 밖에 없다. 하나를 더 만들어보자.

 

questList.Add(20, new QuestData("루도의 동전 찾아주기.", new int[] { 5000, 2000 }));

 

Npc A와 Npc B와 대화를 다 나눴으면, questId를 올리면 된다.

QuestManager.cs에다가 다음 함수로 가는 것을 관리해주는 함수를 작성해보자.

 

void NextQuest()
{
    questId += 10;
    questActionIndex = 0;
}

 

이 함수는 CheckQuest()에 들어가야 한다.

questActionIndex를 늘리다보면 모든 Npc와 대화 후 배열의 마지막 인덱스보다 커진다.

그래서 배열의 길이와 같아진다면 NextQuest()를 실행하면 된다.

 

public void CheckQuest(int id)
{
    if (id == questList[questId].npcId[questActionIndex]) 
        questActionIndex++;

    if (questActionIndex == questList[questId].npcId.Length)
        NextQuest();
}

 

우선 퀘스트가 어떻게 변하는지 확인하기 위하여 코드를 수정해보겠다.

퀘스트 이름을 반환하게끔 함수를 개조 해보겠다.

 

public string CheckQuest(int id)
{
    if (id == questList[questId].npcId[questActionIndex]) 
        questActionIndex++;

   if (questActionIndex == questList[questId].npcId.Length)
        NextQuest();

   return questList[questId].questName;
}

 

그리고 출력하게끔 GameManager.cs에서 고쳐주면 된다.

 

//End Talk
if(talkData==null)
{
        isAction = false;
        talkIndex = 0;
        Debug.Log(questManager.CheckQuest(id));
        return;
}

 

실행하면

 

 

두 Npc와 대화 후 Console 창에서 현재 퀘스트 이름을 확인할 수 있다.

 

퀘스트 진행 오브젝트

 

현재 우리가 만든 퀘스트의 마지막 내용은 ' 동전 찾아주기 '이다.

우선 동전 오브젝트를 만들어주자.

 

코인 오브젝트 정보

 

코인 오브젝트를 끄고 나중에 퀘스트를 받은 후 코인이 생겨나게 하고,

주워먹은 뒤 완수하는 것을 목표로 하겠다.

 

우선 코인을 불러올 수 있어야 한다.

 

public GameObject[] questObject;
//퀘스트 오브젝트를 저장하고 불러올 배열 생성

 

이렇게 저장하면 Editor에서 QuestManager을 선택하게 되면

 

 

위와 같이 바뀔 건데 새로 생긴 Quest Object 배열의 사이즈를 1로 잡아주고,

안에다가 Coin 오브젝트를 넣는다.

 

그러면 아래와 같은 코드를 작성해보자.

 

//퀘스트 번호와 퀘스트 대화 순서에 따라 오브젝트 조절하는 함수
void ControlObject()
{
    switch(questId)
    {
         case 10:
         if(questActionIndex == 2)
                questObject[0].SetActive(true);
         break;
         case 20:
         if(questActionIndex == 1)
                questObject[0].SetActive(false);
         break;
    }
}

 

SetActive를 이용하여 오브젝트를 켰다 껐다한다.

Npc B와 얘기를 하고 나면, questActionIndex가 1에서 2로 바뀌고 if문이 한번 더 실행 되면서 0이 된다.

2에서 0이 되는 사이에 동전이 켜지면 된다.

따라서 ControlObject를 두 If문 사이에 넣으면 된다.

 

public string CheckQuest(int id)
{
    //Next Talk Target
    if (id == questList[questId].npcId[questActionIndex]) 
        questActionIndex++;

    //Control Quest Object
    ControlObject();

    //Talk Complete & Next Quest
    if (questActionIndex == questList[questId].npcId.Length)
         NextQuest();

    //Quest Name
    return questList[questId].questName;
}

 

다음은 대사를 써주면 된다.

 

talkData.Add(1000 + 20, new string[] {"루도의 동전?:1",
                                      "돈을 흘리고 다니면 못쓰지!:3",
                                      "나중에 루도에게 한 마디 해야겠어!!:3"});
talkData.Add(2000 + 20, new string[] {"찾으면 꼭 좀 가져다 줘.:1"});
talkData.Add(2000 + 21, new string[] {"엇, 찾아줘서 고마워..:2"});
talkData.Add(5000 + 20, new string[] {"근처에서 동전을 찾았다."});

 

저장하고 실행해보자.

 

 

이후 동전이 사라지면 잘 따라온 것이다.

Npc B에게 다시 말을 걸면 다음 퀘스트가 없어서 Error가 난다.

그래서 마무리 하는 퀘스트를 만들어보겠다.

 

//QuestManager.cs - Generatedata()에 작성할 것!
questList.Add(30, new QuestData("퀘스트 올 클리어!", new int[] { 0 }));

 

예외 처리

 

Npc  A의 경우 동전을 먹기 전과 먹은 후에 상호작용이 있고 없고의 차이가 있다. 

그 이유는, 우리가 QuestTalkData에다가 Id에 따른 대사를 안 넣어놓았기 때문이다.

 

그럼 일일이 넣어줘야하는 번거러움이 생길 수 있다.

해당 퀘스트 진행 순서 대사가 없을 때, 퀘스트 맨 처음 대사를 갖고 오게 한다.

 

GetTalk()함수를 고쳐보자.

 

public string GetTalk(int id, int talkIndex)
{
     if(!talkData.ContainsKey(id))
     {
     if (talkIndex == talkData[id - id % 10].Length)
            return null;
     else
            return talkData[id - id % 10][talkIndex];
     }

     if (talkIndex == talkData[id].Length)
            return null;
     else
            return talkData[id][talkIndex];
}

 

ContainsKey() : Dictionary에 Key가 있는지 없는지 검사하는 함수

 

이것을 꼭 써주어야 하는 이유가 그냥 10으로 나눈 나머지를 빼버리면

원래 더해서 실행해야 할 것도 빼버려서 오류가 생겨버린다.

 

아무튼 다음은 상자와 책상의 대사가 안 나오는 부분을 해결해 보겠다.

이 두 개의 오브젝트는 퀘스트 진행도와 상관없이 그대로 유지해야 한다.

 

원리는 똑같다. 수를 잘 이용해서 원하는 값을 출력하면 된다.

상자나 책상의 경우 몇 천번대의 Id를 갖고 있기 때문에 10으로 나눈 나머지를 뺀 키가

ContainsKey()를 통하여 존재하지 않는 경우를 찾아 100으로 나누는 과정을 이용하여

작성하면 된다.

 

public string GetTalk(int id, int talkIndex)
{
     if(!talkData.ContainsKey(id))
     {
         if (!talkData.ContainsKey(id-id%10))    //3001번은 존재하지 않음
         {
             if (talkIndex == talkData[id-id%100].Length)
                return null;
             else
                return talkData[id-id%100][talkIndex];
         }
         else
         {
             if (talkIndex == talkData[id - id % 10].Length)
                return null;
             else
                return talkData[id - id % 10][talkIndex];
         }
     }

     if (talkIndex == talkData[id].Length)
        return null;
     else
        return talkData[id][talkIndex];
}

 

 

잘 실행되는 것을 확인할 수 있다.

 

로직 다듬기

 

GetTalk가 지저분해 보이는데 반복되는 부분이 있어보인다.

로직을 다듬어보자.

 

public string GetTalk(int id, int talkIndex)
{
    if(!talkData.ContainsKey(id))
    {
        if (!talkData.ContainsKey(id - id % 10))
            return GetTalk(id - id % 100, talkIndex);      //Get First Talk
         else
                return GetTalk(id-id % 100, talkIndex);        //Get First Quest Talk
    }

    if (talkIndex == talkData[id].Length)
        return null;
    else
        return talkData[id][talkIndex];
}

 

재귀를 사용하여 다듬어 보았다.

 

그리고 마지막으로 우리가 게임 시작을 했을 때, 우리가 어떤 퀘스트를 하고 있는지 알 수 없다.

이거를 처음부터 콘솔창에 띄위는 작업을 해보겠다.

 

void Start()
{
    Debug.Log(questManager.CheckQuest());
}

 

콘솔창에 퀘스트를 띄울려고 한다.

CheckQuest()의 내용을 보면 퀘스트 이름을 반환하는 코드가 있으나,

매개변수로 id를 사용하기 때문에 스캔 대상이 있어야 사용할 수 있다.

 

이를 해결하기 위하여 함수의 오버로드를 사용할 것이다.

즉 같은 이름의 함수를 하나 더 만들겠다는 뜻이다.

 

public string CheckQuest()
{
    return questList[questId].questName;
}