[Unity] 순환&반복 퀘스트

고현규·2024년 1월 24일
0
post-custom-banner

그동안 만들어 온 퀘스트 시스템이 일주일 만에 완성되었다.
아직 경험이 없다보니 처음 사용하는 부분이 많아 우여곡절이 많았다.

구조

1. 퀘스트가 순서대로 진행된다
Damage Upgrade -> HP Upgrade -> Defeat Enemy -> Stage Clear

2. 한 사이클이 돌아가면 퀘스트 목표치가 증가한다.

3. 능력치 증가 퀘스트 (Damage, HP Upgrade)는 퀘스트 진행중이 아니라도 현재 업그레이드 수치에 따라 진행 사항이 증가한다.

4. 퀘스트 진행사항을 저장한다. 저장 용량을 줄이기 위해 몇번째 퀘스트 인지만 Firebase를 통해 저장하고, 어떤 퀘스트인지와 퀘스트 목표치는 내부 스크립트로 구현한다.


코드

# QuestManager 
using UnityEngine;

public class QuestManager
{
    #region Fields

    private int questIndex;

    // Quest DB
    private QuestDamageUp DamageUp = new();
    private QuestHPUp HPUp = new();
    private QuestDefeatEnemy DefeatEnemy = new();
    private QuestReachStage ReachStage = new();

    #endregion

    #region Properties

    public int QuestNum { get; private set; }
    public QuestData[] QuestDB { get; private set; }
    public QuestData CurrentQuest { get; private set; }

    #endregion

    #region Init

    public void InitQuest()
    {
        // 데이터 불러오기
        QuestNum = Manager.Data.Profile.Quest_Complete;
        QuestDB = new QuestData[4];

        LoadQuestdataBase();
    }

    #endregion

    #region Save Load Json

    public void LoadQuestdataBase()
    {
        DamageUp.Init(QuestNum, QuestDB.Length);
        HPUp.Init(QuestNum, QuestDB.Length);
        DefeatEnemy.Init(QuestNum, QuestDB.Length);
        ReachStage.Init(QuestNum, QuestDB.Length);

        QuestDB[0] = DamageUp;
        QuestDB[1] = HPUp;
        QuestDB[2] = DefeatEnemy;
        QuestDB[3] = ReachStage;

        questIndex = QuestNum % QuestDB.Length;
        CurrentQuest = QuestDB[questIndex];
    }

각 퀘스트들을 생성해서 순환에 사용할 배열에 저장해 둡니다.
생성하는 퀘스트들은 전략패턴으로 구현하였습니다.

Firebase에 저장한 '클리어 한 퀘스트 개수' 데이터를 받아서 QuestNum 에 저장합니다.
QuestNum로 현재 진행 되어야 할 퀘스트의 Index를 계산해서 Current Quest에 저장합니다.

    #endregion

    public bool IsQuestComplete()
    {
        if (CurrentQuest.objectiveValue > CurrentQuest.currentValue)
        {
            CurrentQuest.isClear = false;
            return false;
        }
        else
        {
            CurrentQuest.isClear = true;
            EarnQuestReward();
            NextQuest();
            return true;
        }
    }

    // 다음 퀘스트로 넘어가기
    public void NextQuest()
    {
        CurrentQuest.ObjectiveValueUp();
        CurrentQuest.isClear = false;
        
        QuestNum++;
        questIndex++;

        if (questIndex >= QuestDB.Length)
        {
            questIndex = 0;
            QuestDB[2].currentValue = 0; // 몬스터 사냥 횟수 초기화
        }

        CurrentQuest = QuestDB[questIndex];
    }

    public void QuestCurrentValueUp()
    {
        CurrentQuest.currentValue++;
    }

    public void EarnQuestReward()
    {
        Manager.Game.Player.RewardGem(500);
    }
}

퀘스트 클리어 조건이 되었는지 판단하고 아니면 그대로, 맞다면 보상을 주고 다음 퀘스트로 넘아갑니다.
몬스터 사냥 횟수는 0부터 다시 사냥해야 하기 때문에 초기화 해줍니다.
다만, 게임을 껐다 켰을 때는 몬스터 사냥 횟수가 저장되야 해서 데이터로 남겨둘 필요가 있습니다.

# region Quest Data

public abstract class QuestData
{
    public QuestType questType;
    public string questObjective;
    public int ValueUpRate;
    public int objectiveValue;
    public int currentValue;
    public bool isClear;

    public abstract void Init(int questLevel, int questCount);

    public void ObjectiveValueUp()
    {
        objectiveValue *= ValueUpRate;
    }
}
#endregion

퀘스트 데이터는 추상클래스로 만들었고 전략패턴으로 쓰일 각 퀘스트의 부모입니다.

public class QuestDamageUp : QuestData
{
    public override void Init(int questLevel, int questCount)
    {
        questType = QuestType.DamageUp;
        questObjective = "Damage Upgrade";
        ValueUpRate = 2;
        objectiveValue = (questLevel / questCount) < 1 ? 10 : (questLevel / questCount) * ValueUpRate * 10;
        currentValue = Manager.Data.Profile.Stat_Level_AtkDamage;
        isClear = currentValue > objectiveValue;
    }
}

전략패턴의 자식 클래스인 퀘스트 입니다.
캐릭터와 퀘스트 데이터를 가져와 여기서 가공하여 저장합니다.
이런식의 코드를 각 퀘스트별로 만들어 4개 만듭니다.

 private void Die()
 {
     gameObject.layer = LayerMask.NameToLayer("Enemy");

     Manager.Game.Player.RewardGold(_rewards);

     if(Manager.Quest.CurrentQuest.questType == QuestType.DefeatEnemy)
     {
         Manager.Quest.QuestCurrentValueUp();
         UISceneMain uiSceneMain = Manager.UI.CurrentScene as UISceneMain;
         uiSceneMain.UpdateQuestObjective();
     }

     Destroy(gameObject);
 }

몬스터가 죽을 때 발생하는 메서드 입니다. if문 내부 코드가 퀘스트 부분입니다.
CurrentQuest가 '몬스터 잡기' 일 때 현재 퀘스트 값을 증가시키고 UI에 표기합니다.

private void UpdateQuestObjective()
{
    if (questType == QuestType.DamageUp)
    {
        Manager.Quest.QuestDB[0].currentValue++;
        UISceneMain uiSceneMain = Manager.UI.CurrentScene as UISceneMain;
        uiSceneMain.UpdateQuestObjective();
    }
    else if(questType == QuestType.HPUp)
    {
        Manager.Quest.QuestDB[1].currentValue++;
        UISceneMain uiSceneMain = Manager.UI.CurrentScene as UISceneMain;
        uiSceneMain.UpdateQuestObjective();
    }
}

캐릭터 스탯 강화 버튼을 눌렀을 때 발동되는 메서드 입니다.
버튼마다 어떤 타입의 퀘스트인지 Serialize하여 해당 버튼에 맞는 퀘스트 값이 증가됩니다.


후기

생성자, 상속 구조, 인터페이스를 실제로 사용하면서 배우는 기회가 되었다.
직접 사용하려고 하니 글로만 읽고 이해한 것과 달리 너무 어려웠다.
인터페이스를 실사용하는 부분이 가장 어려웠다.

내 생각에는 전략 패턴으로 이루어진 각 말단 객체가 상위 객체를 통해서 생성할 수 있다고 생각했는데, 결국 객체들을 따로 다 생성 한 뒤에 상위 객체인 추상 클래스나 인터페이스로 말단 객체를 컨트롤 한다는 개념을 알게 되었다.

Interface를 상속받은 클래스들을 Interface로 컨트롤 하는 부분은 아직도 헷갈린다.
이번에는 쓸려고 했다가 추상클래스로 전환하느라 확실하게 쓰진 못했다.
다시 쓸 기회가 있다면 그때 좀 걱정을 덜고 확실히 배우는 시간을 가져야 겠다.

profile
게임 개발과 기획
post-custom-banner

0개의 댓글