내일배움캠프 60일차 TIL
오늘 해낸 일
드디어 우리 게임의 아이템 레시피 데이터를 ScriptableObject(SO)에 연결하는 작업을 마쳤다. RecipeData.csv 파일에서 레시피 정보를 읽어와 SO에 연결했고, 심지어 SO를 인스펙터에서 수정하면 다시 CSV 파일에 반영되도록 양방향 동기화 기능까지 구현했다!
💡 이 작업을 왜 했냐면? (배경)
지난번에 아이템 데이터(ItemSO)를 CSV로 자동 생성하고 관리하는 툴을 만들었었다. 이제 아이템들이 생겼으니, 이 아이템들을 가지고 새로운 아이템을 만드는 '레시피' 정보도 관리해야 했다.
레시피는 단순히 텍스트 데이터가 아니라, '어떤 아이템'이 '얼마나' 필요한지 정확히 알아야 했기 때문에, ItemSO끼리 서로를 참조하는 구조가 필요했다. 엑셀로 레시피를 관리하면 좋겠고, Unity에서 SO로 편하게 쓰고 싶었으니, 이 둘을 연결하는 자동화 시스템이 필요했다.
🔧 어떻게 만들었는지 기록!
핵심은 ItemSO 안에 다른 ItemSO를 재료로 참조할 수 있는 구조를 만들고, CSV 데이터를 이 구조에 맞게 변환해서 넣어주는 것이었다.
1️⃣ ItemSO.cs에 레시피 필드 추가
먼저 ItemSO가 자신을 만들기 위한 재료들을 가질 수 있도록 확장했다.
// 레시피 재료 하나를 표현하는 클래스
[System.Serializable] // 인스펙터에서 보이게 하려고 붙였다.
public class RecipeIngredient
{
public ItemSO item; // 어떤 아이템이 재료인지 SO 자체를 참조
public int amount; // 그 아이템이 몇 개 필요한지
}
// 기존 ItemSO 클래스에 레시피 정보를 담을 필드를 추가했다.
[CreateAssetMenu(fileName = "New Item", menuName = "Item/Item")]
public class ItemSO : ScriptableObject
{
// ... 기존 아이템 정보 필드들 ...
public List<RecipeIngredient> recipe; // 이 아이템을 만들기 위한 재료 리스트
}
ItemSO 안에 List<RecipeIngredient>를 넣고, RecipeIngredient가 다시 ItemSO를 참조하는 구조를 만들었다. 이렇게 하면 A라는 아이템이 B, C 아이템을 재료로 가진다는 것을 SO 내부에서 직접 연결할 수 있다.
2️⃣ RecipeData.csv 포맷 구성
레시피 데이터는 엑셀에서 이렇게 작성하기로 했다.
resultItemName,ingredientName,amount
Wooden Sword,Wood,5
Wooden Sword,Slime,2
Campfire,Stone,3
하나의 아이템(resultItemName)이 여러 개의 재료를 가질 수 있기 때문에, 하나의 재료마다 한 줄(row)로 정의하는 방식을 선택했다. 이렇게 하면 엑셀에서 관리하기가 가장 직관적이었다.
3️⃣ ItemSOGenerator.cs에서 레시피 읽어와 SO에 반영
레시피 데이터를 CSV에서 읽어와 SO에 연결하는 로직은 조금 복잡했다.
먼저, CSV 파일을 한 번 읽어서 recipeMap이라는 딕셔너리에 임시로 저장했다. recipeMap은 결과 아이템 이름을 키로 하고, 그 아이템에 필요한 재료 이름과 개수 리스트를 값으로 가진다.
void LoadRecipeData()
{
// 딕셔너리를 초기화했다.
recipeMap = new Dictionary<string, List<(string name, int amount)>>();
// RecipeData.csv 파일을 읽어서 줄 단위로 나눴다.
string[] lines = recipeCsvFile.text.Split('\n');
// 헤더(첫 줄)는 건너뛰고 두 번째 줄부터 읽었다.
for (int i = 1; i < lines.Length; i++)
{
var cols = lines[i].Trim().Split(','); // 각 줄을 쉼표로 나눴다.
string resultItem = cols[0].Trim(); // 결과 아이템 이름
string ingredientItem = cols[1].Trim(); // 재료 아이템 이름
int.TryParse(cols[2], out int amount); // 재료 개수
// 결과 아이템이 딕셔너리에 없으면 새로 리스트를 만들고 추가했다.
if (!recipeMap.ContainsKey(resultItem))
recipeMap[resultItem] = new List<(string, int)>();
recipeMap[resultItem].Add((ingredientItem, amount)); // 재료 정보를 추가했다.
}
}
그리고 ItemSO를 생성할 때, 해당 ItemSO의 itemName을 키로 recipeMap에서 레시피 정보를 찾아왔다.
// 현재 SO의 이름(asset.itemName)으로 레시피가 있는지 찾아봤다.
if (recipeMap.TryGetValue(asset.itemName, out var recipes))
{
asset.recipe = new List<RecipeIngredient>(); // 레시피 리스트를 새로 만들었다.
foreach (var (ingredientName, amount) in recipes) // 각 재료에 대해
{
// 재료 아이템의 이름(ingredientName)으로 AssetDatabase에서 ItemSO를 검색했다.
// ItemSO는 모두 "Assets/08_ScriptableObjects/Items" 폴더에 있다고 가정했다.
string[] guids = AssetDatabase.FindAssets($"{ingredientName} t:ItemSO", new[] { "Assets/08_ScriptableObjects/Items" });
if (guids.Length == 0) continue; // 못 찾으면 다음 재료로 넘어갔다.
string path = AssetDatabase.GUIDToAssetPath(guids[0]); // 찾은 SO의 경로를 가져왔다.
ItemSO ingredientSO = AssetDatabase.LoadAssetAtPath<ItemSO>(path); // 경로로 SO를 로드했다.
// 로드한 SO와 개수를 RecipeIngredient 객체로 만들어 리스트에 추가했다.
asset.recipe.Add(new RecipeIngredient
{
item = ingredientSO,
amount = amount
});
}
}
여기서 가장 중요한 부분은 AssetDatabase.FindAssets와 AssetDatabase.LoadAssetAtPath를 사용해서 재료 아이템의 '이름'만 가지고 실제 SO 에셋을 찾아서 참조로 연결했다는 점이다. CSV에는 이름만 있으니, 이 과정이 꼭 필요했다.
4️⃣ 인스펙터에서 SO 수정 시 RecipeData.csv에도 반영되도록 구현
레시피 정보는 여러 줄로 되어있기 때문에, CSV에 역으로 반영하는 것이 가장 까다로웠다.
public static void UpdateCSV(ItemSO item)
{
// ... 기존 아이템 데이터 업데이트 로직 ...
// 레시피 정보도 CSV에 반영해야 한다.
if (item.recipe != null)
{
// 기존 RecipeData.csv에서 해당 아이템의 레시피 라인을 찾아서 수정하거나,
// 해당 아이템의 모든 레시피 라인을 삭제한 후, 현재 SO의 레시피 정보를
// 새로운 라인으로 다시 추가하는 전략을 사용할 수 있다.
// 나는 후자의 방식(전체 삭제 후 다시 추가)이 구현하기 더 깔끔하다고 생각했다.
}
}
이 부분은 아직 완벽하게 구현하지는 못했지만, 기존 CSV에서 특정 아이템의 레시피 라인들을 모두 삭제하고, SO에 있는 최신 레시피 정보를 새로운 라인으로 다시 추가하는 방식이 가장 안전하고 구현하기 쉬울 것이라고 판단했다. 이름만으로 SO를 찾아 연결하는 것과 반대로, SO에서 이름과 개수를 다시 CSV 형식으로 변환해서 써야 했다.
💡 구현 중 느낀 점
- SO ↔ CSV 양방향 연동은 자동화 툴을 만들 때 정말 유용한 경험이었다. 한쪽만 만들 때는 몰랐던 고려사항들이 많았다.
- ItemSO가 ItemSO를 참조하는 구조(ItemSO → RecipeIngredient → 다른 ItemSO)는 Editor 환경이 아니면 순환 참조나 Resources.Load 같은 방식으로 접근해야 해서 복잡해질 수 있다는 것을 알게 되었다. AssetDatabase 덕분에 에디터에서는 편하게 작업할 수 있었다.
- CSV에서 row 기반으로 다대다 관계(하나의 결과 아이템이 여러 재료를 가짐)를 표현하는 방식이 가장 직관적이고 관리하기 편하다는 것을 확인했다.
🧠 기억 포인트
| 포인트 | 설명 |
| 레시피 구조 | SO 내부에서 다른 SO를 참조 (ItemSO 안에 RecipeIngredient → 또 다른 ItemSO 참조) |
| CSV 포맷 | 다중 재료 관계는 row 단위로 재료 하나씩 정의 |
| SO 찾기 | AssetDatabase.FindAssets, AssetDatabase.LoadAssetAtPath로 이름 기반 검색 및 로드 |
| 동기화 방식 | 인스펙터에서 SO 수정 시, ItemSOToCSVUpdater.UpdateCSV(item)를 호출하여 CSV 업데이트 |
| 주의점 | CSV 파일 순서 보존, null 처리, 아이템 이름 중복 문제 등 고려 필요 |
최종 결과
- ItemSO에 레시피 정보가 성공적으로 연결되었다.
- RecipeData.csv를 기준으로 레시피 SO가 자동으로 생성된다.
- 인스펙터에서 레시피를 수정하면 RecipeData.csv에도 자동으로 반영된다.
- 이제 툴과 데이터가 완전히 연결된 아이템 제작 시스템의 기반이 마련되었다! 뿌듯하다!

'TIL' 카테고리의 다른 글
| 셀룰러 오토마타로 육각형 지형 만드는 시스템 구현 (0) | 2025.07.07 |
|---|---|
| 육각형 맵 Flat-Top 전환 & 6방향 랜덤 지형 배치 (0) | 2025.07.04 |
| CSV 기반 아이템 SO 수정 시 CSV 자동 업데이트 기능 구현 (0) | 2025.07.02 |
| CSV 기반 ScriptableObject 자동 생성 시스템 구축 (0) | 2025.07.01 |
| Pointy-Top 육각형 외형 맵 생성 (with Unity Tilemap + 사각형 타일) (2) | 2025.06.30 |