본문 바로가기

GAME

[Unity2D] 퀘스트 시스템 구현

728x90

 

[유니티 기초 - B24] RPG퀘스트 시스템 구현하기

를 보고 작성된 게시글 입니다.

 

 

일단 "대화 순서"를 지정해보기 위해서  npc를 두명으로 늘려주고 시작하겠습니다. 

백설공주npc를 하나 추가했고 id=2000

Capsulcolider2D 와 Rigidbody2D(Kinematic)을 추가해줬습니다.

레이어는 "Object"

 

<퀘스트 시나리오 변경>

탈리아에게 말걸기 (열쇠를 가져다 달라는 퀘스트 ) -> 백설공주에게 말걸기 (열쇠 정보얻기) ->열쇠 찾기-> 탈리아에게 가져가서 퀘스트 완료 

이렇게 탈리아(id=1000)에게 먼저 말을 건 후 백설공주(id=2000)에게 말을 걸어야 퀘스트 완료가 가능하다

주인공 얘로 바꿈...ㄱ-

 

 

 

퀘스트 대화

 

퀘스트 정보추가 와 관리

대화 시스템때와 유사하게 

  • 퀘스트를 관리해줄 QuestManager.cs 
  • 퀘스트 데이터를 저장할 QuestData.cs

두개의 스크립트를 생성합니다.

 

퀘스트 데이터에는 퀘스트 이름 , 퀘스트에 관련된 npc의 id(여러명 일 수 있으므로 배열)를 저장합니다 .

QuestData.cs

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

public class QuestData : MonoBehaviour
{
    public string questName; //퀘스트 명
    public int[] NpdId; // 퀘스트에 관련된 NPC id 모음
    
    
    
    
    //빈 생성자
    public QuestData(){}

	//생성자
    public QuestData(string name, int[] npcid)
    {
        questName = name;
        NpdId = npcid;
    }


}

또한 새로운 퀘스트를 생성할 때, 

퀘스트 데이터 또한 생성되어야 하고, 모든 퀘스트에는 위에서 언급한 두가지 변수가 필요하므로

두가지 변수를 입력받아 퀘스트 데이터를 생성하는 생성자를 만들어줍니다. 

 

 

QuestManager.cs

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

public class QuestManager : MonoBehaviour
{

    public int questId;
    Dictionary<int, QuestData> questList;


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

    
    void GenerateData()
    {
    	//생성자 이용 (string name, int[] npcid)
        questList.Add(10, new QuestData("탈리아를 구출해라", new int[]{1000,2000}));
    }

*퀘스트 id는 10번대로 지정한다.

 

퀘스트를 관리해줄 퀘스트 매니저에는 

퀘스트 번호와 퀘스트 번호(questid), 실제 퀘스트 데이터를 가진 dictionary형태의 questList가 존재하고, 이를 초기화 해줍니다.

TalkManager.cs때와 유사하게 questid, QuestData를 가진 데이터를 추가해주는 함수를 만들고,

QuestData.cs에서 만들어둔 사용자를 이용해 퀘스트 리스트에 추가합니다.

 

저는 이 퀘스트를 한번 생성해 보려고 합니다.

퀘스트 번호는 10번, 퀘스트에 관련된 npc인 탈리아의 id는 1000이고, 퀘스트 이름은 "탈리아를 구출해라" 이므로

new QuestData("탈리아를 구출해라", new int[]{1000}) 으로 새로운 QuestData를 생성해 주었습니다. 

 

QuestManager.cs

    public int GetQuestTalkIndex(int id) // Npc Id를 받아 퀘스트 번호를 반환하는 함수 
    {
        return questId;
    }

NPC(object : 오브젝트에는 진짜 오브젝트와 npc가 존재했습니다.)의 id를 받아 questid를 반환하는 함수를 작성합니다.

이렇게 되면 조사한 오브젝트의 id가 quest를 가지는 npc라면 questid를 알아내 어떤 퀘스트를 가졌는지 알 수 있도록 해줄 것 입니다. 

-> 조사 액션시 id를 파악하여 퀘스트를 반환가능 

 

 

 

퀘스트용 대사 

퀘스트를 진행하기 전에는 주로 npc와 대화를 하는데,

우리는 대화를 TalkManager.cs에 저장해 두었습니다. 퀘스트를 진행하는 대화도 마찬가지로 이곳에 저장됩니다.

우리가 npc의 id를 1000번대로 저장한 이유는 다음과 같이

1000 : npc 1의 id

1000 + 0 ~ 1000 + n : npc의 초상화 

1000 + 10*(1~n) : 해당 npc의 퀘스트용 대화 

위와 같이 한 npc에 필요한 다양한 분야를 나누기 위함 이었습니다. 

 

우리가 지정한 quest id = 10 이었으므로

1000 + 10 : npc 1000번의 퀘스트용 대화 

를 생성해 주도록 한다

TalkManager.cs

 void GenerateData()
    {
        //대사 생성 (obj id, 대화 )
        talkData.Add(1000, new string[]{"안녕하세요:0", "문 좀 열어주실래요?:0"});
        talkData.Add(2000, new string[]{"안녕:1", "이 마을에는 처음이니?:1"});



        //퀘스트용 대화(obj id + quest id)
        talkData.Add(1000 + 10, new string[]{"엥? 이런곳에 사람이?!?!:2","저기요! \n저 좀 도와주세요:0","왜 여기 갇혀 계세요?:2","저도 잘 모르겠어요. \n저희 아버지가 좀 이상해서요:0","제가 어떻게 해드리면 될까요?:2","몬스터들이 열쇠를 가지고 있어요. \n그것을 구해다 주시면 제가 좋은 기술을 알려드릴게요!:0","네! 잠시만 기다리세요:2"});
        talkData.Add(2000 + 10, new string[]{"안녕:1", "안녕하세요? \n혹시 열쇠를 구할 수 있는 곳을 아시나요?:2","열쇠? 열쇠라면 문 옆에 떨어져 있는걸 본 것 같아:1","감사합니다:2"});



        //초상화 생성 (obj id + portrait number)  0:Talia 1:SnowWhite 2:Jack
        portraitData.Add(1000 + 0, portraitArr[0]); //0번 인덱스에 저장된 초상화를 id = 1000과 mapping
        portraitData.Add(1000 + 2,portraitArr[2]); //2번 인덱스에 저장된 초상화를 id = 1002과 mapping

        portraitData.Add(2000 + 1,portraitArr[1]);
        portraitData.Add(2000 + 2,portraitArr[2]);
    }

퀘스트용 대화 부분을 추가했다.

 

 

이제 게임메니저에서 대화를 불러오는 부분에 다음과 같이 추가한다.

GameManager.cs

    public QuestManager questManager;
    
    
     void Talk(int id, bool isNPC){

        int questTalkIndex = questManager.GetQuestTalkIndex(id); // 조사한 obj의 id를 넘겨 퀘스트 id를 반환받음 
        
        string talkData = talkManager.GetTalk(id + questTalkIndex, talkIndex); //id에 퀘스트 id를 더하면 -> 해당 id를 가진 오브젝트가 가진 퀘스트의 대화를 반환하게 만들기
        
        
        
        //생략
        }

그동안은 id만 넘겼지만 이제는 questid를 반환받아 id + questid를 넘겨 그에 해당하는 대화를 가지고온다.

 

 

 

이제 유니티에서 Empty Object를 만들어 QuestManager를 만든다 

QuestManager.cs스크립트를 적용하고 questid를 10번으로 지정해준다

 

GameManager에도 QuestManager를 드래그하여 초기화 시킨다

 

 

이제 퀘스트용 대화를 나누는 모습을 볼 수 있다 

 

 

퀘스트 진행 순서

하지만 이렇게 설정한 경우, talia에게 말을 먼저 걸지 않고 백설공주에게 먼저 말을 건 경우

탈리아의 퀘스트를 무시해버리게 됩니다. 

퀘스트진행시 npc의 순서는 1000->2000 이므로, 순서를 지정해 주어야 합니다.

 

 

QuestManager.cs

순서를 표현하기위해 변수를 새로 생성합니다.

public int questActionIndex; //퀘스트 npc대화 순서

 

퀘스트id를 반환할 때, 퀘스트 npc대화 순서를 함께 반환합니다.

    public int GetQuestTalkIndex(int id) // Npc Id를 받아 퀘스트 번호를 반환하는 함수 
    {
        return questId + questActionIndex;
    }

이렇게 되면 퀘스트 번호가 10인경우 참여 인원에 따라 10 + 0 / 10 + 1 등등 이 붙어 반환되므로

초상화 생성 때와 마찬가지로 번호에 questIndex(순서)를 하나 더 더해줍니다.

퀘스트용 대화에는 obj id + quest id + questIndex를 번호로 저장합니다. 

//퀘스트용 대화(obj id + quest id + questIndex(순서번호))
        talkData.Add(1000 + 10, new string[]{"엥? 이런곳에 사람이?!?!:2","저기요! \n저 좀 도와주세요:0","왜 여기 갇혀 계세요?:2","저도 잘 모르겠어요. \n저희 아버지가 좀 이상해서요:0","제가 어떻게 해드리면 될까요?:2","몬스터들이 열쇠를 가지고 있어요. \n그것을 구해다 주시면 제가 좋은 기술을 알려드릴게요!:0","네! 잠시만 기다리세요:2"});
        talkData.Add(2000 + 10 + 1, new string[]{"안녕:1", "안녕하세요? \n혹시 열쇠를 구할 수 있는 곳을 아시나요?:2","열쇠? 열쇠라면 나도 하나 가지고있어:1","혹시 어디서 얻으셨나요?:2","몬스터가 가지고 있길래 \n 해치우고 나서 일단 가지고 있어:1","혹시 저에게 주실 수 있나요?:2","뭐.. 별로 필요하지는 않았으니까 좋아\n 가져가도록해:1","감사합니다:2"});

 

 

 

퀘스트 인덱스는 순서이므로 한번 실행시 questActionIndex를 1증가시킨 후 반환해야(위의 함수로) 합니다.

public void CheckQuest()
    {
        questActionIndex++;
    }

다음과 같은 함수를 사용하면 1증가시킬 수 있습니다.

한명의 npc와의 대화가 끝날때 마다 위의 함수를 호출하면 인덱스를 1증가시킬 수 있습니다.

 

 

 

그렇다면 이제 대화가 끝나는 부분에 CheckQuest()를 실행시켜 줍시다.

GameManager.cs

//void Talk(int id, bool isNPC) 내부에 있는 부분

	if(talkData == null) //반환된 것이 null이면 더이상 남은 대사가 없으므로 action상태변수를 false로 설정 
        {
            isAction = false;
            talkIndex=0; //talk인덱스는 다음에 또 사용되므로 초기화해야함 
            questManager.CheckQuest(); // 퀘스트 인덱스 1증가
            return; //void에서의 return 함수 강제종료 (밑의 코드는 실행되지 않음)
        }

talkData가 null인 경우 대화가 종료된 것 이므로, 이 안에 CheckQuest()를 추가해줍니다.

 

 

 

다음퀘스트로 이어지기

탈리아에게 말걸기 (열쇠를 가져다 달라는 퀘스트 ) -> 백설공주에게 말걸기 (열쇠 정보얻기) -> // 여기까지 진행했으므로 

열쇠 찾기-> 탈리아에게 가져가서 퀘스트 완료 //열쇠를 찾아 가져가는 새로운 퀘스트로 이어져야 합니다.

 

새로운 퀘스트를 추가해줍니다.

퀘스트는 10단위 이므로 20번으로 설정해줍니다.

QuestManager.cs

    void GenerateData()
    {
        questList.Add(10, new QuestData("탈리아를 구해라", new int[]{1000,2000}));
        questList.Add(20, new QuestData("열쇠 찾기",new int[]{2000}));
    }

 

 

이제 다음 퀘스트로 넘어가지게 하는 함수를 작성해줍니다.

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

    }

다음 퀘스트로 넘어가려면 퀘스트 id에 10을 더해주고

하나의 퀘스트 내부의 순서를 표현하는 index를 0으로 초기화 시켜주어야 합니다.

 

 

다음 퀘스트로 넘어가는 것은 " 퀘스트가 종료 되었을 때 " 

퀘스트 인덱스(진행중인 퀘스트의 순서)가 해당 퀘스트에 참여하는 npc수와 같아지면 해당 퀘스트가 종료됩니다

 

다음 퀘스트로 넘어가지 않고 해당 (동일)퀘스트 내에서 다음 순서로 넘어가는 것은 " 퀘스트가 종료 되기 전 "

입력받은 npc(obj) id가 QuestManager에 존재하는 퀘스트의 int부분

obj id + quest id + quest index 부분과 같을 때 -> quest index를 1증가해서 동일 퀘스트 내의 다음 순서로 넘어가게 하기 

    public string CheckQuest(int id) //npc id
    {
    	//해당 퀘스트가 종료 전일 때
        if(id == questList[questId].NpdId[questActionIndex]) 
        	// questList에서 questId에 해당하는 퀘스트에서 , 
            //이 퀘스트에 참여하는 npc의 순서번호(questActionIndex)와 입력받은 npc id가 같은 경우 -> 퀘스트 종료 전
            questActionIndex++;

		//퀘스트가 종료되었을 때
        if(questActionIndex == questList[questId].NpdId.Length) 
        //퀘스트 리스트에 있는 NpcId(퀘스트에 참여하는 npc) 수와 같을 때 -> 퀘스트 종료
            NextQuest();

        return questList[questId].questName;
    }

또한 오류방지를 위해 일단 지금 실행중인 퀘스트의 이름을 반환하도록  

return string으로 설정하고 

 

 

CheckQuest() 를 실행하던 Talk함수 내의 부분을 다음과 같이 수정해줍니다.

출력되도록 수정하는 동시에 매개변수(id)를 넘겨줍니다.

    void Talk(int id, bool isNPC){ // id는 오브젝트id 

        int questTalkIndex = questManager.GetQuestTalkIndex(id); // 조사한 obj의 id를 넘겨 퀘스트 id를 반환받음 
        string talkData = talkManager.GetTalk(id+questTalkIndex, talkIndex); //id에 퀘스트 id를 더하면 -> 해당 id를 가진 오브젝트가 가진 퀘스트의 대화를 반환하게 만들기

        if(talkData == null) //반환된 것이 null이면 더이상 남은 대사가 없으므로 action상태변수를 false로 설정 
        {
            isAction = false;
            talkIndex=0; //talk인덱스는 다음에 또 사용되므로 초기화해야함 
            Debug.Log(questManager.CheckQuest(id));
            return; //void에서의 return 함수 강제종료 (밑의 코드는 실행되지 않음)
        }

 

 

 

퀘스트 오브젝트

이제 맵에 열쇠를 한번 배치해 봅시다.

2D sprite를 이용해 열쇠를 배치하고 콜라이더를 설정합니다.

또한 objdata를 넣어주고 id를 300번으로 설정합니다.

 

그리고 퀘스트 데이터에서 해당 퀘스트와 연관된 int배열에 300번을 추가해줍니다.

QuestManager.cs

    void GenerateData()
    {
        questList.Add(10, new QuestData("탈리아를 구해라", new int[]{1000,2000}));
        questList.Add(20, new QuestData("열쇠 찾기",new int[]{2000,300}));
    }

배열순서는 퀘스트 순서대로 배치해야합니다(index 때문)

 

이제 

QuestManager에 퀘스트에 필요한 object를 위한 배열을 선언하고

public GameObject[] questObject;

 

배열에 드래그해서 key를 넣어줍니다.

 

QuestManager.cs

    void ControlObject()
    {
        switch(questId){
            case 10:
            if(questActionIndex == 2) //10번 퀘스트를 끝마치면 열쇠 등장 
                questObject[0].SetActive(true);    
            break;

            case 20:
            if(questActionIndex == 1)//20번 퀘스트에서 1번째 순서가 끝나고 -> 열쇠를 먹으면 열쇠 사라짐 
                questObject[0].SetActive(false); 
            break;
        }
    }

이제 오브젝트가 나타나거나 없어지는 것을 함수로 관리합니다.

10번 퀘스트 진행시 백설공주에게 말을 걸고 나면(quest id = 10, questActionIndex = 2) 열쇠가 등장하게 합니다.

20번 퀘스트 진행시 열쇠에 조사액션을 실행하면(quest id = 20, questActionIndex = 1)  열쇠가 사라지게 한다

 

이러한 오브젝트 관리 함수를 

    public string CheckQuest(int id) //npc id
    {

        //Next Talk Target
        if(id == questList[questId].NpdId[questActionIndex]) //매개변수로 받은 id가 
            questActionIndex++;


        //Control Quest Object
        ControlObject();

        //Talk complete & Next Quest
        if(questActionIndex == questList[questId].NpdId.Length) //퀘스트 리스트에 있는 NpcId(퀘스트에 참여하는 )
            NextQuest();

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

퀘스트 인덱스가 변화하면 실행시켜, 퀘스트 인덱스가 변화할 때 마다

해당 인덱스에 적절한 퀘스트 오브젝트를 조절하도록 함수를 배치한다.

 

이제 퀘스트를 진행할 수 있지만

적절한 순서대로 말을 걸지 않을 경우 오류가 발생한다.

 

이를 해결해보도록 하자

 

 

예외처리

 

GetTalk 부분에 예외처리를 넣는다 

    public string GetTalk(int id, int talkIndex) //Object의 id , string배열의 index
    {

        //예외처리
        if(talkData.ContainsKey(id)) 
        {
            return talkData[id - id%10][talkIndex];
            //id가 존재하지 않을 경우 
            /*
            무언가를 10으로 나눈 나머지는 "한자리수"이다.
            id에서 한자리수 부분은 -> 퀘스트 순서index이다. 
            퀘스트 순서가 부여되지 않은 npc와의 대화에서 오류가 나는 것 이므로
            해당 id가 존재하지 않으면 id에서 한자리수를 빼면 결국 id의 일의자리수가 0이 되므로
            해당 퀘스트에서 해당 npc에 대한 기본 대사가 출력되므로 오류가 생기지 않게 함 

            ex) 1021 -> 1020 
            */
        }

이를 적용하여 정리하면,

 

다음과 같이 수정할 수 있습니다.

    public string GetTalk(int id, int talkIndex) //Object의 id , string배열의 index
    {

        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]; //해당 아이디의 해당
    }

 

이렇게 되면, 

20번 퀘스트 진행시 index가 1인 경우

2000 + 20 + 1 번이 없어서 2000번 npc에게 말을 걸었을 때 발생하던 오류가 발생하지 않고 

자동으로 2000 + 20 으로 실행됩니다. 

 

하지만 여기서도 퀘스트에 속하지 않은 object들을 조사했을 때 나는 오류는 해결되지 않습니다.

여기서 100,200번 object는 퀘스트 대사에 포함되지 않아서 +20조차 존재하지 않으므로 오류가 해결되지 않습니다.

 

이를 해결하기 위해서는 하나를 더 추가해 주어야 합니다.

    public string GetTalk(int id, int talkIndex) //Object의 id , string배열의 index
    {

        //예외처리
        /*
        1. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 없음
            -> 1) 해당 퀘스트의 index(0) 처음에 대사가 있을 때  
                    -> 출력 
            -> 2) 해당 퀘스트의 index(0) 처음에 대사가 없을 때
                    -> 퀘스트 0번 (그냥 기본대사) 출력


        2. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 있음
            -> 출력


        */

        //1. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 없음
        //해당 퀘스트 진행 순서 대사가 없을 때 
        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];
            }
        }

        //2. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 있음
        if(talkIndex==talkData[id].Length)
            return null;
        else
            return talkData[id][talkIndex]; //해당 아이디의 해당
    }

위의 함수를 보면 같은 로직의 함수를 id부분에 따라만 다른 모습을 볼 수 있습니다.

이를 재귀호출로 변경하면 코드를 효율적으로 바꿀 수 있습니다.

    public string GetTalk(int id, int talkIndex)
    {
        //1. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 없음
        if(!talkData.ContainsKey(id)){

            //해당 퀘스트 자체에 대사가 없을 때 -> 기본 대사를 불러옴 (십, 일의 자리 부분 제거 )
            if(!talkData.ContainsKey(id - id%10)) 
                return GetTalk(id - id%100, talkIndex);//GET FIRST TALK
            
            //
            else
                return GetTalk(id - id%10,talkIndex);//GET FIRST QUEST TALK
        }

        //2. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 있음
        if(talkIndex==talkData[id].Length)
            return null;
        else
            return talkData[id][talkIndex]; //해당 아이디의 해당
    }

 

 

 

마지막으로 대화를 걸어야만 퀘스트 이름을 알 수 있게 하지 말고,

게임을 시작했을 때 시작부터 알게 만들어봅시다

 

GameManager.cs의 start()부분에 

퀘스트 이름을 반환하는 함수를 실행합시다 

 

퀘스트 이름을 반환하는 함수는 CheckQuest(int id)였는데, 시작시에는 id를 넣을 수 없고,

우리가 원하는 기능은 단순 이름 반환이므로 같은 이름의 매개변수가 다른 함수를 오버로딩으로 만듭니다.

QuestManager.cs

    public string CheckQuest() //오버로딩 
    {
        //return Quest Name
        return questList[questId].questName;
    }


    public string CheckQuest(int id) //npc id
    {

        //Next Talk Target
        if(id == questList[questId].NpdId[questActionIndex]) //매개변수로 받은 id가 
            questActionIndex++;


        //Control Quest Object
        ControlObject();

        //Talk complete & Next Quest
        if(questActionIndex == questList[questId].NpdId.Length) //퀘스트 리스트에 있는 NpcId(퀘스트에 참여하는 )
            NextQuest();

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

 

이렇게 만든 후

GameManager.cs의 start()부분에 퀘스트 이름을 반환하는 함수를 실행하면 

게임 시작시 부터 맨 첫번째 퀘스트를 시작할 수 있습니다.

    void Start()
    {
        questManager.CheckQuest(); //게임을 시작하자마자 퀘스트 이름을 가져오기 
    }

 

 

 

전체코드

 

QuestData.cs

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

public class QuestData : MonoBehaviour
{
    public string questName; //퀘스트 명
    public int[] NpdId; // 퀘스트에 관련된 NPC id 모음


    //생성자
    //public QuestData(){}

    public QuestData(string name, int[] npcid)
    {
        questName = name;
        NpdId = npcid;
    }

}

 

QuestManager.cs

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

public class QuestManager : MonoBehaviour
{

    public int questId;
    public int questActionIndex;
    public GameObject[] questObject;
    Dictionary<int, QuestData> questList;


    // Start is called before the first frame update
    void Awake()
    {
        questList = new Dictionary<int, QuestData>();
        GenerateData();
    }

    // Update is called once per frame
    void GenerateData()
    {
        questList.Add(10, new QuestData("탈리아를 구해라", new int[]{1000,2000}));
        questList.Add(20, new QuestData("열쇠 찾기",new int[]{300,2000}));
        questList.Add(30, new QuestData("퀘스트 올 클리어!",new int[]{0}));
    }

    public int GetQuestTalkIndex(int id) // Npc Id를 받아 퀘스트 번호를 반환하는 함수 
    {
        return questId + questActionIndex;
    }


    public string CheckQuest() //오버로딩 
    {
        //return Quest Name
        return questList[questId].questName;
    }


    public string CheckQuest(int id) //npc id
    {

        //Next Talk Target
        if(id == questList[questId].NpdId[questActionIndex]) //매개변수로 받은 id가 
            questActionIndex++;


        //Control Quest Object
        ControlObject();

        //Talk complete & Next Quest
        if(questActionIndex == questList[questId].NpdId.Length) //퀘스트 리스트에 있는 NpcId(퀘스트에 참여하는 )
            NextQuest();

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

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

    }

    void ControlObject()
    {
        switch(questId){
            case 10:
            if(questActionIndex == 2) //10번 퀘스트를 끝마치면 열쇠 등장 
                questObject[0].SetActive(true);    
            break;

            case 20:
            if(questActionIndex == 1)//20번 퀘스트에서 1번째 순서가 끝나고 -> 열쇠를 먹으면 열쇠 사라짐 
                questObject[0].SetActive(false); 
            break;
        }
    }
}

 

 

GameManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{

    //스크립트 
    public PlayerMove player;
    public TalkManager talkManager;
    public QuestManager questManager;

    //점수와 스테이지 이동관리하는 오브젝트(클래스)

    public int totalPoint;
    public int stagePoint;
    public int stageIndex;
    public int health;
    public GameObject[] Stages; //스테이지를 오브젝트로 만들었기 떄문에 오브젝트 배열로 관리가능 


    //UI 변수
    public Image[] UIhealth; //이미지는 3개이므로 배열 
    public Text UIPoint;
    public Text UIStage;
    public GameObject UIRestartBtn;



    //대화창 
    public GameObject talkPanel;
    public Text UINameText;
    public Text UITalkText;
    public Text UIInfoText;
    public Image portraitImg;
    public GameObject scanObject;
    public bool isAction; //대화창 활성화 상태 
    public int talkIndex;
    public string name;


    // Start is called before the first frame update
    void Start()
    {
        questManager.CheckQuest(); //게임을 시작하자마자 퀘스트 이름을 가져오기 
    }

    // Update is called once per frame
    void Update()
    {
        UIPoint.text = (totalPoint + stagePoint).ToString();
    }


    public void NextStage(){

        if (stageIndex < Stages.Length-1){   // 마지막 스테이지 아닌 경우 -> 다음스테이지로 

            Stages[stageIndex].SetActive(false);
            stageIndex++; //스테이지 증가 
            Stages[stageIndex].SetActive(true); //다음 스테이지 활성화

            PlayerReposition(); //시작위치에서 플레이어를 태어나게?하는 함수 


            UIStage.text = "STAGE "+(stageIndex + 1);

        }
        else{ //마지막 스테이지인 경우 ->게임끝 

            //플레이어 컨트롤 막기 
            Time.timeScale = 0; //플레이어가 이동되지 않게 함 

            //결과출력 
            Debug.Log("게임 클리어");

            //UI 다시시작버튼 
            Text btnText = UIRestartBtn.GetComponentInChildren<Text>();
            btnText.text = "GameClear!";
            UIRestartBtn.SetActive(true);


        }


        //Calculate point
        totalPoint += stagePoint; // 얻은 지역포인트 전체점수에 포함시키기 
        stagePoint = 0; //지역 포인트 초기화
    }

    private void OnTriggerEnter2D(Collider2D other) {
        
        if(other.gameObject.tag == "Player"){

            //체력감소
            HealthDown(); //경계에 부딪힌게 플레이어면 health 감소


            if(health>1){
            //떨어진 위치에서 플레이어 재생성 -> 플레이어가 죽지 않았을 떄만 해야함 
                PlayerReposition();
            }
        } 
    }

    public void HealthDown(){

        if(health > 1) {//생명이 0보다 크면 단순 생명 감소
            health--;
            UIhealth[health].color = new Color(1,0,0,0.3f);
        }
        else{// 생명이 0이하면 죽음

            //플레이어가 죽는 모션(이패트)
            player.OnDie();

            //UI에 결과 출력
            Debug.Log("죽었습니다.");

            //다시 시작 버튼 
            UIRestartBtn.SetActive(true);

            //죽었을 경우 모든 UI가 사라지도록 해야함 -> All Health UI Off
            UIhealth[0].color = new Color(1,0,0,0.3f);

        }
    }

    void PlayerReposition(){
            
            player.VelocityZero();
            player.transform.position = new Vector3(-12,-2,-1); //플레이어의 시작위치로 되돌아오기
    }


    public void Restart(){ //재시작이므로 처음부터 다시시작이라 scene 0번 
        
        Time.timeScale = 1; //플레이어가 다시 움직일 수 있도록 함 
        SceneManager.LoadScene(0);
    }
    
    public void Action(GameObject scanObj)
    {

            scanObject = scanObj;
            name = scanObject.name;
            //UITalkText.text = "이것은 "+scanObject.name+"이다.";
            ObjData objData = scanObject.GetComponent<ObjData>();
            Talk(objData.id,objData.isNPC);
        

        talkPanel.SetActive(isAction); //대화창 활성화 상태에 따라 대화창 활성화 변경
    }


    void Talk(int id, bool isNPC){ // id는 오브젝트id 

        int questTalkIndex = questManager.GetQuestTalkIndex(id); // 조사한 obj의 id를 넘겨 퀘스트 id를 반환받음 
        string talkData = talkManager.GetTalk(id+questTalkIndex, talkIndex); //id에 퀘스트 id를 더하면 -> 해당 id를 가진 오브젝트가 가진 퀘스트의 대화를 반환하게 만들기

        if(talkData == null) //반환된 것이 null이면 더이상 남은 대사가 없으므로 action상태변수를 false로 설정 
        {
            isAction = false;
            talkIndex=0; //talk인덱스는 다음에 또 사용되므로 초기화해야함 
            Debug.Log(questManager.CheckQuest(id));
            return; //void에서의 return 함수 강제종료 (밑의 코드는 실행되지 않음)
        }

        if(isNPC){
            UIInfoText.text = "";
            UITalkText.text = talkData.Split(':')[0]; //구분자로 문장을 나눠줌  0: 대사 1:portraitIndex
            portraitImg.sprite = talkManager.GetPortrait(id,int.Parse(talkData.Split(':')[1]));

            if(int.Parse(talkData.Split(':')[1])==2)
            {
                UINameText.text = "잭";
            }else{
                UINameText.text = name;
            }


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

        }else{
            UINameText.text="";
            UITalkText.text ="";
            UIInfoText.text = talkData;
            
            portraitImg.color = new Color(1,1,1,0);
        }

        //다음 문장을 가져오기 위해 talkData의 인덱스를 늘림
        isAction=true; //대사가 남아있으므로 계속 진행되어야함 
        talkIndex++;
    }


}

 

ObjData.cs

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

public class ObjData : MonoBehaviour
{
    public int id;
    public bool isNPC;
    
}

 

 

TalkManager.cs

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

public class TalkManager : MonoBehaviour
{
    Dictionary<int, string[]> talkData;
    Dictionary<int, Sprite> portraitData;
    public Sprite[] portraitArr;
    //0:잠자는 숲속의 공주 1:백설공주 2:잭

    // Start is called before the first frame update
    void Awake()
    {
        talkData = new Dictionary<int, string[]>();
        portraitData = new Dictionary<int, Sprite>();
        GenerateData();
    }

    void GenerateData()
    {
        //대사 생성 (obj id, 대화 )
        talkData.Add(1000, new string[]{"안녕하세요:0", "문 좀 열어주실래요?:0",});
        talkData.Add(2000, new string[]{"안녕:1", "이 마을에는 처음이니?:1",});
        talkData.Add(100,new string[]{"쇠로 만들어진 감옥이다.","열쇠없이는 열 수 없는 것 같다.",});
        talkData.Add(200,new string[]{"평범한 문이다. \n들어갈 수 있을 것 같다",});
        talkData.Add(300,new string[]{"평범한 열쇠다. \n어디에 쓸 수 있을까?",});


        //퀘스트용 대화(obj id + quest id + questIndex(순서번호))
        //퀘스트용 대화(obj id + quest id)

        //10번 퀘스트 
        talkData.Add(1000 + 10, new string[]{"엥? 이런곳에 사람이?!?!:2","저기요! \n저 좀 도와주세요:0","왜 여기 갇혀 계세요?:2","저도 잘 모르겠어요. \n저희 아버지가 좀 이상해서요:0","제가 어떻게 해드리면 될까요?:2","몬스터들이 열쇠를 가지고 있어요. \n그것을 구해다 주시면 제가 좋은 기술을 알려드릴게요!:0","네! 잠시만 기다리세요:2",});
        talkData.Add(1000 + 10 + 1,new string[]{"열쇠를 찾아주세요. \n부탁드려요:0",});
        talkData.Add(2000 + 10 + 1, new string[]{"안녕:1", "안녕하세요? \n혹시 열쇠를 구할 수 있는 곳을 아시나요?:2","열쇠? 열쇠라면 문 옆에 떨어져 있는걸 본 것 같아:1","감사합니다:2",});
        
        //20번 퀘스트
        talkData.Add(1000 + 20,new string[]{"열쇠를 찾아주세요. \n부탁드려요:0",});
        talkData.Add(2000 + 20,new string[]{"아직 못찾았니? \n내가 마지막으로 봤을때는 문 옆에 있었어:1",}); //퀘스트 완료 전에 대화를 걸었을 때
        talkData.Add(300 + 20,new string[]{"열쇠를 찾았다",}); 
        talkData.Add(1000 + 20 + 1,new string[]{"열쇠를 찾아줘서 고마워요!:0","별 말씀을요!:2",}); //열쇠를 찾은 후에 


        //30번 퀘스트 
        talkData.Add(1000 + 30, new string[]{"열쇠를 찾아줘서 고마워요!:0","별 말씀을요!:2",});

        //초상화 생성 (obj id + portrait number)
        portraitData.Add(1000 + 0, portraitArr[0]); //0번 인덱스에 저장된 초상화를 id = 1000과 mapping
        portraitData.Add(1000 + 1, portraitArr[1]);
        portraitData.Add(1000 + 2,portraitArr[2]); //2번 인덱스에 저장된 초상화를 id = 1002과 mapping

        portraitData.Add(2000 + 0, portraitArr[0]); 
        portraitData.Add(2000 + 1,portraitArr[1]);
        portraitData.Add(2000 + 2,portraitArr[2]);
    }

/*
    public string GetTalk(int id, int talkIndex) //Object의 id , string배열의 index
    {
*/
        //예외처리
        /*
        1. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 없음
            -> 1) 해당 퀘스트의 index(0) 처음에 대사가 있을 때  
                    -> 출력 
            -> 2) 해당 퀘스트의 index(0) 처음에 대사가 없을 때
                    -> 퀘스트 0번 (그냥 기본대사) 출력


        2. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 있음
            -> 출력


        */

/*
        //1. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 없음
        //해당 퀘스트 진행 순서 대사가 없을 때 
        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];
            }
        }

        //2. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 있음
        if(talkIndex==talkData[id].Length)
            return null;
        else
            return talkData[id][talkIndex]; //해당 아이디의 해당
    }

*/
    public string GetTalk(int id, int talkIndex)
    {
        //1. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 없음
        if(!talkData.ContainsKey(id)){

            //해당 퀘스트 자체에 대사가 없을 때 -> 기본 대사를 불러옴 (십, 일의 자리 부분 제거 )
            if(!talkData.ContainsKey(id - id%10)) 
                return GetTalk(id - id%100, talkIndex);//GET FIRST TALK
            
            //
            else
                return GetTalk(id - id%10,talkIndex);//GET FIRST QUEST TALK
        }

        //2. 해당 퀘스트 id에서 퀘스트index(순서)에 해당하는 대사가 있음
        if(talkIndex==talkData[id].Length)
            return null;
        else
            return talkData[id][talkIndex]; //해당 아이디의 해당
    }

    public Sprite GetPortrait(int id, int portraitIndex)
    {
        //id는 NPC넘버 , portraitIndex : 표정번호(?)
        return portraitData[id+portraitIndex];  
    }
}

 

 

 

진행은 되는데 뭔가 경고가 하나 뜨는데 이건 나중에 해결하기로 ㅎㅏ고

엄청 복잡해졌네요... 좀 정리하고 싶긴한데 급한거 먼저 해야...

 

<참고영상>

youtu.be/RwndWebxbmo

 

728x90

'GAME' 카테고리의 다른 글

[Unity2D] 대화창 구현  (0) 2020.08.29
[Unity2D]조사액션 / 조사창 구현  (0) 2020.08.28
[Unity2D] 기초2D게임 만들기  (0) 2020.08.07
[Unity2D] 적 몬스터 구현하기  (1) 2020.08.01
[Unity2D] 타일맵 Platform 만들기  (0) 2020.08.01