게임에서 퀘스트는 방향성을 제공해 주고 게임의 목적성과 재미를 높여주며, 진행 상황을 추적하고 보상을 제공하여 게임을 보다 흥미롭게 만들어줍니다. 퀘스트 시스템을 구현하고 정리하였습니다.
💬 서론
- 본 글은 게임을 모두 만든 후에 작성한 글로 핵심 주제 외 클래스 및 다른 기능에 대한 함수가 포함될 수 있습니다.
📖 구현 내용
- 적 처치, 아이템 습득에 대한 퀘스트를 구현합니다.
- 플레이어는 퀘스트를 받고 퀘스트를 완료할 수 있습니다.
- 퀘스트 상태를 확인하여 상태별로 이벤트를 처리할 수 있습니다.
- 스크립터블 오브젝트로 퀘스트의 데이터를 관리합니다.
- 스크립터블 오브젝트를 프로젝트 폴더 내에서 관리하고, 이 데이터를 자동으로 불러옵니다.
- ID를 이용하여 고유한 퀘스트의 중복 여부를 검사할 수 있습니다.
- 하나의 퀘스트에서 여러 개의 목표 중 하나만 수행해도 퀘스트를 완료하는지에 대한 여부를 설정할 수 있습니다.
- 퀘스트 콘텐츠를 보여주고 현재 진행 시점, 완료한 퀘스트들의 정보를 볼 수 있습니다.
⚒️ 구현
- 이번 글에서는 플레이어가 퀘스트를 이용할 때 그 내용들을 텍스트로 보여주기 위한 콘텐츠를 관리하는 기능을 다룹니다.
· ContentManager.cs
더보기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Text;
using TMPro;
namespace Quest
{
/// <summary>
/// 퀘스트를 텍스트로 보여주는 기능을 수행하는 매니저
/// </summary>
public class QuestContentManager : Singleton<QuestContentManager>
{
private static bool mIsQuestDialogEnable = false;
public static bool IsQuestDialogEnable
{
get
{
return mIsQuestDialogEnable;
}
}
[Header("풀사이즈 퀘스트 다이얼로그 오브젝트")]
[SerializeField] private GameObject mFullSizeQuestDialog;
[Header("퀘스트 설명 텍스트 콘텐츠")][SerializeField] private QuestContentDataReader mContentsScriptableObject; //퀘스트 설명 텍스트
Dictionary<int, QuestContentData> mQuestContents = new Dictionary<int, QuestContentData>(); //퀘스트 텍스트 콘텐츠
[Space(50)]
[Header("UI 영역")]
[Header("컴팩트 퀘스트 창을 인스턴스 할 부모")][SerializeField] private RectTransform mQuestCompactRoot;
[Header("컴팩트 퀘스트 창 프리팹")][SerializeField] private GameObject mQuestCompactPrefab;
[Header("풀사이즈 퀘스트 창을 인스턴스 할 부모")][SerializeField] private RectTransform mQuestFullRoot;
[Header("풀사이즈 퀘스트 창 프리팹")][SerializeField] private GameObject mQuestFullPrefab;
[Header("풀사이즈 선택 콘텐츠 라벨")] [SerializeField] private TextMeshProUGUI mFullSelectedContentLabel;
private Dictionary<int, QuestCompactContent> mCompactQuestContents = new Dictionary<int, QuestCompactContent>(); //컴팩트 영역에 보여지는 퀘스트들
private Dictionary<int, QuestFullContent> mFullQuestContents = new Dictionary<int, QuestFullContent>(); //풀사이즈 영역에 보여지는 퀘스트들
private void Awake()
{
// 초기화시 전역 활성화상태 해제
QuestContentManager.mIsQuestDialogEnable = false;
//텍스트 콘텐츠 로드
foreach (QuestContentData contentsData in mContentsScriptableObject.DataList) { mQuestContents.Add(contentsData.id, contentsData); }
}
private void Update()
{
TryOpenDialog();
}
private void TryOpenDialog()
{
//옵션이 켜져있는경우 비활성화
if (GameMenuManager.IsOptionActive) { return; }
if (Input.GetKeyDown(KeyManager.Instance.GetKeyCode("Quest")))
{
if(mFullSizeQuestDialog.activeInHierarchy)
{
mFullSizeQuestDialog.SetActive(false);
mIsQuestDialogEnable = false;
UtilityManager.TryLockCursor();
}
else
{
mFullSizeQuestDialog.SetActive(true);
mIsQuestDialogEnable = true;
UtilityManager.UnlockCursor();
}
}
}
public void CompleteQuest(QuestData questData)
{
ToggleCompactQuestContent(questData, false);
mFullQuestContents[questData.questId].CompleteQuest();
}
public void ToggleCompactQuestContent(QuestData questData, bool isEnable)
{
if (isEnable) //활성화?
{
if (mCompactQuestContents.ContainsKey(questData.questId)) // 이미 퀘스트가 있는경우 > 오류
Debug.LogErrorFormat("{0} 퀘스트가 중복됨!", questData.questId);
else
{
// 퀘스트 콘텐츠 프리팹 인스턴스
QuestCompactContent newQuestContent = Instantiate(mQuestCompactPrefab, Vector3.zero, Quaternion.identity, mQuestCompactRoot).GetComponent<QuestCompactContent>();
// 퀘스트 데이터 삽입
newQuestContent.Init(questData);
// 딕셔너리에 삽입
mCompactQuestContents.Add(questData.questId, newQuestContent);
// 최초 1회 퀘스트 업데이트
newQuestContent.UpdateCompactQuestContents(mQuestContents[questData.questId]);
}
}
else
{
// 딕셔너리에 퀘스트가 있으면?
if(mCompactQuestContents.ContainsKey(questData.questId))
{
// 제거
Destroy(mCompactQuestContents[questData.questId].gameObject);
mCompactQuestContents.Remove(questData.questId);
}
}
StartCoroutine(COR_RefreshQuestCompactLayout());
}
public void AddFullQuestContent(QuestData questData)
{
if (mFullQuestContents.ContainsKey(questData.questId)) { Debug.LogErrorFormat("{0} 퀘스트가 중복됨!", questData.questId); } //이미 퀘스트가 있는경우 > 오류
else
{
// 퀘스트 콘텐츠 프리팹 인스턴스
QuestFullContent newQuestContent = Instantiate(mQuestFullPrefab, Vector3.zero, Quaternion.identity, mQuestFullRoot).GetComponent<QuestFullContent>();
// 퀘스트 데이터 삽입
newQuestContent.Init(questData);
// 딕셔너리에 삽입
mFullQuestContents.Add(questData.questId, newQuestContent);
// 최초 1회 퀘스트 업데이트
newQuestContent.UpdateCompactQuestContents(mQuestContents[questData.questId]);
// 위치를 가장 최상단으로 옮기기
newQuestContent.transform.SetAsFirstSibling();
// 컴팩트 퀘스트 추가
ToggleCompactQuestContent(questData, true);
}
}
public void UpdateCurrentQuestState(int questId)
{
if(mCompactQuestContents.ContainsKey(questId))
mCompactQuestContents[questId].UpdateCompactQuestContents(mQuestContents[questId]);
if(mFullQuestContents.ContainsKey(questId))
mFullQuestContents[questId].UpdateCompactQuestContents(mQuestContents[questId]);
}
public void ToggleFullQuestContent(QuestData questData, bool isEnable = true)
{
if(isEnable == false)
{
mFullSelectedContentLabel.text = "";
}
else
{
// 퀘스트 콘텐츠 데이터 획득
QuestContentData contentData = mQuestContents[questData.questId];
// 스트링 빌더 사용
StringBuilder stringBuilder = new StringBuilder();
// 문자열 구성
stringBuilder.Append($"<b>{contentData.title}</b>\n");
stringBuilder.Append($"<size=16>의뢰인: {contentData.receiveFrom}\n</size>");
stringBuilder.Append($"\n<size=14>{contentData.fullContent}</size>");
mFullSelectedContentLabel.text = stringBuilder.ToString();
}
}
#region 코루틴
private IEnumerator COR_RefreshQuestCompactLayout()
{
VerticalLayoutGroup compactRoot = mQuestCompactRoot.GetComponent<VerticalLayoutGroup>();
compactRoot.reverseArrangement = true;
yield return null;
compactRoot.reverseArrangement = false;
}
#endregion
}
}
private static bool mIsQuestDialogEnable = false;
public static bool IsQuestDialogEnable
{
get
{
return mIsQuestDialogEnable;
}
}
- 별도의 UI창을 이용하기 때문에 그 다이얼로그 창이 열려있는지 확인하기 위한 변수입니다.
[Header("풀사이즈 퀘스트 다이얼로그 오브젝트")]
[SerializeField] private GameObject mFullSizeQuestDialog;
- 위에서 언급한 별도의 UI창이 이 오브젝트에 해당합니다.
[Header("퀘스트 설명 텍스트 콘텐츠")][SerializeField] private QuestContentDataReader mContentsScriptableObject; //퀘스트 설명 텍스트
Dictionary<int, QuestContentData> mQuestContents = new Dictionary<int, QuestContentData>(); //퀘스트 텍스트 콘텐츠
- 구글 스프레드시트에서 데이터를 읽어 가져온 값을 레퍼런스 하고, 딕셔너리에 저장합니다.
- "[유니티] 구글 스프레드 시트(엑셀) 연동 3 - 데이터 가져오기"에서 그 내용을 확인할 수 있습니다.
public struct QuestContentData
{
[ShowOnly] public int id;
[TextArea] public string title;
[TextArea] public string receiveFrom;
[TextArea] public string compactContent;
[TextArea] public string fullContent;
}
- 위에서 언급한 QuestContentDataReader에서 읽어오는 데이터의 구조입니다.
[Header("UI 영역")]
[Header("컴팩트 퀘스트 창을 인스턴스 할 부모")][SerializeField] private RectTransform mQuestCompactRoot;
[Header("컴팩트 퀘스트 창 프리팹")][SerializeField] private GameObject mQuestCompactPrefab;
[Header("풀사이즈 퀘스트 창을 인스턴스 할 부모")][SerializeField] private RectTransform mQuestFullRoot;
[Header("풀사이즈 퀘스트 창 프리팹")][SerializeField] private GameObject mQuestFullPrefab;
[Header("풀사이즈 선택 콘텐츠 라벨")] [SerializeField] private TextMeshProUGUI mFullSelectedContentLabel;
- 컴팩트 사이즈와 풀사이즈 UI를 제공합니다.
- 컴팩트 사이즈는 좌측 상단에 요약된 정보를 제공하고, 풀사이즈는 생략 없는 모든 정보를 제공합니다. 차이는 다음 이미지와 같습니다.
private Dictionary<int, QuestCompactContent> mCompactQuestContents = new Dictionary<int, QuestCompactContent>(); //컴팩트 영역에 보여지는 퀘스트들
private Dictionary<int, QuestFullContent> mFullQuestContents = new Dictionary<int, QuestFullContent>(); //풀사이즈 영역에 보여지는 퀘스트들
- 컴팩트, 풀사이즈 UI들이 보이게 되면 이 딕셔너리에 그 값들이 삽입됩니다.
- 어떤 이유에 의해 해당 오브젝트들이 보일 필요가 없다면, 이 딕셔너리에 key값인 questId를 사용하여 접근할 수 있습니다.
private void Awake()
{
// 초기화시 전역 활성화상태 해제
QuestContentManager.mIsQuestDialogEnable = false;
//텍스트 콘텐츠 로드
foreach (QuestContentData contentsData in mContentsScriptableObject.DataList) { mQuestContents.Add(contentsData.id, contentsData); }
}
- mContentsScriptableObject에서 모든 데이터를 읽어 사용 준비를 완료합니다.
private void TryOpenDialog()
{
//옵션이 켜져있는경우 비활성화
if (GameMenuManager.IsOptionActive) { return; }
if (Input.GetKeyDown(KeyManager.Instance.GetKeyCode("Quest")))
{
if(mFullSizeQuestDialog.activeInHierarchy)
{
mFullSizeQuestDialog.SetActive(false);
mIsQuestDialogEnable = false;
UtilityManager.TryLockCursor();
}
else
{
mFullSizeQuestDialog.SetActive(true);
mIsQuestDialogEnable = true;
UtilityManager.UnlockCursor();
}
}
}
- 풀사이즈 UI 다이얼로그 창을 열고 닫습니다.
public void CompleteQuest(QuestData questData)
{
ToggleCompactQuestContent(questData, false);
mFullQuestContents[questData.questId].CompleteQuest();
}
- QuestManager에서 퀘스트 완료가 되었다면, 이 함수가 호출되어 콘텐츠에 퀘스트가 완료되었다고 알립니다.
- 퀘스트가 완료되면 컴팩트UI에 해당 퀘스트를 더 이상 표시하지 않아도 되는 등 여러 추가 기능을 수행하기 위해 사용합니다.
public void ToggleCompactQuestContent(QuestData questData, bool isEnable)
{
if (isEnable) //활성화?
{
if (mCompactQuestContents.ContainsKey(questData.questId)) // 이미 퀘스트가 있는경우 > 오류
Debug.LogErrorFormat("{0} 퀘스트가 중복됨!", questData.questId);
else
{
// 퀘스트 콘텐츠 프리팹 인스턴스
QuestCompactContent newQuestContent = Instantiate(mQuestCompactPrefab, Vector3.zero, Quaternion.identity, mQuestCompactRoot).GetComponent<QuestCompactContent>();
// 퀘스트 데이터 삽입
newQuestContent.Init(questData);
// 딕셔너리에 삽입
mCompactQuestContents.Add(questData.questId, newQuestContent);
// 최초 1회 퀘스트 업데이트
newQuestContent.UpdateCompactQuestContents(mQuestContents[questData.questId]);
}
}
else
{
// 딕셔너리에 퀘스트가 있으면?
if(mCompactQuestContents.ContainsKey(questData.questId))
{
// 제거
Destroy(mCompactQuestContents[questData.questId].gameObject);
mCompactQuestContents.Remove(questData.questId);
}
}
StartCoroutine(COR_RefreshQuestCompactLayout());
}
- 컴팩트 UI는 플레이어의 기호에 따라 언제든지 비활성화될 수 있습니다.
- isEnable 매개변수를 이용하여 questData에 해당하는 퀘스트를 비활성화하거나 활성화할 수 있습니다.
public void AddFullQuestContent(QuestData questData)
{
if (mFullQuestContents.ContainsKey(questData.questId)) { Debug.LogErrorFormat("{0} 퀘스트가 중복됨!", questData.questId); } //이미 퀘스트가 있는경우 > 오류
else
{
// 퀘스트 콘텐츠 프리팹 인스턴스
QuestFullContent newQuestContent = Instantiate(mQuestFullPrefab, Vector3.zero, Quaternion.identity, mQuestFullRoot).GetComponent<QuestFullContent>();
// 퀘스트 데이터 삽입
newQuestContent.Init(questData);
// 딕셔너리에 삽입
mFullQuestContents.Add(questData.questId, newQuestContent);
// 최초 1회 퀘스트 업데이트
newQuestContent.UpdateCompactQuestContents(mQuestContents[questData.questId]);
// 위치를 가장 최상단으로 옮기기
newQuestContent.transform.SetAsFirstSibling();
// 컴팩트 퀘스트 추가
ToggleCompactQuestContent(questData, true);
}
}
- 풀사이즈 UI는 플레이어가 원할 때 콘텐츠 영역을 제거할 순 없습니다. 대신 창 자체를 닫을 수 있습니다.
- 그렇기에 toggle에 대한 속성은 없고, 퀘스트를 받을 때 추가하는 기능밖에 없습니다.
public void UpdateCurrentQuestState(int questId)
{
if(mCompactQuestContents.ContainsKey(questId))
mCompactQuestContents[questId].UpdateCompactQuestContents(mQuestContents[questId]);
if(mFullQuestContents.ContainsKey(questId))
mFullQuestContents[questId].UpdateCompactQuestContents(mQuestContents[questId]);
}
- 특정 조건에 의해 퀘스트 내용이 변경되어야 하면 호출되며 가지고 있는 모든 콘텐츠 텍스트들을 갱신합니다.
public void ToggleFullQuestContent(QuestData questData, bool isEnable = true)
{
if (isEnable == false)
{
mFullSelectedContentLabel.text = "";
}
else
{
// 퀘스트 콘텐츠 데이터 획득
QuestContentData contentData = mQuestContents[questData.questId];
// 스트링 빌더 사용
StringBuilder stringBuilder = new StringBuilder();
// 문자열 구성
stringBuilder.Append($"<b>{contentData.title}</b>\n");
stringBuilder.Append($"<size=16>의뢰인: {contentData.receiveFrom}\n</size>");
stringBuilder.Append($"\n<size=14>{contentData.fullContent}</size>");
mFullSelectedContentLabel.text = stringBuilder.ToString();
}
}
- 풀사이즈 UI의 세부 콘텐츠를 보기 위해 호출하는 함수입니다.
- 이 함수가 호출되면 풀사이즈 UI 다이얼로그 하단에 긴 텍스트가 나타나게 됩니다.
- 그 예시는 아래 사진과 같습니다.
private IEnumerator COR_RefreshQuestCompactLayout()
{
VerticalLayoutGroup compactRoot = mQuestCompactRoot.GetComponent<VerticalLayoutGroup>();
compactRoot.reverseArrangement = true;
yield return null;
compactRoot.reverseArrangement = false;
}
- 컴팩트 영역에서 사용할 VerticalLayoutGroup에서 새로운 오브젝트가 인스턴스 될 때, 자동으로 트랜스폼들을 갱신하기 위해 사용합니다.
· UI 설정
- 전체 구성은 다음과 같습니다.
- 다음은 각 요소별 세부 설명입니다.
1. mQuestCompactRoot
- 컴팩트 UI 오브젝트를 인스턴스 할 부모 트랜스폼입니다.
- Vertical Layout Group을 이용하여 오브젝트가 여러 개 생성되면 수직 방향으로 쌓이게 구현하였습니다.
2. mQuestCompactPrefab
- 콤팩트 UI를 보여줄 프리팹입니다.
- 자세한 정보는 "[유니티] 퀘스트 시스템(4) - 컴팩트 UI"에서 이 내용을 확인할 수 있습니다.
3. mQuestFullRoot
- 풀사이즈 퀘스트 콘텐츠에서 각 슬롯 인스턴스를 할 위치가 될 트랜스폼입니다.
- 아래 이미지의 "인스턴스 스크롤 뷰"에 해당합니다.
4. mQuestFullPrefab
- 풀사이즈 UI에서 인스턴스 된 각 슬롯에 해당하는 프리팹입니다.
- 자세한 정보는 "[유니티] 퀘스트 시스템(5) - 풀사이즈 UI"에서 이 내용을 확인할 수 있습니다.
5. mFullSelectedContentLabel
- 풀사이즈 UI에서 인스턴스 된 각 슬롯을 선택하면 나타나는 모든 텍스트를 보여줄 텍스트 라벨입니다.
- 위 이미지에서 "텍스트 스크롤 뷰" 콘텐츠 내부의 라벨을 사용합니다.
✅ 사용
- 이 매니저를 사용하기 위해서는 mQuestCompactPrefab와 mQuestFullPrefab이 준비되어있어야 합니다.
- "[유니티] 퀘스트 시스템(4) - 컴팩트 UI"에서 이 프리팹에 대한 내용을 볼 수 있습니다.
'unity game modules' 카테고리의 다른 글
[유니티] 퀘스트 시스템(5) - 풀사이즈 UI (0) | 2023.04.11 |
---|---|
[유니티] 퀘스트 시스템(4) - 컴팩트 UI (0) | 2023.04.11 |
[유니티] 퀘스트 시스템(2) - 퀘스트 매니저 (0) | 2023.04.11 |
[유니티] 퀘스트 시스템(1) - 퀘스트 데이터 (0) | 2023.04.11 |
[유니티] 오브젝트의 중심을 기준으로 회전 (0) | 2023.04.08 |