[StarChaser] 로그라이크 게임 프로젝트

[유니티/C#] 퀘스트 시스템 만들기

진서박 2023. 6. 9. 23:51
반응형

이번에 만든것은 퀘스트 시스템이다. 이전에 구조를 잡아놓았던 DataClasses를 활용해 만들었다.

 

이전글 https://jinsso.tistory.com/10

 

[유니티/C#] 데이터를 관리하는 클래스 만들기

이전에 데이터를 csv에서 읽어와 저장하는 코드를 작성했었다. 하지만 기능 구현에 중점을 두고 코드를 짜다보니 보기에도, 사용하기에도 별로고 유지보수에도 별로 좋지 못했다. 이번에는 csv로

jinsso.tistory.com

 

 

이전의 스킬 시스템 구축 요청을 받았을 때의 경험이 있어서 이번에는 비교적 쉽게 만들 수 있었다.

플로우차트, 데이터 테이블

정리를 하면

1) 1단계 퀘스트 3개를 랜덤으로 가져온다

2) 가져올 때 이전에 가져온 항목과 QuestID가 동일하면 다시 뽑는다

3) 3개가 확정이 되었다면 UI에 적용한다

4) 퀘스트를 진행중, 클리어 조건과 현재 진행정도를 비교한다

5) 조건을 충족시켰다면 보상을 지급하고 다음 레벨의 퀘스트로 진행한다

6) 다음 레벨의 퀘스트가 없다면 클리어 상태로 간주한다

 

- 구현

 

퀘스트는 같은 종류의 퀘스트라 해도 클리어 조건과 보상이 다르기 때문에, 관리를 더 편하게 하기 위한 QuestGroup 클래스를 만들었다.

이 클래스는 퀘스트의 진행도와 보상을 관리하게 될것이다.

public class QuestGroup
{
    public Quest_Normal_Data quest; // 퀘스트 데이터
    public Transform uiQuest;   // UI 오브젝트
    public Text questTitle;     // 제목 UI
    public Text questExplain;   // 설명 UI

    public int currentQuestValue = 0; // 현재 퀘스트 진행도
    public int questClearValue = 0;  // 퀘스트 클리어 조건
    public int reward1 = 0;  // 골드
    public int reward2 = 0;  // 영구재화
    public int reward3 = 0;  // 공물
    public int reward4 = 0;  // 무기강화
    public int reward5 = 0;  // 능력획득

    // 퀘스트 변경 시 보상 초기화
    public void InitNormalQuestValue()
    {
        this.currentQuestValue = 0;
        this.questClearValue = quest.QuestClearValue;
        this.reward1 = quest.Reward1Amount;
        this.reward2 = quest.Reward2Amount;
        this.reward3 = quest.Reward3Amount;
    }
}

 

다음은 QuestGroup을 바탕으로 작성한 퀘스트 코드이다.

더보기

QuestBase와 그를 상속받은 QuestNormal의 코드

 

QuestBase

public enum QuestType
{
    None = -1,
    NormalQuest = 0,
    SuddenQuest,
    HiddenQuest
}

public abstract class QuestBase : MonoBehaviour, IQuest
{
    // 퀘스트를 관리할 클래스의 리스트
    protected List<QuestGroup> questDataGroup = new List<QuestGroup>();

    // 최초 초기화
    protected void Init()
    {
        foreach (QuestGroup group in questDataGroup)
        {
            group.InitNormalQuestValue();
            TextTitleUpdate(group);
            TextValueUpdate(group, group.currentQuestValue);
        }
    }

    // 퀘스트 진행도 업데이트
    public void Quest_ValueUpdate(Quest_NormalType type, int curClearVal)
    {
        // 퀘스트 그룹에서 입력받은 퀘스트 타입과 같은 타입을 가진 퀘스트를 가져온다
        QuestGroup _quest = questDataGroup.Find(group => group.quest.QuestType == (int)type);

        // 퀘스트 진행도를 curClearVal만큼 업데이트
        TextValueUpdate(_quest, curClearVal);

        // 클리어 조건을 충족했다면 클리어 함수 실행
        if(_quest.currentQuestValue >= _quest.questClearValue)
        {
            Quest_Clear(_quest);
        }
    }

    // 퀘스트 클리어
    protected void Quest_Clear(QuestGroup group)
    {
        // 다음 퀘스트가 없다면 클리어 처리, 보상 지급
        if (group.quest.NextQuestID == 9999)
        {
            group.questExplain.text = "클리어";
            Quest_Reward(group);
        }
        // 다음 퀘스트가 있다면 보상 지급 후, 다음 퀘스트로 진행
        else
        {
            Quest_Reward(group);
            Quest_NextQuest(group);
        }
    }

    // 다음 퀘스트 진행
    protected void Quest_NextQuest(QuestGroup group)
    {
        // 퀘스트 데이터에서 입력받은(클리어한) 퀘스트의 다음 QuestID의 데이터를 찾아서 할당
        Quest_Normal_Data nextQuest = GameManagers.DataClass.Quest_NormalData.Find(quest => quest.QuestID == group.quest.NextQuestID);

        // 퀘스트그룹의 데이터를 다음 퀘스트로 변경
        group.quest = nextQuest;

        // 현재 진행중인 퀘스트 업데이트
        GameManagers.QuestManager.QuestActivate(group.quest);

        // 퀘스트 보상 업데이트
        group.InitNormalQuestValue();

        // 진행도, 제목 초기화
        TextValueUpdate(group, 0);
        TextTitleUpdate(group);
    }

    // 퀘스트 보상 지급
    protected void Quest_Reward(QuestGroup group)
    {
        // 골드 지급
        GameManagers.PlayerInfo.QuestGoldUpdate(group.reward1);
        
        // 특수재화 지급
        GameManagers.PlayerInfo.QuestNectarUpdate(group.reward2);
        
        // 공물 지급
        GameManagers.PlayerInfo.QuestTributeUpdate(group.reward3);
    }


    // 퀘스트 진행도 업데이트
    protected void TextValueUpdate(QuestGroup group, int value)
    {
        // 진행도 업데이트
        group.currentQuestValue += value;
        
        // 진행도 UI 업데이트
        group.questExplain.text = group.quest.QuestExplainText.ToString() + "(" + group.currentQuestValue + "/" + group.questClearValue + ")";
    }

    // 퀘스트 제목 업데이트
    protected void TextTitleUpdate(QuestGroup group)
    {
        group.questTitle.text = group.quest.QuestTitle.ToString();
    }
}

 

QuestNormal

public class QuestNormal : QuestBase
{
    protected void Start()
    {
        // 랜덤 퀘스트 뽑아와서 퀘스트 그룹에 저장
        DataGroupInit();

        // 퀘스트 최초 초기화
        Init();
    }

       // 랜덤 퀘스트 뽑아와서 퀘스트 그룹에 저장
    private void DataGroupInit()
    {
        // UIQuestMenu 오브젝트의 자식 오브젝트 중에서 "UIQuest"를 포함하는 오브젝트들을 가져오기
        Transform[] uiQuestObjects = transform.GetComponentsInChildren<Transform>()
            .Where(child => child.name.Contains("UIQuest"))
            .ToArray();

        List<Quest_Normal_Data> RandomQuests = GetRandomQuest(GameManagers.DataClass.Quest_NormalData, uiQuestObjects.Length);

        // 랜덤으로 가져온 퀘스트를 퀘스트 그룹에 저장하고 활성화된 퀘스트 목록에도 저장
        for (int i = 0; i < RandomQuests.Count; i++)
        {
            QuestGroup group = new QuestGroup();
            group.quest = RandomQuests[i];
            GameManagers.QuestManager.ActivatedNormalQuest.Add((Quest_NormalType)RandomQuests[i].QuestType, RandomQuests[i]);
            questDataGroup.Add(group);
        }

        // 각 퀘스트 그룹의 정보로 UI초기화
        for(int i = 0; i < uiQuestObjects.Length; i++)
        {
            Text[] _text = uiQuestObjects[i].transform.GetComponentsInChildren<Text>();
            questDataGroup[i].uiQuest = uiQuestObjects[i];
            questDataGroup[i].questTitle = _text[0];
            questDataGroup[i].questExplain = _text[1];
            questDataGroup[i].currentQuestValue = 0;
            questDataGroup[i].questClearValue = questDataGroup[i].quest.QuestClearValue;
        }
    }

    public List<Quest_Normal_Data> GetRandomQuest(List<Quest_Normal_Data> questList, int count)
    {
        List<Quest_Normal_Data> chosenQuests = new List<Quest_Normal_Data>();
        Dictionary<int, List<Quest_Normal_Data>> priorQuestIDDic = new Dictionary<int, List<Quest_Normal_Data>>();

        foreach (Quest_Normal_Data quest in questList)
        {
            if (!priorQuestIDDic.ContainsKey(quest.PriorQuestID))
            {
                priorQuestIDDic[quest.PriorQuestID] = new List<Quest_Normal_Data>();
            }

            priorQuestIDDic[quest.PriorQuestID].Add(quest);
        }

        while (chosenQuests.Count < count)
        {
            int priorQuestID = 9999;
            if (priorQuestIDDic.ContainsKey(priorQuestID))
            {
                List<Quest_Normal_Data> questsWithPriorQuestID = priorQuestIDDic[priorQuestID];

                Quest_Normal_Data chosenQuest = questsWithPriorQuestID[Random.Range(0, questsWithPriorQuestID.Count)];

                if (chosenQuests.Any(skill => skill.QuestID == chosenQuest.QuestID))
                {
                    continue;
                }

                chosenQuests.Add(chosenQuest);
            }
        }

        return chosenQuests;
    }
}

GetRandomQuest() 는 이전의 GetRandomSkill()과 가져오는 조건을 제외하고는 거의 같으므로 따로 주석처리는 하지 않았다

 

 

 

결과

 

 

 

원래는 스킬에 대한 글을 쓰려 했으나, 스킬은 아직 추가할게 남았고 추가를 완료하면 코드를 리팩토링을 거쳐야 해서 그 때 적으려고 한다.

반응형