1. 설계 
2. 트리구조와 무슨 연관? 
3. 퀘스트 수락 -> 퀘스트 완료 -> 연계 퀘스트 추가 플로우
4. 구현 코드
5. 마무리

 

📝 1. 퀘스트 시스템

🔖 설계

 

QuestState 

          ■ 퀘스트의 상태를 나타내기 위해 만든 enum  

          ■ 받기전, 받은 후, 보상 수령 가능한 상태, 수령받은 후 끝난 상태

QuestClass 클래스

           abstract 클래스

           '퀘스트 완료 조건 메서드'를 저장하는 Func<bool> completeQuest 를 필드로 가지고 있음 

           TodoMission() 메서드

                       하위클래스에서 '퀘스트 완료 조건' 구현 

                        조건이 만족하면 => true 반환

                        조건이 불만족하면 => false 반환

 MonsterKillQuest 클래스 / EquiptQuest 클래스

           TodoMission () 메서드를 오버라이딩 

QuestManager 클래스

           List<Quest> performedQuest 

                        수행가능한 퀘스트 클래스들을 담을 컨테이너 

 

📝 2. 트리랑 무슨 연관?

: 연계퀘스트를 어떻게 구성할지 다이어그램을 작성하다가 이 구조가 트리와 굉장히 닮았다고 생각했다.

: 퀘스트에서 트리구조를 사용하면 몇 가지 장점이 있다.

1. 부모퀘스트는 자식에 해당하는 퀘스트들을 관리할 수 있다.

2. 자식에 해당하는 퀘스트는 부모퀘스트 정보를 알 수 있다.

3. 자식에 해당하는 퀘스트는 부모퀘스트를 트리에서 탐색(search) 할 수 있다.

 

🔖 그냥 리스트 사용하면 안 되나?

그럼 해당 퀘스트가 어떤 퀘스트로부터 파생되었는지 알기 힘들다.

 

예를 들어서, 수행하지 못하는 퀘스트도 화면에 표시한다고 생각하자

------------------

[퀘스트]

1. Armor 장비 한 개 착용                 [완료가능]

2. Weapon 장비 한개 착용             [수락가능]

3. 인벤토리에 15개 이상 장비 착용  [수행불가]

------------------

이미지상 '인벤토리에 15개 이상 장비 착용'퀘스트는 '무기 장비 1개 착용'퀘스트 후에 수행가능한 연계퀘스트이다.

즉 , '무기 장비 1개 착용'을 완료해야 퀘스트 수락이 가능하다.

이때, ' 인벤토리에 15개 이상 장비 착용' 퀘스트가 어느 퀘스트로부터 파생되었는지 알 수 있는 방법이 없다.

그래서 트리 구조로 [부모퀘스트] [자식퀘스트]를 연결시키는 것이다.

 

📝 3. 수락 완료  연계퀘스트 추가 플로우

: 주의 깊게 봐야 할 부분은 빨간색 박스 부분이다.

: 퀘스트를 완료했다면 

→ 완료한 퀘스트의 연관퀘스트를 '수행가능한 퀘스트 리스트'에 담아야 한다.

📝 4. 구현 코드

tree를 사용한 연계퀘스트에 중점을 맞췄기 때문에 다른 코드는 생략하였다.

- Quest 클래스

public abstract class Quest
{
    // 필드 
    protected string questName;   // 퀘스트이름
    protected string questStory;  // 퀘스트 스토리 (ex) 미니언들이 너무 많아졋다고 생각~
    protected int rewardGold;               // 리워드 
    protected string questPerform;          // 수행내역 
    protected QuestState questState;        // 수행 스탯 

    // 컨테이너
    protected Dictionary<Item, int> rewardItemByCount;        // 보상 아이템별 count
    protected Func<bool> completeQuest;                       // 성공여부 Func 

    // 트리구조
    protected Quest parentQuest;
    protected List<Quest> childQuest;

    // 프로퍼티
    public Quest ParentQuest { get => parentQuest; set { parentQuest = value; } }
    public List<Quest> ChildQuest => childQuest;

    public Quest(string name, string tooltip, string questPerfom , int rewardGold)
    {
        this.questName = name;
        this.questStory = tooltip;
        this.questPerform = questPerfom;
        this.questState = QuestState.beforeReceive;     // 생성할 때, 받기전으로 설정 
        this.rewardGold = rewardGold;

        if (rewardItemByCount == null)
            rewardItemByCount = new Dictionary<Item, int>();
        if(childQuest == null)
            childQuest = new List<Quest>(); 
    }

    public void AddToItem(Item item, int count)
    {
        // 코드생략
    }

    public void CheckState() 
    {
        // 받기전이면 return 
        if (questState == QuestState.beforeReceive)
            return;

        // 조건을 만족하면
        if (completeQuest.Invoke()) 
        {
            // 완료 state로 변환 
            this.questState = QuestState.complete;
        }
    }

    // child 자식 대입
    public void AddChild(Quest child) 
    {
        child.parentQuest = this;
        childQuest.Add(child);
    }
        // 하위에서 작성해야할 퀘스트 성공 조건
    public abstract bool TodoMission();

    // 퀘스트 진행내역
    public abstract string QuestProgress();
}

Quest 클래스

          ■ 자세한 설명은 상단의 클래스다이어그램 참고

           AddChild() 메서드

                        Quest클래스를 매개변수로 가진다

                        매개변수로 들어온 자식 퀘스트의 부모퀘스트를 this, 즉 내 클래스로 지정한다

                        현재 퀘스트의 자식클래스의 리스트에 매개변수로 들어온 자식 클래스를 add 한다

            CheckState() 메서드

                       Func<bool> completeQuest 를 Invoke()    

                                 ■ true를 return → complete로 상태 변환  

                                  flase를 return 상태 변환 x 

 

- MonsterKillQuest 클래스

public class MonsterKillQuest : Quest
{
    // 처치해야할 몬스터 
    // Monster 타입이면 더 좋을듯
    private List<string> monsterNameList;
    private List<int> killCountList;
    
    // 프로퍼티
    public List<string> MonsterNameList => monsterNameList;
    public List<int> KillCountList => killCountList;  

    public MonsterKillQuest(string name, string tooltip,string perForm , int rewardGold) : 
        base(name, tooltip , perForm , rewardGold)
    {
        // Func에 연결
        completeQuest += TodoMission;
    }

    public void AddtoKillMonsterList(string name , int cnt ) 
    {
        // 코드생략
    }

    public override bool TodoMission()
    {
        bool flag = true;

        // 지금까지 처치한 몬스터 수 
        for (int i = 0; i < monsterNameList.Count; i++) 
        {
            string nowMonster = monsterNameList[i];
            int countToKill = killCountList[i];

            // 잡은횟수
            int killCnt = DungeonManager.Instance.monsterCatches[nowMonster];

            // 처치한 몬스터가 kill count보다 낮으면 -> 실패
            if (killCnt < countToKill)
            { 
                flag = false;
                break;
            }
        }

        // for문안의 조건문에 걸리지않으면 -> killcount대로 다 잡은것 
        return flag;
    }

    public override string QuestProgress()
    {
        // 코드생략
    }

}

MonsterKillQuest 클래스

           자세한 설명은 상단의 클래스다이어그램 참고

            TodoMission() 메서드 오버라이딩 

                       퀘스트 완료 조건 구현

                      □ '현재 잡은 몬스터 수'가 '잡아야 하는 몬스터 수'보다 작으면 false를 return, 완료하면 true를 return

            생성자 

                      Func<bool> completeQuest 에 구현한 TodoMission() 메서드를 추가

 

- QuestManger 클래스

- 필드

private Quest currQuest;                // 현재 퀘스트 저장 

private List<Quest> performableQuests;  // 수행가능 퀘스트

currQuest

           현재 퀘스트 저장 

List<Quest> performableQuests

           수행가능한 퀘스트 컨테이너

           화면에 출력 할 때 해당리스트에 접근해서 퀘스트 이름을 출력

 

- Enter() 메서드

public void Enter()
{
    Console.Clear();

    // 수행가능한 퀘스트 이름만 빼서 배열로 저장 (LINQ)
    // 코드 생략
   
    // 목록(리스트) 출력
    // 코드 생략

    // player input 
    // 코드 생략

    // 1. 현재 퀘스트 
    currQuest = performableQuests[input - 1];
    // 2. 완료여부에 따라 state변화 
    currQuest.CheckState();

    // 퀘스트 print
    // 코드 생략

    // 성공여부에 따라 print 다름 
    PrintRewardBystate();
}

1. 플레이어에 입력에 맞게 Quest 타입의 currQuest를 저장한다.

2. currQuest의 상태변환 메서드를 실행한다 

           자세한 설명은 Quest 클래스의 CheskState() 메서드 참고

 

- PrintRewardBystate() 메서드

private void PrintRewardBystate()
{
    switch (currQuest.QuestState)
    {
        case QuestState.beforeReceive:
            // 코드생략
            break;
        case QuestState.afterReceive:
            // 코드생략
            break;
        case QuestState.complete:
            RemoveListAndAddToChild();
            break;
    }
}

1. 현재 퀘스트의 상태에 따라 다른 메서드를 실행한다.

2. 퀘스트가 complete 상태이면 (완료했으면) RemoveListAndAddToChild() 메서드를 실행한다. 

 

- RemoveAndAddToChild() 메서드

private void RemoveListAndAddToChild()
{
    // 현재 퀘스트의 state를 done으로
    currQuest.ChangeState(QuestState.done);

    // currQuest와 같은 quest 반환
    var temp = performableQuests.Find(quest => quest.Equals(currQuest));

    // 수행가능 리스트에서 삭제
    if (temp != null)
    {
        performableQuests.Remove(temp);
    }

    // 현재 퀘스트의 child리스트에 접근해서 가능한 퀘스트리스트에 넣어야 함 
    for (int i = 0; i < currQuest.ChildQuest.Count; i++)
    {
        performableQuests.Add(currQuest.ChildQuest[i]);
    }
}

1. 현재 퀘스트의 상태를 QuestState.done으로 변경한다.

2. 수행가능한 퀘스트를 담아놓는 리스트에서 현재 퀘스트를 remove 한다. 

3. 현재 퀘스트의 List<Quest>에 접근해서 수행가능한 퀘스트 리스트(perforambleQuest)에 추가한다.


📝 동작 영상

 

 


📝 마무리

: 현재는 수락불가능한 퀘스트가 출력되고 있지는 않다. 

유니티 개인 프로젝트를 할 때 수락 불가능한 퀘스트도 출력해서 퀘스트의 부모 퀘스트 추적 기능까지 추가해 보겠다.

📝 부모퀘스트 추적

(추가예정입니다)

 


TextRpg 팀 프로젝트 깃허브 주소 : 

https://github.com/kimYouChae/Window11_TextRPG

 

GitHub - kimYouChae/Window11_TextRPG

Contribute to kimYouChae/Window11_TextRPG development by creating an account on GitHub.

github.com

 

 

 

+ Recent posts