게임 개발 일지/유니티 엔진 공부

21. 유니티 탑다운 2D 게임 제작 - 퀘스트 시스템 구현

인텔리킴 2024. 4. 30. 21:39

현재까지 진행 상황

퀘스트 대화

오브젝트 데이타 스크립트를 따로 만든것처럼

퀘스트 데이터 스크립트를 따로 만듬

 

* 실전에서는 주로 xml SQL 등의 데이터 베이스를 통해 관리함

 

public class QuestData : MonoBehaviour
{
    public string questName;
    public int[] npcID;

    public QuestData(string name, int[] ID)
    {
        questName = name;
        npcID = ID;
    }
}

퀘스트 데이터에 필요한 것은

퀘스트 이름, 퀘스트 아이디 인덱스

 

이 스크립트는 구조체로 사용

다른 스크립트에서 구조체를 사용하기 용이하도록 생성자를 따로 만들어서 사용

 

public QuestManager questManager;

NPC ID를 받고 퀘스트 번호를 반환하는 함수 생성

 

int questTalkIndex = questManager.GetQuestTalkIndex(id);
string talkData = talkmanager.GetTalk(id + questTalkIndex, talkIndex);

퀘스트를 매니저를 변수로 생성 후 퀘스트 번호를 가져옴

퀘스트 번호 + NPC ID = 퀘스트 대화 데이터 ID

 

//퀘스트 대화
talkData.Add(10 + 1000, new string[] { "우리마을에는 슬픈 전설이 있어... #0", "일단 트윈테일 여자애한테 말 거셈. #1", " 하야쿠 하야쿠 #3" });
talkData.Add(10 + 2000, new string[] { "아틔시미나토아쿠아! #0", "아틔시 퍼펙토나 메이드상! #1", "오츠아쿠아~ #2" });

 

퀘스트 대화는 10(퀘스트 아이디) + 1000(NPC 아이디) 와 같은 형식으로 삽입시킴

 

퀘스트 대화 불러오기

하지만 이방법으로 삽입하면 퀘스트 순서에 상관없이 대화문이 나옴

 

퀘스트 대화순서 변수 생성

퀘스트 순서 끝날시 퀘스트 인덱스 변수 1 올림

 

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

대화 끝날때 CheckQuest()를 통해 인덱스 1 더해서 퀘스트 다음 과정으로 가게함

 

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

 CheckQuest() 에서는

NPC의 아이디를 인자로 받아서

만약 QuestData 구조체의 npcID 배열순서

예를들어 Index가 0이면

questList.Add(10, new QuestData("첫 마을 방문 후 NPC들과 대화", new int[] {1000, 2000 }));

여기서 1000을 불러와서

이 1000이 Npc ID와 같으면 인덱스 1 상승

 

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

다음 퀘스트를 받게 해주는 함수 작성

다음 퀘스트를 받으면 인덱스 초기화

 

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

public class QuestManager : MonoBehaviour
{
    public int questId;
    public int questActionIndex;
    Dictionary<int, QuestData> questList;
 
    void Awake()
    {
        questList = new Dictionary<int, QuestData>();
        GenerateData();
    }

    void GenerateData()
    {
        questList.Add(10, new QuestData("첫 마을 방문 후 NPC들과 대화", new int[] {1000, 2000 }));
        questList.Add(20, new QuestData("남자한테 돌아가기", new int[] { 2000, 1000 }));
        questList.Add(30, new QuestData("동전 찾기", new int[] { 1000, 2000 }));

    }

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

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

 

완성된 퀘스트 매니저 스크립트

 

퀘스트가 끝나고 다음 퀘스트 제공

퀘스트 오브젝트 구현

void ControlObject()
{
    switch (questId)
    {
        case 10:
           break;
        case 20:
            if (questActionIndex == 2)
                questObject[0].SetActive(true);
            break;
        case 30:
            if(questActionIndex==1)
                questObject[0].SetActive(false);
            break;
    }
}

컨트롤 오브젝트 함수를 통해

퀘스트 아이디가 각 케이스와 같을때마다

퀘스트 오브젝트를 OnOff

 

public void CheckQuest(int id)
{
    
    //퀘스트가 현재 진행과정과 같은지 확인
    if (id == questList[questId].npcID[questActionIndex]) {
        questActionIndex++;
        //컨트롤 퀘스트 오브젝트
        ControlObject();
    }
    //현재 퀘스트가 끝났을 경우
    if (questActionIndex == questList[questId].npcID.Length)
    {
        NextQuest();
    }
}

퀘스트 인덱스를 상승시킬때 퀘스트 오브젝트 컨트롤 수행

 

실행결과
실행결과2
실행결과3

 

하지만 퀘스트 수행 중 다른 NPC에게 말을 걸면 제대로 대화가 되지 않는 문제가 있음

 

오류

 

예외 처리

if (!talkData.ContainsKey(id))
{
    //해당 퀘스트 진행 순서중 대사가 없을때.
    //퀘스트의 맨처음 대사를 가져옴
    if (talkIndex == talkData[id - (id % 10)].Length)
        return null;
    else
        return talkData[id - (id % 10)][talkIndex];
}

* talkData.ContainsKey(talkIndex) : 딕셔너리에 키값이 있는지 없는지 검사해주는 코드

GetTalk 코드에 NpcID값이 없을경우 퀘스트 대화순서를 아예 제거하고 재탐색

id - (id % 10) : id 값에서 10 나머지 값을 뻄

예시 : id =1022 일경우 1022 - (1022 % 10) = 1020을 리턴 

 

만약 talkIndex 값이 그렇게 나온 1020에 해당하는 배열의 길이가 talkIndex값과 같을때

(퀘스트라인 막바지였을때) = null값을 호출

아닐 경우 다시 그 퀘스트의 talkIndex에 해당하는 순서의 대화를 불러옴

 

하지만 이경우 그냥 평범한 오브젝트나 NPC에게 말을 걸때 제약이 생김

 

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;
    }
    return talkData[id][talkIndex];
}

제약 조건을 좀 더 추가해서 스크립트 작성

 

예시) 만약 퀘스트 상태가 10이고 talkIndex가 2일때 100인 오브젝트를 만지면 id : 110이 GetTalk 함수에 들어옴

그러면 첫 조건 (!talkData.ContainsKey(id)) 에서 id가 없기 때문에 다음 조건으로 넘어감

다음 조건 (!talkData.ContainsKey(id - id % 10)) 에서 110 - 0 = 110 이기 때문에 다음 조건으로 넘어감

그리고 마지막 조건 (talkIndex == talkData[id - (id % 100)].Length)에서  2 != (110 - 10 = 100의 길이 1) 

이기 때문에 talkData[110 - (10)][talkIndex]; 으로 100 값의 talkData

talkData.Add(100, new string[] {"코레와... 책상이네요."});

 

이라는 데이터를 불러옴

 

퀘스트를 받은 도중에도 기본 대사가 나옴

 

로직 다듬기

다음으로 지저분한 로직을 다듬어야함

 

    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;
        }
        return talkData[id][talkIndex];
    }

 위 코드에서

 

if (talkIndex == talkData[id - (id].Length)
    return null;
else
    return talkData[id - (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);
        }
        else
        {
            //만약 퀘스트 진행 순서 대사가 없다면
            //퀘스트 맨처음의 대사를 가져온다.
            return GetTalk(id - (id % 100), talkIndex);
        }
    }
    if (talkIndex >= talkData[id].Length)
    {
        return null;
    }
    return talkData[id][talkIndex];
}

 

* 리턴 값이 있는 재귀함수는 return까지 같이 써주어야함

 

 

퀘스트 이름 띄우기

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

 

게임 매니저에 Start함수를 작성 후 

디버그 로그로 CheckQuest() 호출

하지만 이대로면 오류 발생

public void CheckQuest(int id)
{
    
    //퀘스트가 현재 진행과정과 같은지 확인
    if (id == questList[questId].npcID[questActionIndex]) {
        questActionIndex++;
        //컨트롤 퀘스트 오브젝트
        ControlObject();
    }
    //현재 퀘스트가 끝났을 경우
    if (questActionIndex == questList[questId].npcID.Length)
    {
        NextQuest();
    }
}
public string CheckQuest()
{
    return questList[questId].name;
}

 

퀘스트매니저 스크립트에 함수는 같지만 인자가 다른 함수를 작성

* 오버로딩 : 매개변수에 따라 함수 호출

어차피 가져와야하는 것은 퀘스트 이름뿐이기 떄문에 퀘스트 이름만 리턴

 

퀘스트 이름 리턴됨