목표
게임 내에서 반복적인 행동 유도를 위한 퀘스트 시스템을 만들기 위해, 다음과 같은 구조를 설계하였습니다:
- 퀘스트 데이터를 JSON으로 불러와 자동 분류
- 일일, 주간, 업적 퀘스트 탭으로 구분된 UI 구성
- 조건 충족 시 보상 수령 가능 여부 표시 및 완료 처리
1. 퀘스트 데이터 관리 (QuestManager)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 퀘스트 데이터 구조 정의
[System.Serializable]
public class QuestData
{
public int key; // 고유 ID
public string name; // 퀘스트 이름
public int period; // 0: 일일, 1: 주간, 나머지: 업적
public QuestConditions conditionType; // 달성 조건 타입
public string questDesc; // 퀘스트 설명
public int conditionCount; // 달성해야 할 수치
public int currentConditionCount; // 현재 달성 수치
public int rewardKey; // 보상 아이템 ID
public int rewardAmount; // 보상 수량
public bool isClear; // 완료 여부
}
// 퀘스트 전체를 관리하는 매니저 클래스 (싱글톤)
public class QuestManager : Singleton<QuestManager>
{
public List<QuestData> dailyQuestList = new List<QuestData>();
public List<QuestData> weeklyQuestList = new List<QuestData>();
public List<QuestData> achievementQuestList = new List<QuestData>();
public event System.Action OnQuestUpdated; // UI에 알릴 이벤트
private void Start()
{
SetQuestData(); // 시작 시 퀘스트 데이터 로드
}
// 게임 매니저에서 퀘스트 테이블을 읽어와 리스트에 분류
public void SetQuestData()
{
foreach (var data in GameManager.Instance.DataManager.QuestTableLoader.ItemsList)
{
QuestData questData = new QuestData
{
key = data.key,
name = data.name,
period = data.period,
questDesc = data.conditionDesc,
conditionType = data.condition,
conditionCount = data.conditionCount,
currentConditionCount = 0,
rewardKey = data.rewardKey,
rewardAmount = data.rewardNum,
isClear = false
};
switch (data.period)
{
case 0: dailyQuestList.Add(questData); break;
case 1: weeklyQuestList.Add(questData); break;
default: achievementQuestList.Add(questData); break;
}
}
}
// 전체 퀘스트 리스트를 순회할 수 있게 제공
public IEnumerable<QuestData> GetAllQuestList()
{
foreach (var q in dailyQuestList) yield return q;
foreach (var q in weeklyQuestList) yield return q;
foreach (var q in achievementQuestList) yield return q;
}
// 조건 타입에 따라 퀘스트 진행도 업데이트
public void UpdateQuestProgress(QuestConditions type, int amount = 1)
{
foreach (var quest in GetAllQuestList())
{
if (quest.isClear) continue; // 완료된 퀘스트는 무시
if (quest.conditionType == type)
quest.currentConditionCount += amount;
}
// UI 갱신 이벤트 호출
OnQuestUpdated?.Invoke();
}
}
포인트
- QuestConditions 타입으로 진행도를 판단
- OnQuestUpdated 델리게이트를 통해 UI에 이벤트 전달
2. 퀘스트 UI 탭 및 슬롯 관리 (QuestUIHandler)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
// 퀘스트 패널을 제어하는 UI 핸들러 클래스
public class QuestUIHandler : MonoBehaviour
{
[SerializeField] private GameObject questSlot; // 슬롯 프리팹
private List<QuestSlot> questSlotPool = new List<QuestSlot>(); // 풀링 리스트
[Header("일간")]
[SerializeField] private GameObject dailyQuestPanel;
[SerializeField] private Button dailyQuestButton;
[SerializeField] private Transform dailyQuestTransform;
[Header("주간")]
[SerializeField] private GameObject weeklyQuestPanel;
[SerializeField] private Button weeklyQuestButton;
[SerializeField] private Transform weeklyQuestTransform;
[Header("업적")]
[SerializeField] private GameObject achievementableQuestPanel;
[SerializeField] private Button achievementableQuestButton;
[SerializeField] private Transform achievementableQuestTransform;
private void OnEnable() => QuestManager.Instance.OnQuestUpdated += SettingQuestSlots;
private void OnDisable() => QuestManager.Instance.OnQuestUpdated -= SettingQuestSlots;
private void Start()
{
// 탭 버튼 이벤트 연결
dailyQuestButton.onClick.AddListener(OnClickDailyQuestButton);
weeklyQuestButton.onClick.AddListener(OnClickWeeklyQuestButton);
achievementableQuestButton.onClick.AddListener(OnClickAchievementableQuestButton);
// 기본으로 일일 퀘스트 탭 활성화
dailyQuestPanel.SetActive(true);
weeklyQuestPanel.SetActive(false);
achievementableQuestPanel.SetActive(false);
SettingQuestSlots(); // 슬롯 초기화
}
// 탭 전환 메서드
private void OnClickDailyQuestButton()
{
dailyQuestPanel.SetActive(true);
weeklyQuestPanel.SetActive(false);
achievementableQuestPanel.SetActive(false);
}
private void OnClickWeeklyQuestButton()
{
dailyQuestPanel.SetActive(false);
weeklyQuestPanel.SetActive(true);
achievementableQuestPanel.SetActive(false);
}
private void OnClickAchievementableQuestButton()
{
dailyQuestPanel.SetActive(false);
weeklyQuestPanel.SetActive(false);
achievementableQuestPanel.SetActive(true);
}
// 퀘스트 데이터를 받아와 슬롯에 세팅
public void SettingQuestSlots()
{
int index = 0;
foreach (var data in QuestManager.Instance.GetAllQuestList())
{
// 퀘스트 타입에 따라 부모 오브젝트 결정
Transform targetParent = data.period switch
{
0 => dailyQuestTransform,
1 => weeklyQuestTransform,
_ => achievementableQuestTransform
};
QuestSlot slot;
if (index < questSlotPool.Count)
{
slot = questSlotPool[index];
slot.gameObject.SetActive(true);
}
else
{
slot = Instantiate(questSlot, targetParent).GetComponent<QuestSlot>();
questSlotPool.Add(slot);
}
slot.transform.SetParent(targetParent); // 부모 재지정
slot.SetQuestData(data);
index++;
}
// 완료된 퀘스트는 리스트의 맨 뒤로 이동
for (int i = 0; i < questSlotPool.Count; i++)
{
if (questSlotPool[i].questData.isClear)
questSlotPool[i].gameObject.transform.SetAsLastSibling();
}
// 사용하지 않는 슬롯은 비활성화
for (int i = index; i < questSlotPool.Count; i++)
{
questSlotPool[i].gameObject.SetActive(false);
}
}
}
포인트
- 슬롯 풀링을 적용하여 매 프레임마다 Instantiate를 하지 않도록 최적화
- 퀘스트 기간(period)에 따라 부모 오브젝트를 스위칭해서 표시
3. 개별 퀘스트 슬롯 UI 처리 (QuestSlot)
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
// 개별 퀘스트 슬롯에 대한 UI 처리 담당
public class QuestSlot : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI questName; // 퀘스트 이름 텍스트
[SerializeField] private Image rewardIcon; // 보상 아이콘 (사용 X 시 제거 가능)
[SerializeField] private TextMeshProUGUI rewardValue; // 보상 수량 텍스트
[SerializeField] private GameObject rewardAlertIcon; // 보상 수령 가능 표시
[SerializeField] private Button questSlotButton; // 슬롯 클릭 버튼
[SerializeField] private GameObject clearOverlay; // 완료 시 오버레이
public QuestData questData;
bool isAlert = false;
private void Start()
{
// 슬롯 클릭 시 보상 수령 처리
questSlotButton.onClick.AddListener(QuestClear);
}
// 슬롯에 데이터 바인딩
public void SetQuestData(QuestData data)
{
this.questData = data;
questName.text = data.name;
rewardValue.text = data.rewardAmount.ToString();
clearOverlay.SetActive(false);
// 이미 완료된 퀘스트는 클릭 불가 처리
if (data.isClear)
{
rewardAlertIcon.SetActive(false);
questSlotButton.enabled = false;
clearOverlay.SetActive(true);
return;
}
// 수령 가능 여부 판단
isAlert = data.currentConditionCount >= data.conditionCount;
rewardAlertIcon.SetActive(isAlert);
if (isAlert)
{
// 수령 가능 항목은 UI 상단에 정렬
this.gameObject.transform.SetAsFirstSibling();
}
}
// 보상 수령 처리
public void QuestClear()
{
if (isAlert)
{
questData.isClear = true;
this.gameObject.transform.SetAsLastSibling(); // 완료된 항목은 아래로
rewardAlertIcon.SetActive(false);
questSlotButton.enabled = false;
clearOverlay.SetActive(true); // 회색 오버레이 표시
}
}
}
포인트
- 보상 수령 조건 달성 시 빨간 표시 (rewardAlertIcon)
- 수령 후에는 클릭 불가 처리 및 반투명 오버레이 표시
마무리
이번 퀘스트 시스템 구현에서 신경 썼던 부분은 다음과 같습니다.
- 조건 기반 자동 분류 구조로 퀘스트 UI 유지 보수성 향상
- 풀링 방식 적용으로 슬롯 인스턴스 재활용 최적화
- 퀘스트 완료 여부에 따른 시각적 피드백 처리 (알림 아이콘, 오버레이 등)
'내일배움캠프 > Unity Final Projcet' 카테고리의 다른 글
[Unity] 강화카드 중복 제한 로직 구현 및 딕셔너리 기반 구조 개선하기 (0) | 2025.05.08 |
---|---|
[Unity] 모바일 UI가 노치·카메라 홀에 가려질 때 레터박스 처리하기 (트러블 슈팅) (0) | 2025.05.07 |
[Unity] 프로젝트 XSD 중간발표 회고 – 기능 너머의 고민들 (0) | 2025.05.02 |
[Unity] 타워디펜스 게임에서 2배속 기능과 실시간 타이머 유지 구현 (0) | 2025.05.01 |
[Unity] NavMeshAgent 장애물 감속 문제 (트러블슈팅) (0) | 2025.04.30 |