[TIL] 44일 차 - Quest 만들기 (2)

ChangBeom·2025년 3월 28일

TIL

목록 보기
45/53
post-thumbnail

오늘은 어제 만들었던 QuestBase를 토대로 여러가지 퀘스트를 만들고, QuestManager에서 모든 퀘스트들을 관리하도록 구현해볼 예정이다.


[QuestManager]

어제까진 QuestBase에서 퀘스트의 모든 처리를 담당했는데, 이젠 QuestManager에서 모든 퀘스트들을 저장해두고 시작과 클리어 처리를 맡길 것이다.

//  QuestManager.cs
using System.Collections.Generic;
using UnityEngine;

public class QuestManager : MonoSingleton<QuestManager>
{
    public QuestBase[] questArr;
    private Dictionary<int, QuestBase> questDictionary;

    private void Start()
    {
        questArr = GetComponentsInChildren<QuestBase>();
        questDictionary = new Dictionary<int, QuestBase>();

        for(int i = 0; i < questArr.Length; i++)
        {
            questDictionary[questArr[i].questId] = questArr[i];
        }
    }

    /// <summary>
    /// 퀘스트 시작 시 text를 UI에 출력
    /// </summary>
    public void QuestStart(int id)
    {
        QuestBase quest = questDictionary[id];

        quest.questState = QuestState.ONGOING;

        Debug.Log($"{questDictionary[id].questName}");
        Debug.Log($"{questDictionary[id].questDescription}");
        Debug.Log("퀘스트 시작");
    }

    /// <summary>
    /// 퀘스트 클리어 시 text를 UI에 출력
    /// </summary>
    public void QuestClear(int id)
    {
        QuestBase quest = questDictionary[id];

        quest.questState = QuestState.CLEAR;

        Debug.Log("퀘스트 완료");
    }

    /// <summary>
    /// 모든 퀘스트를 진행 전 상태로 리셋
    /// </summary>
    public void QuestReset()
    {
        Debug.Log("퀘스트 리셋");
        for(int i = 0; i < questDictionary.Count; i++)
        {
            questDictionary[i].questState = QuestState.BEFORE;
        }
    }
}

간단하게 설명하면, MonoSingleton을 상속받아 싱글톤으로 만들어서 모든 퀘스트에서 쉽게 접근할 수 있도록 만들고, QuestManager의 자식객체로 들어있는 퀘스트들을 받아와서 퀘스트의 id를 키값으로 딕셔너리에 저장하여 관리하는 방식이다. 이렇게 저장해둔 퀘스트는 id를 통해 쉽게 접근할 수 있으며, 이를 활용해서 퀘스트의 시작, 클리어, 리셋 등을 처리하는 것이다.

QuestBase에서 처리하던 일들을 QuestManager에 넘겨서 QuestBase는 다음과 같이 수정되었다.

//  QuestBase.cs
using UnityEngine;

public enum QuestState
{
    BEFORE,
    ONGOING,
    CLEAR,
}

public abstract class QuestBase : MonoBehaviour
{
    [Header("공통 퀘스트 정보")]
    public int questId;
    public string questName;
    public string questDescription;
    public QuestState questState = QuestState.BEFORE;

    protected QuestManager questManager;

    protected virtual void Start()
    {
        questManager = QuestManager.Instance;
    }

    protected virtual void Update()
    {
        if(questState == QuestState.ONGOING)
        {
            QuestGoal();
        }
    }

    /// <summary>
    /// 퀘스트의 목적, 무엇을 해야 완료되는 지
    /// </summary>
    protected abstract void QuestGoal();

    /// <summary>
    /// 퀘스트 초기 설정
    /// </summary>
    protected abstract void QuestInit();

    /// <summary>
    /// 해당 퀘스트의 범위 안에 들어왔을 때, 대상이 플레이어인 경우, 퀘스트 시작
    /// </summary>
    /// <param name="other"> 충돌 대상 </param>
    protected virtual void OnTriggerEnter(Collider other)
    {
        if(questState == QuestState.BEFORE)
        {
            if (other.CompareTag("Player"))
            {
                QuestInit();
                questManager.QuestStart(questId);
            }
        }
    }
}

이제 QuestBase에는 해당 퀘스트의 정보, 클리어 조건, 어떻게 하면 퀘스트가 시작 되는 지 등 일련의 퀘스트 과정에 필요한 로직들만 남게되고, Quest의 관리는 QuestManager가 하게 되었다.


[QuestUseItem]

이 클래스는 플레이어가 아이템을 사용하면 클리어되는 퀘스트를 구현하고 있다.

//  Player.cs
using System;
using UnityEngine;

public class Player : Monobehavior
{  
    public event Action OnItemUsed;

    public void UseItem()
    {
        OnItemUsed?.Invoke();
    }
}




//  QuestUseItem.cs  
public class QuestUseItem : QuestBase
{
	private bool _isuUseItem;
 
    /// <summary>
    /// 퀘스트 요구사항 초기화 및 이벤트 구독
    /// </summary>
    protected override void QuestInit()
    {
        _isuUseItem = false;
        GameManager.Instance.Player.OnItemUsed += UseItemCheck;
    }

    /// <summary>
    /// 아이템이 사용되면 바로 퀘스트를 클리어 처리하고 이벤트 구독 취소
    /// </summary>
    protected override void QuestGoal()
    {
        if(questState == QuestState.ONGOING)
        {
            if (_isuUseItem)
            {
                questManager.QuestClear(questId);
                GameManager.Instance.Player.OnItemUsed -= QuestGoal;
            }
        }
    }

    private void UseItemCheck()
    {
        _isuUseItem = true;
    }
}

로직을 보면 아주 간단하다. Player에 이벤트 액션으로 OnItemUsed를 선언하고, 아이템이 사용될 때 OnItemUsed를 호출한다. 그럼 이제 QuestUseItem에서 구독해둔 UseItemCheck 함수가 호출되며 아이템이 사용되었다는 사실을 알게되어 퀘스트가 클리어 되는 로직이다.


[QuestKillEnemy]

앞에서 QuestUseItem을 만들었던 방식을 그대로 이용해서 적을 일정 마리 수 이상 처치하면 클리어 되는 퀘스트를 만들었다.

//  EnemyManager.cs
using System;
using UnityEngine;

public class EnemyManager : MonoSingleton<QuestManager>
{  
    public event Action OnDie;

    public void Die()
    {
        OnDie?.Invoke();
    }
}




//  QuestKillEnemy.cs
using UnityEngine;

public class QuestKillEnemy : QuestBase
{
    [Space(10)]
    [Header("적 처치 퀘스트 정보")]
    public int requiredKillEnemyCount;

    private int _curKillEnemyCount;

    /// <summary>
    /// 퀘스트 요구치 초기화 및 이벤트 구독
    /// </summary>
    protected override void QuestInit()
    {
        _curKillEnemyCount = 0;
        EnemyManager.Instance.OnDie += KillEnemy;
    }

    /// <summary>
    /// 요구 처치 마리수를 채우면 퀘스트를 클리어 처리하고 이벤트 구독 취소
    /// </summary>
    protected override void QuestGoal()
    {
        if(questState == QuestState.ONGOING)
        {
            if(_curKillEnemyCount >= requiredKillEnemyCount)
            {
                questManager.QuestClear(questId);
                EnemyManager.Instance.OnDie -= KillEnemy;
            }
        }
    }

    private void KillEnemy()
    {
        _curKillEnemyCount++;
    }
}

로직은 QuestUseItem과 거의 유사하다. EnemyManager에 이벤트 액션으로 OnDie를 선언하고, 몬스터가 처치될 때 OnDie를 호출한다. 그러면 QuestKillEnemy에서 구독해둔 KillEnemy 함수가 호출되며 현재 처치한 몬스터 수를 늘린다. 그렇게 현재 처치한 몬스터의 수가 요청된 몬스터 처치 수를 넘게 되면 퀘스트가 클리어 되는 로직이다.

                                             

0개의 댓글