카테고리 없음

유니티 - 퀘스트 시스템을 구현해보자.

근본넘치는개발자 2024. 12. 30. 23:58

프로젝트를 진행하면서 퀘스트 시스템 구현한 내용을 정리하고자 합니다.

참고 자료의 내용을 토대로 퀘스트 시스템을 구축했습니다.

 

퀘스트 시스템을 구현하는 방법은 다양한데 

위 방식을 택한 이유는 싱글톤을 기반으로 다양한 조건 처리를 비교적 쉽게 할 수 있다는 점,

규모가 크지 않은 현재 프로젝트에 적용하기에 적합하다는 판단에서였습니다.

 

언제 사용하면 좋을지부터 장단점까지.

참고 자료에서 자세하게 정리해 주고 있어서

한번 읽어보시면 좋을 것 같습니다.

 

기본적인 형태는 MVC 패턴(참고 자료 기반)에

추가로 전략 패턴을 적용하여 구현했습니다.

 

엄연히 따지자면 QuestBase하고 UIQuestSlot에서

직접 참조하고 있는 부분들이 있어서 순수 MVC는 아닙니다.

 

2024.11.07 - [디자인 패턴] - 디자인 패턴 - MVC(Model-View-Controller)패턴에 대해 알아보자(+ MVP, MVVM)

 

구성은 다음과 같습니다.

  • Model: QuestBase
  • View: UIQuestSlot
  • Controller: QuestManager

 

작업한 엑셀 시트 (UGS 활용 중)

 

 

UGS에서는 첫 열을 딕셔너리의 키값으로 가지기에,

퀘스트ID를 키값으로 하고자 다음과 같이 만들었습니다.

 

지금은 데이터양이 많지 않지만 추후 확장까지 고려해서

(퀘스트ID / 10)을 통해 같은 퀘스트를 한눈에 찾고 구분할 수 있도록 하고자 했습니다.

또한 퀘스트 조건에 따라서 QuestType으로 구분하여 

각 조건에 맞게 코드에서 대응할 수 있도록 다음과 같이 작성했습니다.

(전략 패턴 적용, QuestBase를 상속받는 각 타입별 퀘스트 구현)

 

퀘스트가 아이템/재화를 요구하는 상황이면 Consume,

특정 몬스터 처치면 Kill 이런 식입니다.

 

코드 일부 공개

몬스터 처치에 따른 결과 코드 중 일부입니다.

 

 

QuestManager

더보기


    public void UpdateKillQuests(int enemyId, int amount = 1)
    {
        UpdateQuestsByType<KillQuest>(enemyId, amount);
    }

    public void UpdateConsumeQuests(int itemId, int amount = 1)
    {
        UpdateQuestsByType<ConsumeQuest>(itemId, amount);
    }


    public void UpdateStageQuests(int itemId, int amount = 1)
    {
        UpdateQuestsByType<StageQuest>(itemId, amount);
    }

    public void UpdateGachaQuest(int itemId, int amount = 1)
    {
        UpdateQuestsByType<GachaQuest>(itemId, amount);
    }

    public void UpdateEnforceQuests(int itemId, int amount = 1)
    {
        UpdateQuestsByType<EnforceQuest>(itemId, amount);
    }

    public void UpdateQuestClearQuest(int itemId, int amount = 1)
    {
        UpdateQuestsByType<QuestClearQuest>(itemId, amount);
    }


    private void UpdateQuestsByType<T>(int targetId, int amount = 1) where T : QuestBase
    {
        var currentQuests = GetCurrentQuests();
        foreach (var quest in currentQuests)
        {
            if (quest is T typedQuest &&
                (typedQuest.questData.requireConditionID == targetId || typedQuest.questData.requireConditionID == 0))
            {
                UpdateQuestProgress(quest.questData.ID, targetId, amount);
            }
        }
    }

UIQuestSlot

 

더보기

public void UpdateQuestProgress()
{
    titleText.text = _currentQuest.questData.description;
    int progress = _currentQuest.GetProgress();  
    int requireCount = _currentQuest.questData.requireCount;

    progressText.text = $"{progress}/{requireCount}";

    // 진행도 바 업데이트
    float progressBarText = (float)progress / requireCount;
    progressBar.fillAmount = progressBarText;

    // 보상 정보 표시
    rewardAmountText.text = _currentQuest.questData.rewardCount.ToString();

    rewardButton.interactable = _currentQuest.isCompleted;
}


// 완료 상태와 보상 버튼 관련 업데이트
public void UpdateRewardState()
{
    bool isCompleted = _currentQuest.isCompleted;
    bool hasReceivedReward = false;

    if (GameManager.Instance.playerData.questData.ContainsKey(_currentQuest.questData.ID))
    {
        hasReceivedReward = GameManager.Instance.playerData.questData[_currentQuest.questData.ID].isCompleted;

        QuestManager.Instance.UpdateQuestClearQuest(0);
    }
    else
    {
        QuestManager.Instance.CreateNewQuestSaveData(_currentQuest.questData.ID);
        hasReceivedReward = false;
    }

    rewardButton.interactable = isCompleted && !hasReceivedReward;
    completeMark.SetActive(hasReceivedReward);
}

public void OnRewardButtonClick()
{
    if (_currentQuest != null && _currentQuest.isCompleted)
    {
        QuestManager.Instance.QuestCompletion(_currentQuest.questData.ID);
        UpdateRewardState();
    }
}

Quest Base

 

더보기

Quest base : 


    // 각 퀘스트 타입에서 자신의 condition을 초기화
    protected abstract void InitializeCondition();

    public virtual int GetProgress()
    { 
        return condition.GetCurrentProgress();
    
    }

    // 데이터 로딩 후 각 퀘스트 컨디션 동기화
    public void SetProgress(int progress)
    {
        if (condition != null)
        {
            condition.SetProgress(progress);
            if (progress >= questData.requireCount)
            {
                isCompleted = true;
            }
        }
    }


    public void UpdateConditionProgress(int targetId, int count)
    {
        if (condition is ITargetQuset targetCondition)
        {
            targetCondition.UpdateProgress(targetId, count);
        }
        else if (condition is ICountQuest countCondition)
        {
            countCondition.UpdateProgress(count);
        }

         

        if (GetProgress() >= questData.requireCount)
        {
            isCompleted = true;
        }

    }

 

KillQuest, KillQuestCondition

더보기

public class KillQuest : QuestBase
{
    public KillQuest(QuestData data) : base(data)
    {
        InitializeCondition();
    }

    protected override void InitializeCondition()
    {
        condition = new KillQuestCondition(questData);
    }
}

public class KillQuestCondition : ITargetQuset
{
    private readonly QuestData questData;
    private int targetCount;
    private int currentCount;

    public KillQuestCondition(QuestData data)
    {
        this.questData = data;
        this.targetCount = data.requireCount;
        this.currentCount = 0;
    }

    public bool CheckCondition() => currentCount >= targetCount;

    public int GetCurrentProgress()
    {
        return currentCount;
    }

    public void UpdateProgress(int target, int killCount)
    {
        if(questData.requireConditionID == target)
        currentCount += killCount;
    }

    public void Reset()
    {
        currentCount = 0;
    }

    public void SetProgress(int progress)
    {
        currentCount = progress;
    }
}

 

ITargetQuest, IQuestCondition, ICountQuest

 

이렇게 구분한 이유는 

강화나 일부 퀘스트를 보면 아이템 ID가 필요 없기에 이렇게 구분했습니다. 

더보기

public interface IQuestCondition
{

    void Reset();
    int GetCurrentProgress();

    void SetProgress(int progress);
}

public interface ITargetQuset : IQuestCondition
{

    void UpdateProgress(int targetId, int count);

}

public interface ICountQuest : IQuestCondition
{

    void UpdateProgress(int count);

}


결과물 

 

마무리 

오늘은 프로젝트 기간동안 퀘스트 시스템을 구현했던 내용을 정리해봤습니다.

일단 테스트 배포가 코앞에 있어서 급하게 동작까지는 어떻게든 구현했지만

리팩토링 해야 할 부분이 많이 보여서 아마 더 작업을 진행 할 것 같습니다.

(퀘스트 슬롯 오브젝트 풀링 적용, UI 등 ) 

 

 

참고한 자료 

https://programmingdev.com/unity-%EC%9C%A0%EB%8B%88%ED%8B%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B8%B0%EB%B0%98-%ED%80%98%EC%8A%A4%ED%8A%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84-%EC%97%85%EC%A0%81-%EB%B0%8F-%EC%A3%BC/

 

[Unity] 유니티 데이터 기반 퀘스트 시스템 구현 (업적 및 주간 일간) - 달여행

퀘스트 시스템은 어떤 게임이던 필수적으로 들어가는 경우가 많습니다. 퀘스트 시스템 특성 상 게임의 여러 부분과 엮이다 보니 설계를 잘못할 경우 제작 일정에 큰 영향을 미칠 수 있습니다.

programmingdev.com