소소한 나의 하루들

탑다운 2d RPG(5) - 퀘스트 시스템 구현하기 본문

개발/유니티

탑다운 2d RPG(5) - 퀘스트 시스템 구현하기

소소한 나의 하루 2024. 2. 1. 00:07

출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared

 

📚 유니티 기초 강좌

유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌

www.youtube.com

이번에는 RPG의 핵심 '퀘스트 시스템'을 만들어본다.


#1. 퀘스트 대화

퀘스트 시스템을 관리할 수 있는 QuestManager 스크립트와 Quest Manager 오브젝트를 생성한다.

퀘스트매니저에서 쓰이는 것은 퀘스트 데이터이다. 그래서 퀘스트 데이터가 어떻게 구성되어있는지 살펴본다.

이전에 오브젝트들에게 Id와 NPC인지 아닌지 isNPC를 부여하는 스크립트 ObjectData를 만들어서 적용시켰었다.

이번에도 새로 Quest Data 정보를 담는 QuestData 스크립트를 생성한다. 이번에는 오브젝트에 적용시키는 것이 아니고, 코드에서 불러서 사용한다. 따라서 Monobehaviour를 지운다. 그러면 이제 using UnityEngine; 부분도 필요없어졌다.

 

QuestData에서 필요한 것은, 퀘스트 변수를 규정하고 있는 변수 2가지이다. 

1) 퀘스트 이름을 담고있는 string형 변수
2) '그 퀘스트와 연관되어있는 NPC Id'를 저장하고 있는 int형 배열

 

QuestManager에서 필요한 것은

1) 지금 진행중인 퀘스트 아이디인 int형 변수

2) 퀘스트 데이터를 저장할 Dictionary 변수 ← 퀘스트 id / (퀘스트 이름 + 퀘스트 연관 NPCid 갖고있는 배열)

//QuestData
using System.Collections;
using System.Collections.Generic;

public class QuestData
{
    public string questName;
    public int[] npcId;

    public QuestData(string name, int[] npc)
    {
        questName = name;
        npcId = npc;
    }
}

//QuestManager
void GenerateData()
{
    questList.Add(10, new QuestData("마을 사람들과 대화하기", new int[] {1000, 2000}))
}

QuestManager 스크립트에서 퀘스트 데이터를 저장할 GenerateData()함수를 만들고, 여기에서 Dictionary 변수인questList에 Add() 함수로 'QuestId'와 'QuestData'를 저장한다. (여기서 Add()로 넣어준 10이 QuestId이다.)

QuestData는 questName과 npcId가 필요하다. 그러면 구조체를 만들어야한다.

그리고 QuestData 컴포넌트에서 구조체 생성을 위해 매개변수 생성자를 작성한다. 최상위 클래스인 QuestData와 이름이 같게 구조체를 만들어주고, questName 문자열 변수와 npcId int형 변수를 그대로 채워줄 매개변수 생성자를 위처럼 작성해준다. 생성자 포멧에 맞도록 QuestManager에서 작성해주면 된다.

questList.Add(10, new QuestData("마을 사람들과 대화하기", new int[] { 1000, 2000 }));

QuestManager에서 QuestData() 호출 시, int[]에는 해당 퀘스트에 연관된 NPC Id를 입력해준다.
("마을 사람들과 대화하기"라는 퀘스트에 연관된 NPC Id는 1000과 2000이다.)

 

그리고 NPC Id를 받고 퀘스트 번호(QuestTalkIndex)를 반환하는 함수를 생성한다. (GetQuestTalkIndex())

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

그 다음 GameManager에서 QuestManager를 변수로 생성 후, 대화창에 초상화와 텍스트 출력을 관리하는 Talk()에다 

작성해서 퀘스트 번호를 가져온다.

우리가 대화를 할 때 TalkManager의 GetTalk()에서 id를 받아서 거기에 해당하는 대화를 가지고 와서 talkData에 저장한다.
그런데 그 id에다가 GetQuestTalkIndex()에서 퀘스트번호를 반환받아 값을 갖고있는 questTalkIndex를 더한다.

NPC id(id) + 퀘스트번호(questTalkIndex) = 퀘스트 대화 데이터 Id

void GenerateData()
{
    //Talk Data
    talkData.Add(1000, new string[] { "안녕?:1", "이곳에 처음 왔구나?:2" });
    talkData.Add(2000, new string[] { "좋은아침?:4", "밥은 먹었니?:6" });

    talkData.Add(100, new string[] { "평범한 집이다. 누가 살고있을까?" });
    talkData.Add(200, new string[] { "평범한 나무다." });
    talkData.Add(300, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });
    talkData.Add(400, new string[] { "평범한 나무상자다." });
    talkData.Add(500, new string[] { "속이 비어있는 것 같은 박스다." });

    //Quest Talk
    talkData.Add(1000 + 10, new string[] { "어서 와:0", "이 마을에는 놀라운 전설이 있다는데:1", "오른쪽 호수 쪽에 루도가 알려줄꺼야.:0" });
    talkData.Add(2000 + 10, new string[] { "반가워.:5", "이 호수의 전설을 들으러 온거야?:6", "그럼 일 좀 하나 해주면 좋을텐데..:5", "내 집 근처에 떨어진 동전 좀 주워줬으면 해. 10개만 주워다주면 고맙겠어.:4"}); "오른쪽 호수 쪽에 루도가 알려줄꺼야.:0" });
    ...
}

이제 TalkManager에서 대화와 초상화 데이터를 갖고있는 GenerateData()에 NPC Id + 퀘스트번호(questTalkIndex)에 해당하는 대화 데이터를 작성해준다.

그러면 이제 퀘스트를 시작하게되면 //Talk Data가 아니라 //Quest Talk의 데이터가 나와야한다.

 

그리고 실행하기 전에 Quest Manager 오브젝트에 QuestManager 스크립트를 드래그 적용시켜주고, 그 후에 Game Manager 오브젝트에 Quest Manager 오브젝트를 드래그 적용시켜준다.

그런데 이렇게하면 이 상태로는 순서 상관없이 대화가 진행된다. (퀘스트 진행 순서를 무시한다 : 무시되는 NPC 발생)

보통 퀘스트는 순번이 정해져있다.

questList.Add(10, new QuestData("마을 사람들과 대화하기", new int[] { 1000, 2000 }));

그래서 QuestManager에 넣어준 new int[] {1000, 2000}에서 1000, 2000이 퀘스트 순번이다.

10은 questId의 첫번째 순서인걸까..

//QuestManager
public int GetQuestTalkIndex(int id)
{
    return questId + questActionIndex;
}

public void CheckQuest()
{
    questActionIndex++;
}

//GameManager
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;
        questManager.CheckQuest();
        return;
    }
    ...
}

그러면 이제 퀘스트 순서를 만들어보도록 한다. QuestManager에 퀘스트 대화순서 변수를 생성한다. (questActionIndex)

이 변수는 위의 1000과 2000을 가져올 때 쓰는 부가적인 인덱스이다.

앞에서 QuestManager의 GetQuestTalkIndex()에 적었던 return 값을 퀘스트번호(questId) + 퀘스트대화순서(questActionIndex)로 적는다.

퀘스트번호(questId) + 퀘스트대화순서(questActionIndex) = 퀘스트 대화 Id

처음에는 10 + 0 = 10 대화를 끝내면, 10 + 1 = 11이 된다. 퀘스트 대화가 끝나면 퀘스트 대화순서(questActionIndex)가 가산된다(올라간다).

: 대화 진행을 위해 퀘스트 대화순서를 올리는 함수를 생성한다.

//TalkManager
void GenerateData()
{
    //Talk Data
    talkData.Add(1000, new string[] { "안녕?:1", "이곳에 처음 왔구나?:2" });
    talkData.Add(2000, new string[] { "좋은아침?:4", "밥은 먹었니?:6" });

    talkData.Add(100, new string[] { "평범한 집이다. 누가 살고있을까?" });
    talkData.Add(200, new string[] { "평범한 나무다." });
    talkData.Add(300, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });
    talkData.Add(400, new string[] { "평범한 나무상자다." });
    talkData.Add(500, new string[] { "속이 비어있는 것 같은 박스다." });

    //Quest Talk
    talkData.Add(1000 + 10, new string[] { "어서 와:0", "이 마을에는 놀라운 전설이 있다는데:1", "오른쪽 호수 쪽에 루도가 알려줄꺼야.:0" });
    talkData.Add(2000 + 11, new string[] { "반가워.:5", "이 호수의 전설을 들으러 온거야?:6", "그럼 일 좀 하나 해주면 좋을텐데..:5", "내 집 근처에 떨어진 동전 좀 주워줬으면 해. 10개만 주워다주면 고맙겠어.:4"});
    ...
    }

퀘스트 대화가 끝나면, questActionIndex가 1 올라가면서 이제는 10이 아니라 퀘스트ID가 11이 된다.

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

이제 CheckQuest()와 GenerateData()의 쓰임새를 연결시켜본다.

//QuestManager
void GenerateData()
{
    questList.Add(10, new QuestData("마을 사람들과 대화하기", new int[] { 1000, 2000 }));
}

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

//QuestData
using System.Collections;
using System.Collections.Generic;

public class QuestData
{
    public string questName;
    public int[] npcId;

    public QuestData(string name, int[] npc)
    {
        questName = name;
        npcId = npc;
    }
}

CheckQuest에서 대화를 나눴던 NPC Id를 받아온다. 정해진 순서에 맞게 NPC와 대화했을 때만 퀘스트 대화순서를 올리도록 작성한다.

그래서 정해진 순서가 아닌 NPC와 대화를 나눴을 때는 퀘스트 대화 순서가 올라가지 않는다.

if(id == questList[questId].npcId[questActionIndex])

[questId] 지금 현재 진행중인 퀘스트 Id

그러면 이제 퀘스트 진행중인 QuestData를 가져온다.  (구조체 QuestData)

여기서 .npcId 해서 npcId를 담고있는구조체 내부 int형 배열에 접근한다. {1000, 2000}


#2. 퀘스트 진행

이제 퀘스트 Id 10번에서 첫번째 NPC 1000번, 두번째 NPC 2000번과 대화를 다 나눴으면, 다음 퀘스트를 수행하기 위해 퀘스트 Id(questId)를 올리면 된다.

따라서 다음 퀘스트를 위한 함수를 생성한다. (NextQuest()) 여기서는 questId를 일정 값만큼 가산하고, questActionIndex는 다시 0으로 초기화한다.

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

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

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

이제 이 함수 NextQuest()를 퀘스트 순번대로 NPC 순서를 넘기는 CheckQuest()에서 활용한다.

: 퀘스트 대화순서가 끝에 도달했을 때 퀘스트Id 번호 증가

 

//QuestManager
void GenerateData()
{
    questList.Add(10, new QuestData("마을 사람들과 대화하기", new int[] { 1000, 2000 }));
    questList.Add(20, new QuestData("루도의 동전 찾아주기", new int[] { 2000, 1000 }));
}

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

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

    return questList[questId].questName; //진행중인 퀘스트의 questName을 뽑아서 반환
}

/GameManager
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;
        questManager.CheckQuest(id);
        Debug.Log(questManager.CheckQuest(id));
        return;
    }
    ...
}

다음 퀘스트로 변환했는지 확인하기 위해 questName을 콘솔에 보여주도록 코드를 수정한다.

 

그러면 이제 QuestManager에서 이번 퀘스트에서 다음에 대화해야할 NPC를 확인해주고, 퀘스트가 끝났으면, 다음 퀘스트 확인해줄 수 있다. + 현재 퀘스트 이름까지 같이 확인가능.


#3. 퀘스트 오브젝트

그런데, '루도의 동전'을 찾아줘야하는데 동전 오브젝트를 만들지 않았다. 그래서 만들어준다.

저번에 2d 플랫포머 프로젝트 진행할때 사용했던 에셋을 설치해준다. 그리고 Coin 오브젝트에 동전 sprite를 적용시켜주고, ObjectData 스크립트를 적용시켜준다. (Id는 5000)
그 후 오브젝트가 안보이도록 비활성화시킨다. 퀘스트를 받은 후에 동전이 보이도록 하겠다.

 

퀘스트 오브젝트를 저장할 배열을 선언하고, 에디터에서 아까 만들어줬던 Coin 오브젝트를 Quenst Manager의 스크립트에 드래그 적용시킨다. 그러면 이제 Coin 오브젝트 관리가 가능하고, 어떠한 조건에 의하여 Coin 오브젝트를 Active해주면 된다.

//TalkManager
void GenerateData()
{
    //Talk Data
    talkData.Add(1000, new string[] { "안녕?:1", "이곳에 처음 왔구나?:2" });
    talkData.Add(2000, new string[] { "좋은아침?:4", "밥은 먹었니?:6" });

    talkData.Add(100, new string[] { "평범한 집이다. 누가 살고있을까?" });
    talkData.Add(200, new string[] { "평범한 나무다." });
    talkData.Add(300, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });
    talkData.Add(400, new string[] { "평범한 나무상자다." });
    talkData.Add(500, new string[] { "속이 비어있는 것 같은 박스다." });

    //Quest Talk
    talkData.Add(1000 + 10, new string[] { "어서 와:0", "이 마을에는 놀라운 전설이 있다는데:1", "오른쪽 호수 쪽에 루도가 알려줄꺼야.:0" });
    talkData.Add(2000 + 11, new string[] { "반가워.:5", "이 호수의 전설을 들으러 온거야?:6", "그럼 일 좀 하나 해주면 좋을텐데..:5", "내 집 근처에 떨어진 동전 좀 주워줬으면 해. 10개만 주워다주면 고맙겠어.:4"});

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

//QuestManager
void GenerateData()
{
    questList.Add(10, new QuestData("마을 사람들과 대화하기", new int[] { 1000, 2000 }));
    questList.Add(20, new QuestData("루도의 동전 찾아주기", new int[] { 1000, 2000, 5000, 2000}));
}

void ControlObject()
{
    switch (questId)
    {
        case 10:
            if (questActionIndex == 2) //퀘스트1에서의 대화가 모두 끝났을 때(퀘스트2 진입)
                questObject[0].SetActive(true);
            break;
        case 20:
            if (questActionIndex == 3) //동전 먹고난 후
                questObject[0].SetActive(false);
            break;
    }
}

이제 퀘스트 오브젝트를 관리할 함수를 생성한다. (ControlObject()) questId에 따라서 오브젝트가 활성화될 수 있게 관리해준다. 퀘스트 번호, 퀘스트 대화순서에 따라 오브젝트를 조절해준다.

퀘스트 10일 때 대화가 다 끝나면 퀘스트 오브젝트 Coin은 true로 설정하고, 그 다음 퀘스트 20 첫 대화에서 퀘스트 오브젝트 Coin은 false로 한다. (CheckQuest() 안에 입력해줘서 대화가 끝난 후 보여줘야하거나 숨겨야할 퀘스트 오브젝터가 있으면 ControlObject()를 실행해서 오브젝트를 컨트롤해준다.)

 

퀘스트 진행 중, '퀘스트 진행을 유도'하는 일상 대화 넣기

//QuestManager
void GenerateData()
{
    questList.Add(10, new QuestData("마을 사람들과 대화하기", new int[] { 1000, 2000 }));
    questList.Add(20, new QuestData("루도의 동전 찾아주기", new int[] { 1000, 2000, 5000, 2000}));
}

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

    //Control Quest Object
    ControlObject();

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

    return questList[questId].questName;
}

//TalkManager
void GenerateData()
{
    //Talk Data
    talkData.Add(1000, new string[] { "안녕?:1", "이곳에 처음 왔구나?:2" });
    talkData.Add(2000, new string[] { "좋은아침?:4", "밥은 먹었니?:6" });

    talkData.Add(100, new string[] { "평범한 집이다. 누가 살고있을까?" });
    talkData.Add(200, new string[] { "평범한 나무다." });
    talkData.Add(300, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });
    talkData.Add(400, new string[] { "평범한 나무상자다." });
    talkData.Add(500, new string[] { "속이 비어있는 것 같은 박스다." });

    //Quest Talk
    talkData.Add(1000 + 10, new string[] { "어서 와:0", "이 마을에는 놀라운 전설이 있다는데:1", "오른쪽 호수 쪽에 루도가 알려줄꺼야.:0" });
    talkData.Add(1000 + 11, new string[] { "아직 못만났어?:0", "루도는 오른쪽 호수 쪽에 있어.:1"});
    talkData.Add(2000 + 11, new string[] { "반가워.:5", "이 호수의 전설을 들으러 온거야?:6", "그럼 일 좀 하나 해주면 좋을텐데..:5", "내 집 근처에 떨어진 동전 좀 주워줬으면 해. 10개만 주워다주면 고맙겠어.:4"});

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

    //Portrait Data
    portraitData.Add(1000 + 0, portraitArray[0]);
    portraitData.Add(1000 + 1, portraitArray[1]);
    portraitData.Add(1000 + 2, portraitArray[2]);
    portraitData.Add(1000 + 3, portraitArray[3]);

    portraitData.Add(2000 + 4, portraitArray[4]);
    portraitData.Add(2000 + 5, portraitArray[5]);
    portraitData.Add(2000 + 6, portraitArray[6]);
    portraitData.Add(2000 + 7, portraitArray[7]);
}

 

퀘스트가 진행되는 '순서'를 QuestManager의 GenerateData()에 배열로 나열해놓고, 퀘스트 진행 중에 퀘스트 관련 대화를 끝낸 (퀘스트 진행 외의) NPC와 대화할때 "퀘스트 진행을 유도"하는 목적으로 약간 일상적인 대화를 할 수도 있다.

여기서 '퀘스트 진행 중'에 [questActionIndex]가 어떻게 가산될지를 계산하면서 퀘스트 진행 중, 퀘스트 외적인 일상적인 대화를 작성해야한다.

 

그리고 실행시켜보면, 퀘스트가 끝나는 시점에 마지막 퀘스트 대사 "엇 찾아줘서 고마워" 부분의 대화창이 종료되지 않고 에러가 발생하는데 다음 퀘스트가 지정되지 않았기 때문이다.

void GenerateData()
{
    questList.Add(10, new QuestData("마을 사람들과 대화하기", new int[] { 1000, 2000 }));
    questList.Add(20, new QuestData("루도의 동전 찾아주기", new int[] { 1000, 2000, 5000, 2000}));
    questList.Add(30, new QuestData("퀘스트를 클리어하였습니다.", new int[] { 0 }));
}

그래서 퀘스트를 클리어했다는 마지막 대화가 나오도록 했다. (더미데이트 0)


#4. 예외 처리

그런데 문제가 하나 더 있다. 대사를 고려하지 않은 NPC에게 말을 걸면 위와 같은 에러가 나타난다.

그렇다면 모든 NPC에게 모든 상황마다 전부 대사를 입력해놔야하는 것일까?

 

에러가 나는 상황은 'Key(npcId)가 입력되지 않은(=대사가 설정되지 않은) NPC에게 대화를 걸었을 때' 이다.

그래서 이 경우에는 Dictionary 변수 안에 Key가 존재하는지 검사하는 ContainsKey() 함수를 사용한다.

ContainsKey() : Dictionary에 Key가 존재하는지 검사하는 함수

//TalkManager
void GenerateData()
{
    //Talk Data
    talkData.Add(1000, new string[] { "안녕?:1", "이곳에 처음 왔구나?:2" });
    talkData.Add(2000, new string[] { "좋은아침?:4", "밥은 먹었니?:6" });

    talkData.Add(100, new string[] { "평범한 집이다. 누가 살고있을까?" });
    talkData.Add(200, new string[] { "평범한 나무다." });
    talkData.Add(300, new string[] { "누군가 사용했던 흔적이 있는 책상이다." });
    talkData.Add(400, new string[] { "평범한 나무상자다." });
    talkData.Add(500, new string[] { "속이 비어있는 것 같은 박스다." });

    //Quest Talk
    talkData.Add(1000 + 10, new string[] { "어서 와:0", "이 마을에는 놀라운 전설이 있다는데:1", "오른쪽 호수 쪽에 루도가 알려줄꺼야.:0" });
    talkData.Add(1000 + 11, new string[] { "아직 못만났어?:0", "루도는 오른쪽 호수 쪽에 있어.:1"});
    talkData.Add(2000 + 11, new string[] { "반가워.:5", "이 호수의 전설을 들으러 온거야?:6", "그럼 일 좀 하나 해주면 좋을텐데..:5", "내 집 근처에 떨어진 동전 좀 주워줬으면 해. 10개만 주워다주면 고맙겠어.:4"});

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

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

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

만약 talkData에 id를 가진 key가 없다면, 퀘스트 대화순서 제거 후 재탐색을 한다.

지금 id = 1000 + 21 key는 코드가 작성되어있지 않은 상태이다. 따라서 Id 1000인 NPC에게 말을 걸면 에러가 발생할텐데, key가 있는지 확인하고 없다면 return talkData[id - id % 10][talkIndex];를 반환한다.

결국 (1000 + 21) - (1021 % 10 = 1) = 1020이 된다. 1020은 1000 + 20이니까, 코드가 작성되어있고, 이렇게 뒤로 돌아가서 데이터를 찾아준다.

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];
}

※물론 다시 재탐색하는 대화도 끝이 있으므로 마지막에는 null값을 반환한다.

이렇게 퀘스트 진행 중 순서 대사가 없을 때 해당 NPC의 이전 퀘스트 대사를 가지고 온다.

 

퀘스트 진행과 상관없는 오브젝트/NPC 또는 퀘스트 진행 이전 NPC 대사 출력 (일상적인 기본 대화)

나무상자는 퀘스트 진행과 상관없다. 따라서 퀘스트 진행 중에도 나무상자는 "평범한 나무상자다"라는 대사를 유지하고 싶다.

따라서 (퀘스트 영역 내에서) 퀘스트 맨 처음 대사마저 없을 때는 어떻게 해야할지 결정해야한다.

→그럴 때는 기본 대사를 가지고 오면 된다. : questId를 빼버리면 된다. (퀘스트번호까지 제거 후 재탐색)

//TalkManager
public string GetTalk(int id, int talkIndex)
{
    if (!talkData.ContainsKey(id))  //현재 퀘스트 대사가 없을 때
    {
        if (!talkData.ContainsKey(id - id % 10))
        {
            //퀘스트 맨 처음 대사마저 없을 때
            //기본 대사를 가지고 온다.
            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];
}

#5. 로직 다듬기

if (talkIndex == talkData[id - id % 100].Length)
    return null;
else
    return talkData[id - id % 100][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);
        }
        else
        {
            //해당 퀘스트 진행 중 현재 대사가 없을 때
            //이전 퀘스트 대사를 가지고 온다.
            return GetTalk(id - id % 10, talkIndex);
        }   
    }

    //현재 퀘스트 대사 반환
    if (talkIndex == talkData[id].Length)
        return null;
    else 
        return talkData[id][talkIndex];
}public string GetTalk(int id, int talkIndex)
{
    if (!talkData.ContainsKey(id)) //현재 퀘스트 대사가 없다면
    {
        if (!talkData.ContainsKey(id - id % 10))
        {
            //퀘스트 맨 처음 대사마저 없을 때
            //기본 대사를 가지고 온다.
            GetTalk(id - id % 100, talkIndex);
        }
        else
        {
            //해당 퀘스트 진행 중 현재 대사가 없을 때
            //이전 퀘스트 대사를 가지고 온다.
            GetTalk(id - id % 10, talkIndex);
        }   
    }

    //현재 퀘스트 대사 반환
    if (talkIndex == talkData[id].Length)
        return null;
    else 
        return talkData[id][talkIndex];
}

여기서 반환 값(return문)이 있는 재귀함수return 까지 꼭 써줘야한다.


그리고 하나의 문제가 더 있다.

우리가 게임을 실행시켰을 때, 어떤 퀘스트를 하고있는지 파악할 방법이 없다. 반드시 NPC와 대화를 해야만 퀘스트이름을 콘솔창을 통해 확인할 수 있다.

그래서 아예 처음부터 퀘스트 이름을 가져오고 싶다.

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

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

    //Control Quest Object
    ControlObject();

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

    return questList[questId].questName;
}

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

QuestManager의 CheckQuest()를 이용해 퀘스트 이름을 가져오려고 하니, CheckQuest()는 매개변수로 id를 받는다. 하지만 맨 처음에는 NPC Id가 없기 때문에 CheckQuest에서 자체적으로 코드를 수정하도록 한다.

 

코드를 수정하고나서보니, CheckQuest() 함수가 2개 생겼다. 같은 이름이지만 하나는 매개변수 id를 받고 하나는 매개변수를 안받는다.

 

오버로딩(Overloading) : 매개변수에 따라 함수를 호출한다. (함수 이름이 같다.)

 

그래서 '매개변수에 따라서' 함수를 호출할 것이다. CheckQuest() 함수를 호출할 때 매개변수를 주지 않으면 아래쪽 함수가 실행될 것이고, 매개변수를 주게되면 위쪽 함수가 실행될 것이다.

이 기술을 '오버로딩'이라고 한다.

 

이제 실행을 하게되면, Start()에서 먼저 퀘스트 내역을 가져오기 때문에 현재 퀘스트 내역을 알 수 있게 되었다.

Comments