개발에서 중요한 것은 데이터를 어떻게 관리하고 유지할 것인가입니다.
엑셀로 사용하는 방법도 있지만,
데이터를 효율적으로 관리하고 업데이트하는 방법 중 하나는 Google 스프레드시트를 활용하는 것입니다.
이 블로그에서는 Unity Editor Window를 활용해 스프레드시트 데이터를 불러오고 JSON 파일로 저장하거나, 자동으로 C# 클래스를 생성하는 도구를 만드는 방법을 다룹니다.
목차
- 왜 Google 스프레드시트인가?
- Unity에서 Google 스프레드시트 데이터를 읽는 방법
- 스프레드시트 데이터 파싱 코드
- 코드 설명
1. 왜 Google 스프레드시트인가?
Google 스프레드시트는 무료로 제공되며, 클라우드 기반으로 데이터를 관리할 수 있는 매우 유용한 툴입니다. 여러 개발자나 기획자가 데이터를 동시에 관리할 수 있으며, 실시간으로 데이터를 업데이트할 수 있는 장점이 있습니다. 특히, 게임 개발에서 아이템 정보나 설정값을 관리하는데 적합합니다.
- 실시간 데이터 관리: 데이터를 실시간으로 변경하고 즉시 반영할 수 있습니다.
- 다중 사용자 협업: 여러 명이 동시에 데이터를 관리하고 수정할 수 있습니다.
- 손쉬운 접근성: 어디서나 브라우저로 접속하여 데이터를 확인하고 수정할 수 있습니다.
2. Unity에서 Google 스프레드시트 데이터를 읽는 방법
Unity와 Google 스프레드시트를 연동하려면 스프레드시트 데이터를 API로 받아와야 합니다.
Google 스프레드시트의 데이터를 읽기 위한 API를 만들고, Unity에서 활용하는 방식으로 구현할 수 있습니다.
2.1. Google 스프레드시트 API 설정하기
먼저 Google 스프레드시트의 데이터를 외부에서 접근할 수 있도록 API 설정이 필요합니다.
- Google 스프레드시트 공유 설정: 스프레드시트 파일에서 "공유" 버튼을 클릭하고, "링크를 가진 모든 사용자가 보기"로 설정합니다.
- Google Apps Script 생성: Google Apps Script를 통해 스프레드시트를 JSON 형태로 내보내기 위한 API를 생성합니다. 다음과 같은 코드를 사용해 간단한 Google Apps Script를 작성할 수 있습니다.
- Web 앱으로 배포: Google Apps Script의 "배포" 메뉴에서 "웹 앱으로 배포"를 선택하고 URL을 얻습니다. 이 URL을 통해 스프레드시트의 데이터를 JSON 형식으로 외부에서 접근할 수 있습니다.
function doGet() {
// Google 스프레드시트의 고유 ID: URL에서 /d/와 /edit 사이의 문자열을 넣습니다.
var spreadsheet = SpreadsheetApp.openById('** Google 스프레드시트의 고유 ID 문자열 정보 **');
var sheets = spreadsheet.getSheets();
var result = [];
for (var i = 0; i < sheets.length; i++) {
var sheet = sheets[i];
result.push({
"sheetName": sheet.getName(),
"sheetId": sheet.getSheetId()
});
}
// JSON 배열을 객체로 감싸서 반환
var output = {
"sheetData": result
};
return ContentService.createTextOutput(JSON.stringify(output)).setMimeType(ContentService.MimeType.JSON);
}
3. Unity 스프레드시트 데이터 파싱 코드 작성하기
Unity Editor에서 Google 스프레드시트 데이터를 불러오기 위해 Editor Window를 사용합니다.
Editor Window는 커스텀 툴을 제작할 수 있는 강력한 기능을 제공하며,
Unity 내에서 API를 통해 데이터를 가져오고 이를 JSON으로 저장하거나 C# 클래스로 변환할 수 있습니다.
코드 개요
- SheetParsing 클래스는 Unity Editor에서 스프레드시트 데이터를 관리하는 도구입니다. 데이터를 API로 불러와 선택한 시트를 JSON 파일로 저장하거나, 시트의 구조를 자동으로 C# 클래스로 변환합니다.
- 데이터를 가져오고 파싱하는 기능은 UnityWebRequest를 사용하며, 데이터를 비동기로 처리하기 위해 EditorCoroutine을 활용합니다
#if UNITY_EDITOR
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Unity.EditorCoroutines.Editor;
using Unity.Mathematics;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
public partial class SheetParsing : EditorWindow
{
string sheetAPIurl = "** 배포 받은 url 링크 **";
string sheeturl = "** 구글스프레드시트 링크 **";
private List<SheetData> sheets = new List<SheetData>();
private int selectedSheetIndex = 0;
private bool isFetching = false;
[MenuItem("Tools/Google Sheet Parsing Tool")]
public static void ShowWindow()
{
EditorWindow window = GetWindow(typeof(SheetParsing));
window.titleContent = new GUIContent("Google Sheet Parser");
window.maxSize = new Vector2(600, 400);
window.minSize = new Vector2(600, 400);
}
private void OnGUI()
{
GUILayout.Space(10);
if (isFetching)
{
EditorGUILayout.LabelField("Fetching data...");
}
else
{
if (sheets.Count > 0)
{
string[] sheetNames = sheets.Select(s => s.sheetName).ToArray();
selectedSheetIndex = EditorGUILayout.Popup("Select Sheet", selectedSheetIndex, sheetNames);
}
else
{
EditorGUILayout.LabelField("No sheets found.");
}
GUILayout.Space(20);
if (GUILayout.Button("Fetch Sheets Data", GUILayout.Height(40)))
{
EditorCoroutineUtility.StartCoroutine(FetchSheetsData(), this);
}
}
GUILayout.Space(30);
if (GUILayout.Button("Parse Selected Sheet and Create Class", GUILayout.Height(40)))
{
if (sheets.Count > 0)
{
ParseSelectedSheet();
}
else
{
EditorUtility.DisplayDialog("Error", "Please fetch sheet names and select a sheet.", "OK");
}
}
}
private IEnumerator FetchSheetsData()
{
isFetching = true;
UnityWebRequest request = UnityWebRequest.Get(sheetAPIurl);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
ProcessSheetsData(request.downloadHandler.text);
}
else
{
Debug.LogError("Error fetching data: " + request.error);
}
isFetching = false;
Repaint();
}
private void ProcessSheetsData(string json)
{
var sheetsData = JsonUtility.FromJson<SheetDataList>(json);
sheets.Clear();
sheets.AddRange(sheetsData.sheetData);
if (sheets.Count > 0)
{
selectedSheetIndex = 0;
}
}
private void ParseSelectedSheet()
{
var selectedSheet = sheets[selectedSheetIndex];
string jsonFileName = RemoveSpecialCharacters(selectedSheet.sheetName);
Debug.Log($"Selected Sheet: {selectedSheet.sheetName}, Sheet ID: {selectedSheet.sheetId}");
EditorCoroutineUtility.StartCoroutine(ParseGoogleSheet(jsonFileName, selectedSheet.sheetId.ToString()), this);
}
private string RemoveSpecialCharacters(string sheetName)
{
return Regex.Replace(sheetName, @"[^a-zA-Z0-9\s]", "").Replace(" ", "_");
}
private IEnumerator ParseGoogleSheet(string jsonFileName, string gid, bool notice = true)
{
string sheetUrl = $"{sheeturl}/export?format=tsv&gid={gid}";
UnityWebRequest request = UnityWebRequest.Get(sheetUrl);
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
EditorUtility.DisplayDialog("Fail", "GoogleConnect Fail!", "OK");
yield break;
}
string data = request.downloadHandler.text;
List<string> rows = ParseTSVData(data);
if (rows == null || rows.Count < 4)
{
Debug.LogError("Not enough data rows to parse.");
yield break;
}
HashSet<int> dbIgnoreColumns = GetDBIgnoreColumns(rows[0]);
var keys = rows[1].Split('\t').ToList();
var types = rows[2].Split('\t').ToList();
JArray jArray = new JArray();
for (int i = 3; i < rows.Count; i++)
{
var rowData = rows[i].Split('\t').ToList();
// 첫 열이 DB_IGNORE라면 행 제외
if (rowData[0].Equals("DB_IGNORE", StringComparison.OrdinalIgnoreCase))
{
Debug.Log($"Row {i + 1} ignored due to DB_IGNORE");
continue;
}
var rowObject = ParseRow(keys, types, rowData, dbIgnoreColumns);
if (rowObject != null)
{
jArray.Add(rowObject);
}
}
SaveJsonToFile(jsonFileName, jArray);
string className = CreateDataClass(jsonFileName, keys, types, dbIgnoreColumns); // C# 클래스 생성
if (notice)
{
EditorUtility.DisplayDialog("Success", "Sheet parsed and saved as JSON successfully!", "OK");
AssetDatabase.Refresh();
}
}
// TSV 데이터 파싱
private List<string> ParseTSVData(string data)
{
return data.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
// DB_IGNORE 열 필터링
private HashSet<int> GetDBIgnoreColumns(string headerRow)
{
var dbIgnoreColumns = new HashSet<int>();
var firstRow = headerRow.Split('\t').ToList();
for (int i = 0; i < firstRow.Count; i++)
{
if (firstRow[i].Equals("DB_IGNORE", StringComparison.OrdinalIgnoreCase))
{
dbIgnoreColumns.Add(i);
Debug.Log($"Column {i + 1} ignored due to DB_IGNORE");
}
}
return dbIgnoreColumns;
}
// 개별 행 파싱
private JObject ParseRow(List<string> keys, List<string> types, List<string> rowData, HashSet<int> dbIgnoreColumns)
{
var rowObject = new JObject();
for (int j = 0; j < keys.Count && j < rowData.Count; j++)
{
if (dbIgnoreColumns.Contains(j)) continue;
string key = keys[j];
string type = types[j];
string value = rowData[j].Trim();
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) continue;
rowObject[key] = ConvertValue(value, type);
}
return rowObject.HasValues ? rowObject : null;
}
// 값을 적절한 형식으로 변환하는 메서드
private JToken ConvertValue(string value, string type)
{
switch (type.Trim()) // 불필요한 공백 제거
{
case "int": return int.TryParse(value, out int intValue) ? intValue : 0;
case "long": return long.TryParse(value, out long longValue) ? longValue : 0L;
case "float": return float.TryParse(value, out float floatValue) ? floatValue : 0.0f;
case "double": return double.TryParse(value, out double doubleValue) ? doubleValue : 0.0d;
case "bool": return bool.TryParse(value, out bool boolValue) ? boolValue : false;
case "byte": return byte.TryParse(value, out byte byteValue) ? byteValue : (byte)0;
case "int[]": return JArray.FromObject(value.Split(',').Select(v => int.TryParse(v.Trim(), out int tempInt) ? tempInt : 0));
case "float[]": return JArray.FromObject(value.Split(',').Select(v => float.TryParse(v.Trim(), out float tempFloat) ? tempFloat : 0.0f));
case "string[]": return JArray.FromObject(value.Split(',').Select(v => v.Trim()));
case "DateTime": return DateTime.TryParse(value, out DateTime dateTimeValue) ? dateTimeValue : DateTime.MinValue; // DateTime 변환
case "TimeSpan": return TimeSpan.TryParse(value, out TimeSpan timeSpanValue) ? timeSpanValue : TimeSpan.Zero;
case "Guid": return Guid.TryParse(value, out Guid guidValue) ? guidValue.ToString() : Guid.Empty.ToString();
default: return value; // 기본적으로 문자열로 반환
}
}
// JSON 파일 저장 메서드
private void SaveJsonToFile(string jsonFileName, JArray jArray)
{
string directoryPath = Path.Combine(Application.dataPath, "Resources", "JsonFiles");
// 폴더가 존재하지 않으면 생성
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
string jsonFilePath = Path.Combine(directoryPath, $"{jsonFileName}.json");
File.WriteAllText(jsonFilePath, jArray.ToString());
Debug.Log($"Saved JSON to: {jsonFilePath}");
}
// C# 클래스 생성 메서드
private string CreateDataClass(string fileName, List<string> keys, List<string> types, HashSet<int> dbIgnoreColumns)
{
string className = fileName; // 파일 이름을 클래스 이름으로 사용
string directoryPath = Path.Combine(Application.dataPath, "Resources/DataClass");
// 폴더가 존재하지 않으면 생성
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
string dataClassPath = Path.Combine(directoryPath, $"{className}.cs");
using (StreamWriter writer = new StreamWriter(dataClassPath))
{
writer.WriteLine("using System.Collections.Generic;");
writer.WriteLine("[System.Serializable]");
writer.WriteLine($"public class {className}");
writer.WriteLine("{");
// 클래스 필드 생성
for (int i = 0; i < keys.Count; i++)
{
if (dbIgnoreColumns.Contains(i)) continue; // DB_IGNORE가 설정된 컬럼 건너뜀
string fieldType = ConvertTypeToCSharp(types[i]);
string fieldName = keys[i];
// 필드명이 비어있지 않은지 확인
if (!string.IsNullOrEmpty(fieldName))
{
writer.WriteLine($"\tpublic {fieldType} {fieldName};");
}
}
writer.WriteLine();
// Dictionary 생성
string keyType = ConvertTypeToCSharp(types[1]); // 첫 번째 컬럼을 Dictionary 키로 사용
writer.WriteLine($"\tpublic static Dictionary<{keyType}, {className}> tableDic = new Dictionary<{keyType}, {className}>();");
writer.WriteLine("}");
}
Debug.Log($"Saved C# class to: {dataClassPath}");
AssetDatabase.Refresh(); // 새로 생성된 클래스를 에디터에서 인식하도록 리프레시
return className; // 생성된 클래스 이름을 반환
}
private string ConvertTypeToCSharp(string type)
{
switch (type.Trim()) // 불필요한 공백 제거
{
case "int": return "int";
case "long": return "long";
case "float": return "float";
case "double": return "double";
case "bool": return "bool";
case "byte": return "byte";
case "int[]": return "int[]";
case "float[]": return "float[]";
case "string[]": return "string[]";
case "DateTime": return "System.DateTime"; // DateTime에 대한 올바른 반환값
case "TimeSpan": return "System.TimeSpan";
case "Guid": return "System.Guid";
default: return "string"; // 기본적으로 string으로 처리
}
}
}
// SheetData 클래스
[System.Serializable]
public class SheetData
{
public string sheetName;
public int sheetId;
}
// SheetDataList 클래스
[System.Serializable]
public class SheetDataList
{
public SheetData[] sheetData;
}
#endif
4. 코드 설명
주요 함수 설명
- FetchSheetsData: Google 스프레드시트의 시트 목록을 가져오는 함수로, UnityWebRequest를 사용해 데이터를 불러옵니다.
- ProcessSheetsData: 불러온 시트 데이터를 처리해 리스트로 관리합니다.
- ParseGoogleSheet: 선택한 시트를 TSV로 파싱하여 JSON으로 변환하고, 이를 저장하는 함수입니다.
- SaveJsonToFile: 파싱된 데이터를 JSON 파일로 저장합니다.
- CreateDataClass: 시트의 구조를 기반으로 자동으로 C# 클래스를 생성하여 파일로 저장합니다.
*위 내용 오타 및 수정해야 하는 내용 있으면 댓글로 알려주시면 감사합니다.