Unity 에디터에서 개발할 때, 자동화 같은 개발 유틸적인 부분에서 강력한 도구인
Unity 에디터 전용 Api Assetdatabase 에 대해서 정리한다.
그리고 Unity 에디터 전용 유틸리티 클래스인 EditorUtility에 대해서 정리하고
이를 활용한 CSVToSO 자동화 스크립트를 소개한다.
UnityEditor.AssetDatabase는 Unity 에디터 전용 API로, 프로젝트의 Asset들을 관리하고 조작하는 데 사용되는 도구이다.
런타임에서 사용할 수 없고, 에디터 환경에서만 동작한다.
이를 통해 스크립트로 .asset, .prefab, .mat, .fbx, .csv 등 Unity 프로젝트 내의 자산 파일들을 자동으로 생성, 수정, 복사, 삭제, 이동할 수 있다.
주로 사용되는 용도는 다음과 같다.
| 메서드명 | 설명 | 예시 |
|---|---|---|
LoadAssetAtPath<T>(string path) | 경로를 기반으로 특정 타입의 에셋 로드 | AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Textures/Tex.png") |
CreateAsset(Object asset, string path) | 새로운 에셋 생성 및 저장 | AssetDatabase.CreateAsset(mySO, "Assets/Data/MySO.asset") |
DeleteAsset(string path) | 해당 경로의 에셋 삭제 | AssetDatabase.DeleteAsset("Assets/Old.asset") |
CopyAsset(string from, string to) | 에셋 복사 | AssetDatabase.CopyAsset("Assets/Source.prefab", "Assets/Copy.prefab") |
RenameAsset(string path, string newName) | 에셋 이름 변경 | AssetDatabase.RenameAsset("Assets/My.asset", "Renamed") |
SaveAssets() | 메모리 상의 변경 내용을 디스크에 저장 | AssetDatabase.SaveAssets() |
Refresh() | 에셋 데이터 갱신 (파일 변경 감지) | AssetDatabase.Refresh() |
FindAssets(string filter, string[] folders = null) | 필터 조건에 맞는 에셋을 GUID로 반환 | AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Prefabs" }) |
GUIDToAssetPath(string guid) | GUID → 경로 변환 | AssetDatabase.GUIDToAssetPath(guid) |
AssetPathToGUID(string path) | 경로 → GUID 변환 | AssetDatabase.AssetPathToGUID("Assets/My.asset") |
IsValidFolder(string path) | 폴더 유효성 확인 | AssetDatabase.IsValidFolder("Assets/Resources") |
ImportAsset(string path) | 에셋 강제 임포트 | AssetDatabase.ImportAsset("Assets/Data/Updated.csv") |
MoveAsset(string oldPath, string newPath) | 에셋 이동 | AssetDatabase.MoveAsset("Assets/Old.asset", "Assets/New.asset") |
GetAssetPath(Object obj) | 오브젝트에서 경로 얻기 | AssetDatabase.GetAssetPath(mySO) |
Unity 에디터 전용 유틸리티 클래스로 에디터에서 UI, 오브젝트 상태 조작, 확인창, 경고창, 프로그래스 바 등을 다룰 때 사용한다.
런타임에서는 사용 불가하며, UnityEditor 네임스페이스에 속해 있다.
주로 사용되는 용도는 다음과 같다.
| 메서드명 | 설명 | 예시 |
|---|---|---|
SetDirty(Object target) | 오브젝트가 수정되었음을 에디터에 알림 (저장 대상이 됨) | EditorUtility.SetDirty(mySO) |
DisplayDialog(string title, string message, string ok) | 확인 팝업창 | EditorUtility.DisplayDialog("알림", "작업이 완료되었습니다.", "확인") |
DisplayDialog(string title, string message, string ok, string cancel) | 확인/취소 팝업 | EditorUtility.DisplayDialog("저장", "변경사항을 저장할까요?", "예", "아니오") |
DisplayProgressBar(string title, string info, float progress) | 진행 표시 바 UI 표시 | EditorUtility.DisplayProgressBar("로딩 중", "에셋 처리 중...", 0.5f) |
ClearProgressBar() | 진행 바 종료 | EditorUtility.ClearProgressBar() |
OpenFilePanel(string title, string directory, string extension) | 파일 열기 다이얼로그 | EditorUtility.OpenFilePanel("CSV 선택", "", "csv") |
SaveFilePanel(string title, string directory, string defaultName, string extension) | 파일 저장 위치 선택 다이얼로그 | EditorUtility.SaveFilePanel("저장 위치", "", "NewFile", "txt") |
OpenFolderPanel(string title, string folder, string defaultName) | 폴더 선택 창 | EditorUtility.OpenFolderPanel("폴더 선택", "", "") |
DisplayCancelableProgressBar(string title, string info, float progress) | 취소 가능한 프로그레스 바 | EditorUtility.DisplayCancelableProgressBar("진행 중", "작업 중...", 0.3f) |
IsPersistent(Object obj) | 오브젝트가 프로젝트 에셋인지 확인 | EditorUtility.IsPersistent(mySO) |
InstanceIDToObject(int id) | 인스턴스 ID → Object 변환 | Object obj = EditorUtility.InstanceIDToObject(id) |
위 개념을 가지고 CSV로 만든 데이터를 단순한 클래스 객체가 아닌, ScriptableObject로 만들어야 하는 경우가 있다.
대표적으로 CSV에는 이미지 파일을 가지고 있을 수 없는데, 이 경우 SO로 변환하여 이미지 스프라이트 타입의 데이터를 가지고 있게 할 수 있다.
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
public static class CsvToSOGenerator
{
private const string SO1_PATH = "Assets/Resources/SO1/";
private const string SO2_PATH = "Assets/Resources/SO2/";
static readonly string[] SPRITE_SEARCH_ROOTS = { "이미지파일경로명" };
static Dictionary<string, Sprite> _spriteCache;
[MenuItem("Tools/Import CSV → SO")]
public static void ImportAll()
{
//스트라이트 캐시 데이터를 먼저 준비한다.
BuildSpriteCache();
var quests = CsvReader.Load("CSV파일명1", Data1.FromCsv);
foreach (var q in quests)
{
var asset = GetOrCreate<SO1>(QUEST_PATH, q.Id);
//만들어둔 데이터 클래스와 SO데이터 필드를 동기화
asset.Id = q.Id;
asset.Name = q.Name;
asset.Description = q.Description;
asset.Objective = q.Objective;
asset.ClearScene = q.ClearScene;
asset.Condition1 = q.Condition1;
asset.ObjectID1 = q.ObjectID1;
asset.Count1 = q.Count1;
asset.Condition2 = q.Condition2;
asset.ObjectID2 = q.ObjectID2;
asset.Count2 = q.Count2;
asset.RewardID = q.RewardID;
if(!string.IsNullOrEmpty(q.ImageName)&& _spriteCache.TryGetValue(q.ImageName, out var spriteQ))
{
asset.Image = spriteQ;
}
else
{
asset.Image = null;
}
EditorUtility.SetDirty(asset);
}
var stories = CsvReader.Load("CSV파일명2", Data2.FromCsv);
foreach (var s in stories)
{
var asset = GetOrCreate<SO2>(STORY_PATH, s.Id);
//만들어둔 데이터 클래스와 SO데이터 필드를 동기화
asset.Id = s.Id;
asset.Stage = s.Stage;
asset.Title = s.Title;
asset.Description = s.Description;
asset.QuestId1 = s.QuestID1;
asset.QuestId2 = s.QuestID2;
asset.Stack = s.Stack;
if (!string.IsNullOrEmpty(s.ImageName) && _spriteCache.TryGetValue(s.ImageName, out var spriteS))
{
asset.Image = spriteS;
}
else
{
asset.Image = null;
}
EditorUtility.SetDirty(asset);
}
AssetDatabase.SaveAssets();
Debug.Log("CSV → SO 변환 완료");
}
private static T GetOrCreate<T>(string dir, int id) where T : ScriptableObject
{
Directory.CreateDirectory(dir);
var path = $"{dir}{id}.asset";
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
if (asset == null)
{
asset = ScriptableObject.CreateInstance<T>();
AssetDatabase.CreateAsset(asset, path);
}
return asset;
}
//스트라이트 이미지 캐시 빌더
static void BuildSpriteCache()
{
_spriteCache = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase);
var files = AssetDatabase.FindAssets("t:Sprite", SPRITE_SEARCH_ROOTS);
foreach (var file in files)
{
var path = AssetDatabase.GUIDToAssetPath(file);
foreach (var obj in AssetDatabase.LoadAllAssetsAtPath(path))
{
if (obj is Sprite sp)
{
if (_spriteCache.ContainsKey(sp.name))
{
Debug.LogWarning($"[SpriteCache] 중복 이름 발견: {sp.name} / {path}");
}
else
{
_spriteCache.Add(sp.name, sp);
}
}
}
}
}
}
#endif
//------------------------------------
//SO데이터
[CreateAssetMenu(menuName = "DB/StorySO")]
public class SO1 : ScriptableObject
{
[Header("[Id]")]
public int Id;
[Header("[Stage]")]
public int Stage;
[Header("Text")]
[TextArea] public string Title;
[TextArea(3, 7)] public string Description;
[Header("[퀘스트1 & 2]")]
public int QuestId1;
public int QuestId2;
[Header("[진행도 스택]")]
public int Stack;
[Header("[이미지]")]
public Sprite Image;
}
//------------------------------------------
//원본 데이터 모델 클래스
public class StoryData
{
public int Id { get; private set; }
public int Stage { get; private set; }
public string Title { get; private set; }
public string Description { get; private set; }
public int QuestID1 { get; private set; }
public int QuestID2 { get; private set; }
public int Stack { get; private set; }
public string ImageName { get; private set; }
public static StoryData FromCsv(string[] c) => new StoryData
{
Id = CsvReader.ParseId(c[0]),
Stage = CsvReader.ParseSafe<int>(c[1]),
Title = StringTable.Get(c[2]),
Description = StringTable.Get(c[3]),
QuestID1 = CsvReader.ParseId(c[4]),
QuestID2 = CsvReader.ParseId(c[5]),
Stack = CsvReader.ParseSafe<int>(c[6]),
ImageName = CsvReader.ParseSafe<string>(c[7])
};
}
//----------------------------------------------
//CSVReader
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// CSV 파싱 헬퍼 클래스
/// </summary>
public static class CsvReader
{
private const string CSV_ROOT = "CSV"; // Resources/CSV 이하
// TextAsset → IEnumerable<string[]>
private static IEnumerable<string[]> ParseLines(TextAsset ta)
{
if (ta == null)
throw new System.IO.FileNotFoundException(
$"CSV asset not found in Resources/CSV");
var lines = ta.text.Split('\n');
// 0: 헤더, 1: 타입 선언 → 2번부터 실데이터
for (int i = 2; i < lines.Length; ++i)
{
var raw = lines[i].Trim();
if (string.IsNullOrEmpty(raw)) continue;
var cols = raw.Split(',');
for (int c = 0; c < cols.Length; ++c)
{
cols[c] = cols[c].Trim();
cols[c] = FixComma(cols[c]);
}
yield return cols;
}
}
public static string FixComma(string raw)
{
// 〈c〉 = ","
return raw.Replace("<c>", ",");
}
private static Dictionary<Type, Func<string, object, object>> _parseMethods = new()
{
{ typeof(int), (str, def) => int.TryParse (str, out var v) ? v : def },
{ typeof(float), (str, def) => float.TryParse (str, out var v) ? v : def },
{ typeof(double), (str, def) => double.TryParse(str, out var v) ? v : def },
// 문자열 = trim & null-safe
{ typeof(string), (str, def) => string.IsNullOrEmpty(str) ? def : str },
};
public static T ParseSafe<T>(string s, T defaultValue = default)
{
if (_parseMethods.ContainsKey(typeof(T)))
return (T)_parseMethods[typeof(T)].Invoke(s, defaultValue);
throw new("파싱 불가");
}
public static T[] Load<T>(string fileName, Func<string[], T> map)
{
var ta = Resources.Load<TextAsset>($"{CSV_ROOT}/{fileName}");
var list = new List<T>();
foreach (var cols in ParseLines(ta))
list.Add(map(cols));
return list.ToArray();
}
//생략
}