오늘은 어제 만들었던 QuestBase를 토대로 여러가지 퀘스트를 만들고, 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가 하게 되었다.
이 클래스는 플레이어가 아이템을 사용하면 클리어되는 퀘스트를 구현하고 있다.
// 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 함수가 호출되며 아이템이 사용되었다는 사실을 알게되어 퀘스트가 클리어 되는 로직이다.
앞에서 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 함수가 호출되며 현재 처치한 몬스터 수를 늘린다. 그렇게 현재 처치한 몬스터의 수가 요청된 몬스터 처치 수를 넘게 되면 퀘스트가 클리어 되는 로직이다.