게임 개발 과정에서 데이터 관리와 업데이트는 매우 중요한 요소입니다. 특히 능력치나 게임 설정 데이터처럼 자주 변경되는 정보를 효율적으로 관리하기 위해 구글 스프레드시트를 활용하면 여러 면에서 장점을 얻을 수 있습니다.
이번 글에서는 구글 스프레드시트에서 데이터를 다운로드해 TSV 형식으로 받아 JSON으로 변환한 후 이를 ScriptableObject(SO)에 자동으로 적용하는 전체 프로세스를 구현한 코드를 설명합니다.
ScriptableObject(SO) 파일 스크립트
using UnityEngine;
[CreateAssetMenu(fileName = "NewMonstere", menuName = "Scriptable Object/Monster Data", order = int.MaxValue)]
public class MonsterDataSO : ScriptableObject
{
[SerializeField] private int monsterID;
[SerializeField] private string monsterName;
[SerializeField] private int monsterHp;
[SerializeField] private float monsterSpeed;
public int MonsterID => monsterID;
public string MonsterName => monsterName;
public int MonsterHp => monsterHp;
public float MonsterSpeed => monsterSpeed;
public void SetData(int id, string name, int hp, float speed)
{
this.monsterID = id;
this.monsterName = name;
this.monsterHp = hp;
this.monsterSpeed = speed;
}
}
인스펙터 버튼 생성 및 데이터 다운로드 메인 스크립트
using Newtonsoft.Json.Linq;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
[CustomEditor(typeof(SheetDataDownloader))]
public class SheetDownButton : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
SheetDataDownloader fnc = (SheetDataDownloader)target;
if (GUILayout.Button("Download SheetData"))
{
fnc.StartDownload(true);
}
}
}
public class SheetDataDownloader : MonoBehaviour
{
[SerializeField] private List<MonsterDataSO> monsterDataSO = new List<MonsterDataSO>();
string folderPath = "Assets/Resources/Data/ScriptableObjects/Monsters"; // 해당 파일 경로로 SO파일 처리
const string URL_MonsterDataSheet = "https://docs.google.com/spreadsheets/d/1Gl5MOQE_yY-DLmhh3kwKA8m78EUg/export?format=tsv&range=A1:D11";
private void Awake()
{
StartDownload(false);
}
private void Start()
{
Invoke("SetActiveDisable", 10f);
}
private void SetActiveDisable()
{
gameObject.SetActive(false);
}
public void StartDownload(bool renameFiles)
{
StartCoroutine(DownloadMonsterData(renameFiles));
}
/// <summary>
/// 구글 스프레드시트에서 능력치 데이터를 다운로드하여 ScriptableObject에 적용하는 함수
/// </summary>
/// <param name="renameFiles">false라면 so파일 이름변경 X</param>
/// <returns></returns>
IEnumerator DownloadMonsterData(bool renameFiles)
{
UnityWebRequest www = UnityWebRequest.Get(URL_MonsterDataSheet);
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
string tsvText = www.downloadHandler.text;
string json = ConvertTSVToJson(tsvText);
JArray jsonData = JArray.Parse(json); // JSON 문자열을 JArray로 변환
ApplyDataToSO(jsonData, renameFiles);
}
else
{
Debug.LogError("데이터 가져오기 실패: " + www.error);
}
}
/// <summary>
/// TSV 데이터를 JSON 형식으로 변환하는 함수
/// </summary>
/// <param name="tsv">TSV 형식의 문자열</param>
/// <returns>변환된 JSON 형식의 문자열</returns>
string ConvertTSVToJson(string tsv)
{
string[] lines = tsv.Split('\n'); // 줄 단위로 분리
if (lines.Length < 2) return "[]"; // 데이터가 없으면 빈 JSON 배열 반환
string[] headers = lines[0].Split('\t'); // 첫 번째 줄을 헤더로 사용
JArray jsonArray = new JArray();
for (int i = 1; i < lines.Length; i++)
{
string[] values = lines[i].Split('\t'); // 데이터 값 분리
JObject jsonObject = new JObject();
for (int j = 0; j < headers.Length && j < values.Length; j++)
{
string cleanValue = values[j].Trim();
jsonObject[headers[j].Trim()] = cleanValue;
}
jsonArray.Add(jsonObject);
}
return jsonArray.ToString();
}
/// <summary>
/// 다운로드한 데이터를 ScriptableObject(SO)에 적용하는 함수.
/// 기존 SO 데이터를 보관하는 폴더의 모든 ScriptableObject를 삭제한 후 새로운 데이터를 생성하여 적용한다.
/// </summary>
/// <param name="jsonData">스프레드시트에서 받은 JSON 데이터</param>
/// <param name="renameFiles">true면 SO 파일명을 JSON 데이터의 name 값으로 변경</param>
private void ApplyDataToSO(JArray jsonData, bool renameFiles)
{
ClearAllMonsterDataSO();
monsterDataSO.Clear();
for (int i = 0; i < jsonData.Count; i++)
{
JObject row = (JObject)jsonData[i];
int monsterID = 0;
int.TryParse(row["ID"]?.ToString(), out monsterID);
string monsterName = row["Name"]?.ToString() ?? "";
int monsterHp = 0;
int.TryParse(row["Hp"]?.ToString(), out monsterHp);
float monsterSpeed = 0f;
float.TryParse(row["Speed"]?.ToString(), out monsterSpeed);
MonsterDataSO monsterData = new MonsterDataSO();
// 기존 SO 개수가 부족하면 새로 생성
if (i < monsterDataSO.Count)
{
monsterData = monsterDataSO[i];
}
else
{
monsterData = CreateNewMonsterDataSO(monsterName); // 새로운 SO 생성
monsterDataSO.Add(monsterData);
}
// 파일명 변경
if (renameFiles)
{
RenameScriptableObjectFile(monsterData, monsterName);
}
monsterData.SetData(monsterID, monsterName, monsterHp, monsterSpeed);
EditorUtility.SetDirty(monsterData); // 변경 사항 강제 적용, 반드시 하나했을때 개별적으로 적용시키기.
Debug.Log($"{monsterData.name} 업데이트 완료");
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
/// <summary>
/// SO의 파일명을 변경하는 함수
/// </summary>
private void RenameScriptableObjectFile(MonsterDataSO so, string newFileName)
{
#if UNITY_EDITOR
string path = AssetDatabase.GetAssetPath(so);
string newPath = Path.GetDirectoryName(path) + "/" + newFileName + ".asset";
if (path != newPath)
{
AssetDatabase.RenameAsset(path, newFileName);
Debug.Log($"파일명 변경: {path} => {newPath}");
// 즉시 저장하여 반영
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
#endif
}
/// <summary>
/// 지정된 폴더 내의 모든 ScriptableObject(SO) 파일을 삭제하는 함수.
/// </summary>
private void ClearAllMonsterDataSO()
{
if (!Directory.Exists(folderPath))
{
Debug.LogWarning("SO 폴더가 존재하지 않음");
return;
}
string[] files = Directory.GetFiles(folderPath, "*.asset");
foreach (string file in files)
{
AssetDatabase.DeleteAsset(file);
Debug.Log($"삭제됨: {file}");
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
/// <summary>
/// 새로운 AbilityDataSO ScriptableObject를 생성하고 지정된 폴더에 저장하는 함수.
/// </summary>
/// <param name="fileName">생성할 SO 파일의 이름</param>
/// <returns>생성된 DataSO 객체</returns>
private MonsterDataSO CreateNewMonsterDataSO(string fileName)
{
MonsterDataSO newSO = ScriptableObject.CreateInstance<MonsterDataSO>();
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
string assetPath = $"{folderPath}/{fileName}.asset";
AssetDatabase.CreateAsset(newSO, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"새로운 ScriptableObject 생성: {assetPath}");
return newSO;
}
}
1. 해당 코드의 목적 및 기능 설명
이 코드는 Unity 프로젝트 내에서 구글 스프레드시트에 기록된 능력치 데이터를 자동으로 다운로드하고 이를 JSON으로 변환한 후 ScriptableObject(SO) 형태로 저장 및 갱신하는 기능을 수행합니다.
주요 기능은 다음과 같습니다.
- 데이터 다운로드: UnityWebRequest를 사용해 지정된 URL에서 TSV 데이터를 받아옵니다.
- TSV → JSON 변환: TSV 형식의 데이터를 파싱하여 JSON 배열로 변환합니다.
- ScriptableObject 갱신: 기존의 SO 파일들을 삭제하고 JSON 데이터를 기반으로 새로 생성하여 데이터를 적용합니다.
- 파일명 변경 및 저장: 옵션에 따라 새로 생성되거나 기존의 ScriptableObject 파일명을 JSON 데이터의 이름으로 변경하고 변경 사항을 저장합니다.
- Custom Editor 제공: Inspector에 “Download SheetData” 버튼을 추가해 손쉽게 데이터를 다운로드 및 적용할 수 있도록 합니다.
2. 구글 스프레드시트를 사용하여 데이터를 관리하는 이유
구글 스프레드시트는 여러 팀원이 동시에 데이터를 편집할 수 있고 실시간으로 변경 사항이 반영되기 때문에 다음과 같은 장점이 있습니다.
- 실시간 업데이트: 게임 개발 도중 능력치나 설정 값이 변경되면 스프레드시트의 데이터를 즉시 수정할 수 있으며, 최신 정보를 바로 게임에 반영할 수 있습니다.
- 협업 용이성: 기획자, 디자이너, 개발자 모두가 동일한 스프레드시트를 참조할 수 있어 데이터의 일관성을 유지할 수 있습니다.
3. 왜 CSV가 아닌 TSV를 사용하는가?
데이터를 구글 스프레드시트에서 내보낼 때 CSV 대신 TSV 형식을 선택하는 이유는 다음과 같습니다.
- 구분 기호의 일관성: CSV는 쉼표(,)를 구분 기호로 사용하지만, 데이터에 쉼표나 소수점이 포함될 경우 파싱이 어려워질 수 있습니다. 반면, TSV는 탭(\t)을 사용하여 구분하므로 이러한 문제를 최소화합니다.
- 간단한 파싱: TSV는 단순히 탭으로 각 열을 분리하므로, 복잡한 인코딩이나 예외 처리가 필요하지 않아 안정적으로 데이터를 처리할 수 있습니다.
4. 왜 TSV 데이터를 JSON으로 변환하는가?
구글 스프레드시트에서 받은 TSV 데이터를 바로 사용하는 대신 JSON으로 변환하는 이유는 다음과 같습니다.
- 구조화된 데이터 관리: JSON은 키-값 쌍 구조로 데이터를 표현하므로 TSV의 헤더와 각 행의 데이터를 자연스럽게 매핑할 수 있습니다.
- 강력한 파싱 및 직렬화 도구 활용: Newtonsoft.Json과 같은 라이브러리를 사용하여 JSON 데이터를 쉽게 직렬화/역직렬화할 수 있으므로 데이터 처리와 관리가 훨씬 수월해집니다.
- 확장성: JSON은 중첩된 객체나 배열을 지원하므로 데이터 형식이 복잡해지더라도 쉽게 확장할 수 있습니다.
5. 코드 설명
1) Custom Editor: SheetDownButton
- 역할: Inspector 창에 “Download SheetData” 버튼을 추가하여, 버튼 클릭 시 SheetDataDownloader의 StartDownload(true)를 호출합니다.
- 장점: 에디터 내에서 손쉽게 데이터를 갱신할 수 있어 개발 생산성이 높아집니다.
2) SheetDataDownloader
- 데이터 다운로드: UnityWebRequest를 통해 구글 스프레드시트 URL에서 TSV 데이터를 받아옵니다.
- TSV → JSON 변환: ConvertTSVToJson 함수는 TSV 데이터를 줄 단위로 분리한 후, 첫 번째 줄을 헤더로 사용하여 JSON 배열로 변환합니다.
- ScriptableObject 갱신: ApplyDataToSO 함수는 기존의 MonsterDataSO파일들을 삭제하고, 새로 생성한 ScriptableObject에 JSON 데이터를 적용합니다.
- 파일명 변경: 옵션에 따라, 새로 생성된 SO의 파일명을 JSON 데이터의 name 값으로 변경합니다.
- 데이터 적용 완료 후 저장: AssetDatabase를 통해 변경 사항을 저장하고 에셋을 리프레시합니다.
3) MonsterDataSO
- 역할: 능력치 데이터를 저장하는 ScriptableObject입니다.
- 주요 필드: monsterID, monsterName, monsterHp, monsterSpeed
- SetData 메서드: JSON에서 읽어온 데이터를 이 SO에 적용하는 역할을 합니다.
6. 사용 방법
1) 구글 스프레드 시트 설정 방법
우선 스프레드 시트의 우측 상단에 있는 공유 버튼을 눌러 액세스를 링크가 있는 사용자에게 공유해야합니다.
2) 주소 설정 방법
코드에 URL_MonsterDataSheet 변수를 보면 구글 스프레드 시트 주소가 들어가있습니다. 그리고 뒷쪽을 자세히 보면 주소뿐만 아니라 파일 형식과 가져올 데이터 범위까지 설정되어 있습니다.
const string URL_MonsterDataSheet = "https://docs.google.com/spreadsheets/d/1Gl5MOQE_yY-DLmhh3kwKA8m78EUg6sFDk9k8NIrp0vY/export?format=tsv&range=A1:D11";
설정 방법은 다음과 같습니다
1 - 주소 가져오기
- 구글 스프레드시트를 열고, URL에서 /edit? 이전까지의 부분을 복사합니다.
- 이 부분이 스프레드시트의 기본 주소가 됩니다.
2 - 파일 형식 지정
- URL의 파라미터 중 format=tsv 부분에서, TSV 대신 CSV로 받고 싶다면 format=csv로 수정하면 됩니다.
- 이를 통해 원하는 파일 형식에 맞춰 데이터를 가져올 수 있습니다.
3 - 데이터 범위 설정
- 스프레드시트에서 데이터를 드래그하면 좌측 상단에 범위(예: A1:D11)가 표시됩니다.
- 이 값을 URL의 range= 뒤에 추가하여, 특정 영역의 데이터만 가져오도록 설정할 수 있습니다.
3) 파일 경로 지정
SO파일을 관리할 폴더를 지정합니다.
해당 경로에 SO파일이 생성, 편집, 관리가 이뤄집니다.
string folderPath = "Assets/Resources/Data/ScriptableObjects/Monsters"; // 해당 파일 경로로 SO파일 처리
4) 작동 방법
스크립트를 컴포넌트로 추가하면 인스펙터 창에 버튼이 함께 표시됩니다. 이 버튼은 Custom Editor를 통해 만들어졌으며 버튼을 클릭하면 스크립트에 설정된 기능이 실행됩니다.
[CustomEditor(typeof(SheetDataDownloader))]
public class SheetDownButton : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
SheetDataDownloader fnc = (SheetDataDownloader)target;
if (GUILayout.Button("Download SheetData"))
{
fnc.StartDownload(true);
}
}
}
결론
이 프로젝트를 통해 구글 스프레드시트의 데이터를 Unity로 자동으로 다운로드하고 TSV 데이터를 JSON으로 변환하여 ScriptableObject(SO)에 적용하는 방법을 구현했습니다.
이 방식을 활용하면 실시간 업데이트와 팀 협업에 유리한 데이터 관리 시스템을 구축할 수 있습니다.
'Unity > 코드 연구' 카테고리의 다른 글
[Unity] 사운드 매니저 구현 (Sound Manager) (0) | 2025.01.13 |
---|