AssetDatabase와 EditorUtility를 활용한 CSVToSO

Hyeon O·2025년 7월 24일

Unity

목록 보기
13/15

개요

Unity 에디터에서 개발할 때, 자동화 같은 개발 유틸적인 부분에서 강력한 도구인

Unity 에디터 전용 Api Assetdatabase 에 대해서 정리한다.
그리고 Unity 에디터 전용 유틸리티 클래스인 EditorUtility에 대해서 정리하고

이를 활용한 CSVToSO 자동화 스크립트를 소개한다.

AssetDatabase

개념

UnityEditor.AssetDatabase는 Unity 에디터 전용 API로, 프로젝트의 Asset들을 관리하고 조작하는 데 사용되는 도구이다.

런타임에서 사용할 수 없고, 에디터 환경에서만 동작한다.

이를 통해 스크립트로 .asset, .prefab, .mat, .fbx, .csv 등 Unity 프로젝트 내의 자산 파일들을 자동으로 생성, 수정, 복사, 삭제, 이동할 수 있다.

주로 사용되는 용도는 다음과 같다.

  • Asset 파일 자동 생성 (ScriptableObject, Material, Prefab 등)
  • 에셋의 경로 기반 검색 및 로딩
  • GUID, 경로 등을 통한 참조 확인
  • Refresh 및 Import 조작
  • 메타파일 관련 작업

주요 메서드 정리

메서드명설명예시
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)

EditorUtility

개념

Unity 에디터 전용 유틸리티 클래스로 에디터에서 UI, 오브젝트 상태 조작, 확인창, 경고창, 프로그래스 바 등을 다룰 때 사용한다.

런타임에서는 사용 불가하며, UnityEditor 네임스페이스에 속해 있다.

주로 사용되는 용도는 다음과 같다.

  • 사용자 입력 받기 (팝업/파일 다이얼로그)
  • 씬이나 오브젝트에 변경이 있음을 Unity에 알리기
  • Progress Bar, Dialog 등 유저 피드백 제공
  • 에디터에서 객체 유효성 검증, 선택 객체 처리 등

주요 메서드 정리

메서드명설명예시
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)

AssetDatabase와 EditorUtility를 활용한 CSV → SO 자동화

위 개념을 가지고 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();
    }
	//생략
}
profile
천천히, 꾸준하게, 끝까지

0개의 댓글