다음 과정을 하기 위해 먼저 Package Manager에서 Newtonsoft Json 을 설치해야 됩니다.
다음 에셋을 설치해주면 됩니다.
그 이후
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
using Newtonsoft.Json;
public class DataBaker : EditorWindow
{
// 데이터가 저장될 기본 경로 (폴더가 없으면 에러가 날 수 있으니 미리 생성해두는 것을 권장)
private const string JSON_PATH = "Assets/Resources/Data/";
private const string SO_PATH = "Assets/Resources/Data/";
[MenuItem("Data/Bake All JSON to SO (Dynamic)")]
public static void BakeAllData()
{
BakeNodesData();
// Localization은 여러 시트를 하나의 SO에 합치거나 각각 나눌 수 있습니다. 여기서는 분리하는 방식을 씁니다.
BakeLocalizationData("Localization_Dialogue");
BakeLocalizationData("Localization_UI");
BakeLocalizationData("Localization_Record");
BakeSelectNodesData();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("<color=green>모든 데이터 베이킹 완료!</color>");
}
// --- 1. Nodes 데이터 베이킹 ---
private static void BakeNodesData()
{
var response = LoadJson("Nodes");
if (response == null) return;
var headerMap = CreateHeaderMap(response.values[0]);
var database = GetOrCreateSO<NodeDatabaseSO>("NodeDatabase");
database.nodes.Clear();
for (int i = 1; i < response.values.Count; i++)
{
var row = response.values[i];
if (IsEmptyRow(row) || string.IsNullOrEmpty(GetValue(row, headerMap, "nodeId"))) continue;
NodeData newNode = new NodeData
{
nodeId = GetValue(row, headerMap, "nodeId"),
chapterId = GetValue(row, headerMap, "chapterId"),
sessionId = GetValue(row, headerMap, "sessionId"),
speakerId = GetValue(row, headerMap, "speakerId"),
nodeType = GetValue(row, headerMap, "nodeType"),
textKey = GetValue(row, headerMap, "textKey"),
nodeStartEvent = GetValue(row, headerMap, "nodeStartEvent"),
EventTiger = GetValue(row, headerMap, "EventTiger"),
autoNextNodeId = GetValue(row, headerMap, "autoNextNodeId"),
selectNodeId = GetValue(row, headerMap, "selectNodeId")
};
database.nodes.Add(newNode);
}
EditorUtility.SetDirty(database);
Debug.Log($"Nodes 베이킹 완료 ({database.nodes.Count}개)");
}
private static void BakeLocalizationData(string sheetName)
{
var response = LoadJson(sheetName);
if (response == null) return;
var headerMap = CreateHeaderMap(response.values[0]);
// 파일명을 시트명과 동일하게 생성 (예: Localization_Dialogue.asset)
var database = GetOrCreateSO<LocalizationDatabaseSO>(sheetName);
database.localizedTexts.Clear();
for (int i = 1; i < response.values.Count; i++)
{
var row = response.values[i];
if (IsEmptyRow(row) || string.IsNullOrEmpty(GetValue(row, headerMap, "textKey"))) continue;
LocalizeData newData = new LocalizeData
{
textKey = GetValue(row, headerMap, "textKey"),
ko = GetValue(row, headerMap, "ko"),
En = GetValue(row, headerMap, "En"),
Jp = GetValue(row, headerMap, "Jp"),
Note = GetValue(row, headerMap, "Note")
};
database.localizedTexts.Add(newData);
}
EditorUtility.SetDirty(database);
Debug.Log($"{sheetName} 베이킹 완료 ({database.localizedTexts.Count}개)");
}
private static void BakeSelectNodesData()
{
var response = LoadJson("SelectNodes");
// SelectNodes 파일이 아직 없다면 패스
if (response == null) return;
var headerMap = CreateHeaderMap(response.values[0]);
var database = GetOrCreateSO<SelectNodeDatabaseSO>("SelectNodeDatabase");
database.choices.Clear();
for (int i = 1; i < response.values.Count; i++)
{
var row = response.values[i];
if (IsEmptyRow(row) || string.IsNullOrEmpty(GetValue(row, headerMap, "selectGroupId"))) continue;
SelectNodeData newData = new SelectNodeData
{
selectGroupId = GetValue(row, headerMap, "selectGroupId"),
choiceId = GetValue(row, headerMap, "choiceId"),
textKey = GetValue(row, headerMap, "textKey"),
nextNodeId = GetValue(row, headerMap, "nextNodeId"),
condition = GetValue(row, headerMap, "condition")
};
database.choices.Add(newData);
}
EditorUtility.SetDirty(database);
Debug.Log($"SelectNodes 베이킹 완료 ({database.choices.Count}개)");
}
private static GoogleSheetResponse LoadJson(string fileName)
{
string fullPath = Path.Combine(Application.dataPath.Replace("Assets", ""), JSON_PATH, fileName + ".json");
if (!File.Exists(fullPath))
{
Debug.LogWarning($"[DataBaker] {fileName}.json 파일을 찾을 수 없습니다.");
return null;
}
string jsonText = File.ReadAllText(fullPath);
return JsonConvert.DeserializeObject<GoogleSheetResponse>(jsonText);
}
private static Dictionary<string, int> CreateHeaderMap(List<string> headers)
{
var map = new Dictionary<string, int>();
for (int i = 0; i < headers.Count; i++)
{
string headerName = headers[i].Trim();
if (!string.IsNullOrEmpty(headerName) && !map.ContainsKey(headerName))
map.Add(headerName, i);
}
return map;
}
private static string GetValue(List<string> row, Dictionary<string, int> headerMap, string headerName)
{
if (headerMap.TryGetValue(headerName, out int index) && index < row.Count)
return row[index];
return string.Empty;
}
private static T GetOrCreateSO<T>(string fileName) where T : ScriptableObject
{
string fullPath = SO_PATH + fileName + ".asset";
T asset = AssetDatabase.LoadAssetAtPath<T>(fullPath);
if (asset == null)
{
asset = ScriptableObject.CreateInstance<T>();
AssetDatabase.CreateAsset(asset, fullPath);
}
return asset;
}
private static bool IsEmptyRow(List<string> row)
{
return row == null || row.Count == 0;
}
}
다음 코드를 Editor폴더에 저장해줍니다
그리고 순차적으로 다음 스크립트들을 Assets/Scripts/Data/ 안에 생성해 둡니다.
using System.Collections.Generic;
[System.Serializable]
public class GoogleSheetResponse
{
public string range;
public string majorDimension;
// 실제 데이터가 담기는 2차원 리스트 [행][열]
public List<List<string>> values;
}
이 밑에는 실질적인 데이터를 어떻게 ScriptableObject에 집어 넣을 것 인지를 정합니다.
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class LocalizeData
{
public string textKey;
public string ko;
public string En;
public string Jp;
public string Note;
}
[CreateAssetMenu(fileName = "LocalizationDatabase", menuName = "Data/LocalizationDatabase")]
public class LocalizationDatabaseSO : ScriptableObject
{
public List<LocalizeData> localizedTexts = new List<LocalizeData>();
private Dictionary<string, LocalizeData> textDict;
public void Initialize()
{
textDict = new Dictionary<string, LocalizeData>();
foreach (var text in localizedTexts)
{
if (!string.IsNullOrEmpty(text.textKey) && !textDict.ContainsKey(text.textKey))
textDict.Add(text.textKey, text);
}
}
public LocalizeData GetTextData(string key) => (textDict != null && textDict.TryGetValue(key, out var data)) ? data : null;
}
*ScriptableObject화 코드는 시트 내용을 각 열에 맞게 채우는 과정이라 1가지만 첨부했습니다.
해당 코드를 작성하게 되면 유니티 에디터 상단의 Data -> Bake ALL Json to SO 버튼을 누르게 되면
다음과 같이 모든 데이터가 ScriptableObject로 만들어지게 됩니다.
이상으로 시트 데이터 api연결 -> Json 파일 변환 -> ScriptableObject화 해주는 (임시) 정규화 파이프라인 구축이 끝났습니다.
다음으로는 해당 데이터를 유니티 씬에 출력하는 로직을 작성해보겠습니다.
다음 부터는 코드는 그냥 요약만 해서 올리도록 하겠습니다.
VS Code에서 복사 붙여넣게 했더니 공백을 전부 글자수로 측정하네요;;
코드는 굳이 올릴필요없을듯
다음 일지부터 반영해보도록 할게용.