이전에 진행한 팀 스크럼에서 퀘스트의 데이터 테이블을 구글 시트로 만들고,구글 시트의 데이터를 Unity로 가져와서 별도의 Scene수정 없이 퀘스트를 자동으로 생성하면 좋겠다는 이야기가 나와서 그것들을 구현 해보려고 한다.
먼저 데이터 테이블에서 관리할 데이터들을 정리해보자.
- id : 퀘스트의 키값
- name : 퀘스트의 이름
- description : 퀘스트 설명
- position : 퀘스트를 생성할 위치
- scale : 퀘스트 시작 범위
- type : 어떤 퀘스트인지
- type에 따른 변수 : ex) 이동 퀘스트에서 얼마나 이동하면 클리어 되는지, 적을 처치하는 퀘스트에서 몇 명을 처치해야 하는 지 등
*position과 scale은 왜 필요한지 의문일 수도 있는 데, 현재 퀘스트 로직이 퀘스트에 Trigger가 존재하고, Player가 Trigger에 충돌하면 퀘스트가 시작되기 때문에 그 Trigger의 위치와 크기라고 생각하면 된다.
이 정보들을 토대로 QuestData를 만들면 다음과 같다.
using GoogleSheetsToUnity; using System.Collections.Generic; using UnityEngine; using UnityEditor; using UnityEngine.Events; using System; public enum QuestType { PLAYER_MOVE, USE_ITEM, KILL_ENEMY, } [Serializable] public struct QuestInfo { public int id; public string name; public string description; public Vector3 position; public Vector3 scale; public QuestType type; // PLAYER_MOVE type일 때 사용 public float requiredDistance; // KILL_ENEMY type일 때 사용 public int requiredKillEnemyCount; } [CreateAssetMenu(fileName = "QuestData", menuName = "Scriptable Object/New QuestData")] public class QuestData : SheetDataReaderBase { public List<QuestInfo> questInfoList = new List<QuestInfo>(); QuestInfo questInfo; /// <summary> /// 행의 키워드를 토대로 열의 값들을 가져와서 가공하는 함수 /// </summary> /// <param name="list"> 구글 시트의 열 </param> public void UpdateStats(List<GSTU_Cell> list) { questInfo = new QuestInfo(); foreach (var cell in list) { switch (cell.columnId) { case "id": questInfo.id = int.Parse(cell.value); break; case "name": questInfo.name = cell.value; break; case "description": questInfo.description = cell.value; break; case "posX": questInfo.position.x = float.Parse(cell.value); break; case "posY": questInfo.position.y = float.Parse(cell.value); break; case "posZ": questInfo.position.z = float.Parse(cell.value); break; case "scaleX": questInfo.scale.x = float.Parse(cell.value); break; case "scaleY": questInfo.scale.y = float.Parse(cell.value); break; case "scaleZ": questInfo.scale.z = float.Parse(cell.value); break; case "type": questInfo.type = (QuestType)Enum.Parse(typeof(QuestType), cell.value); break; case "requiredDistance": if(questInfo.type == QuestType.PLAYER_MOVE) { questInfo.requiredDistance = float.Parse(cell.value); } break; case "requiredKillEnemyCount": if(questInfo.type == QuestType.KILL_ENEMY) { questInfo.requiredKillEnemyCount = int.Parse(cell.value); } break; } } questInfoList.Add(questInfo); } #if UNITY_EDITOR [CustomEditor(typeof(QuestData))] public class QuestDataReaderEditor : Editor { QuestData data; private void OnEnable() { data = (QuestData)target; } /// <summary> /// 커스텀 에디터 버튼. 누르면 데이터를 읽어옴 /// </summary> public override void OnInspectorGUI() { base.OnInspectorGUI(); GUILayout.Space(20); if (GUILayout.Button("구글 시트 데이터 읽어오기")) { ReadSheet(UpdateData); data.questInfoList.Clear(); } } /// <summary> /// 구글 시트의 데이터를 읽어옴 /// </summary> /// <param name="callback"> 데이터를 읽어오는 함수 </param> /// <param name="mergedcells"> 병합된 셀을 고려할 것인지 </param> private void ReadSheet(UnityAction<GstuSpreadSheet> callback, bool mergedcells = false) { SpreadsheetManager.Read(new GSTU_Search(data.sheetURL, data.sheetName), callback, mergedcells); } /// <summary> /// 지정한 시작열부터 끝열까지의 데이터를 전부 가져옴 /// </summary> /// <param name="sheet"> 구글 시트의 데이터 </param> private void UpdateData(GstuSpreadSheet sheet) { for (int i = data.startRowIndex; i <= data.endRowIndex; ++i) { data.UpdateStats(sheet.rows[i]); } EditorUtility.SetDirty(target); } } } #endif
위 코드는 커스텀 에디터로 만든 버튼을 통해 구글 시트에서 데이터를 가져와서 QuestData라는 ScriptableObject에 저장해두는 코드이다.
Google Sheet에서 데이터를 가져와서 가공하는 부분은 이전에 정리했던 글을 참고하자.
이제 가져온 이 데이터를 토대로 QuestManager에서 Scene에 퀘스트를 추가해보자.
이전의 QuestManager는 퀘스트의 시작과 클리어를 관리하는 일만 하고 있었다면, 이젠 퀘스트를 생성하여 맵에 배치하는 역할까지 수행한다.
using System.Collections.Generic; using UnityEngine; public class QuestManager : MonoSingleton<QuestManager> { public QuestData questData; private Dictionary<int, QuestBase> questDictionary; protected override void Awake() { base.Awake(); InitSpawnQuest(); } /// <summary> /// 게임이 실행되면 QuestData를 토대로 맵에 퀘스트 생성 /// </summary> private void InitSpawnQuest() { questDictionary = new Dictionary<int, QuestBase>(); for (int i = 0; i < questData.questInfoList.Count; i++) { QuestInfo info = questData.questInfoList[i]; GameObject questObject = new GameObject($"Quest{i}"); TypeAddComponent(questObject, info); QuestBase quest = questObject.GetComponent<QuestBase>(); quest.questInfo = info; questDictionary[info.id] = quest; questObject.transform.SetParent(this.transform); questObject.transform.localPosition = info.position; questObject.transform.localScale = info.scale; questObject.AddComponent<BoxCollider>(); questObject.GetComponent<BoxCollider>().isTrigger = true; } } /// <summary> /// 타입에 따라 다른 컴포넌트를 추가함 /// </summary> /// <param name="questObject"></param> /// <param name="info"></param> private void TypeAddComponent(GameObject questObject, QuestInfo info) { switch (info.type) { case QuestType.PLAYER_MOVE: questObject.AddComponent<QuestMove>(); break; case QuestType.USE_ITEM: questObject.AddComponent<QuestUseItem>(); break; case QuestType.KILL_ENEMY: questObject.AddComponent<QuestKillEnemy>(); break; } } /// <summary> /// 퀘스트 시작 시 text를 UI에 출력 /// </summary> public void QuestStart(int id) { QuestBase quest = questDictionary[id]; quest.questState = QuestState.ONGOING; Debug.Log($"{questDictionary[id].questInfo.name}"); Debug.Log($"{questDictionary[id].questInfo.description}"); 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; } } }
이전과 달리 큰 변화가 있는 부분은 Awake 함수에서 InitSpawnQuest 함수를 호출하는 것이다. InitSpawnQuest 함수가 이제 게임이 시작될 때 QuestData를 토대로 Scene에 퀘스트를 생성하고 배치하는 함수이다. 작동방식은 QuestData에 저장되어 있는 모든 퀘스트 목록을 순회하며, 빈 오브젝트를 생성하고 해당 빈 오브젝트에 퀘스트 타입에 맞는 컴포넌트를 추가해주고, 위치와 크기를 세팅해준다음 마지막으로 collider를 붙혀주는 방식이다. 이렇게 되면 게임이 시작됐을 때 자동으로 퀘스트가 생성된다.
| 게임 시작 전 |
|---|
![]() |
| 게임 시작 후 |
|---|
![]() |