Unity/기능구현

[Unity] Table CSV 파일을 ScriptableObject로 변환해보자

민트초밥 2024. 4. 30. 11:04

1. Excel 파일 만들기

 

엑셀파일의 규격이 정해져 있어야 작동하기 때문에 규격에 맞는 테이블 데이터를 만드는 작업이 필요하다.

 

테이블의 구조는 아래와 같다.

 

 

1행은 Column의 이름을 나타낸다. (ID, Name, Desc, IconPath)

 

2행은 데이터의 타입을 나타낸다.

  => 2행은 자동화 코드를 만들 때 변수의 타입을 지정해 주기 위해서 사용했다.

 

3행부터 테이블 데이터를 입력한다.

 

 

 

2. Excel -> CSV 파일 변환

 

CSV 파일로 바꾸는 방법은 여러가지가 있기 때문에 어떻게든 CSV 파일만 생성하면 된다.

 

 

Excel에서 *.csv 파일로 저장하기

 

 

 

웹에서 변환

 

XLS (EXCEL) CSV 변환 (온라인 무료) — Convertio

xls 파일(들) 업로드 컴퓨터, Google Drive, Dropbox, URL에서 선택하거나 이 페이지에서 드래그하여 선택해 주세요.

convertio.co

 

 

 

ㆍ이럴 때 사용하기 위해 만들어둔 기능

 

 

[Python] 엑셀 파일 CSV로 바꾸기

import osimport pandasimport tkinter as tkfrom tkinter import filedialogdef select_excel_folder(): global excel_folder_path excel_folder_path = filedialog.askdirectory(initialdir=f'{os.getcwd()}', title='Excel 폴더 선택') button_excel_folder.configure(

mintchobab.tistory.com

 

 

 

 

 

3. 실행 코드

 

전체 코드는 git에 있으니까 

 

public class TableMaker : MonoBehaviour
{    
    public static string CSVFolderPath = "Assets/Tables";
    public static string ScriptableFolderPath = "Assets/Resources/ScriptableObject";

    public static void MakeTableScript()
    {
        try
        {
            string[] guids = AssetDatabase.FindAssets("", new string[] { CSVFolderPath });

            foreach (var guid in guids)
            {
                string path = AssetDatabase.GUIDToAssetPath(guid);
                TextAsset asset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);

                if (asset == null || !path.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                    continue;

                List<Dictionary<string, object>> tableDataList = TableCSVReader.Read(asset, out string[] header, out string[] types);

                if (header.Length == 0 || types.Length == 0)
                    throw new Exception($"{nameof(TableMaker)} : Table Header or Type Error");

                WriteCode(asset.name, header, types);
            }
            
            EditorUtility.SetDirty(scriptableObj);

            AssetDatabase.Refresh();
            AssetDatabase.SaveAssets();
        }
        catch(Exception e)
        {
            Debug.LogError($"{nameof(TableMaker)} : {e.Message}");
        }
    }
}

 

특정 폴더의 모든 csv 파일을 읽어와서 테이블로 변환하는 작업

※ 테이블 데이터가 아닌 csv 파일이 있다면 정상작동하지 않는다.

 

 

 

using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;

public class TableCSVReader : MonoBehaviour
{
    static string SPLIT_RE = @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))";
    static string LINE_SPLIT_RE = @"\r\n|\n\r|\n|\r";
    static char[] TRIM_CHARS = { '\"' };


    public static List<Dictionary<string, object>> Read(TextAsset data)
    {
        return ReadInternal(data, out string[] header, out string[] types);
    }

    public static List<Dictionary<string, object>> Read(TextAsset data, out string[] header, out string[] types)
    {
        return ReadInternal(data, out header, out types);
    }


    public static List<Dictionary<string, object>> ReadInternal(TextAsset data, out string[] header, out string[] types)
    {
        var list = new List<Dictionary<string, object>>();

        header = new string[] { };
        types = new string[] { };

        if (data == null)
        {
            Debug.LogError($"{nameof(TableCSVReader)} : TextAsset is Null");
            return null;
        }

        var lines = Regex.Split(data.text, LINE_SPLIT_RE);

        if (lines.Length <= 1)
            return list;

        header = Regex.Split(lines[0], SPLIT_RE);
        types = Regex.Split(lines[1], SPLIT_RE);

        for (var i = 2; i < lines.Length; i++)
        {
            var values = Regex.Split(lines[i], SPLIT_RE);
            if (values.Length == 0 || values[0] == "")
                continue;

            var entry = new Dictionary<string, object>();

            for (var j = 0; j < header.Length && j < values.Length; j++)
            {
                string value = values[j];
                value = value.TrimStart(TRIM_CHARS).TrimEnd(TRIM_CHARS).Replace("\\", "");

                object finalvalue = value;

                if (int.TryParse(value, out int intValue))
                {
                    finalvalue = intValue;
                }
                else if (float.TryParse(value, out float floatValue))
                {
                    finalvalue = floatValue;
                }

                entry[header[j]] = finalvalue;
            }

            list.Add(entry);
        }

        return list;
    }
}

 

csv 파일을 읽어서 데이터를 추출하는 작업

 

앞서 말했듯 테이블의 1행이 Dictionary의 key가 돼서 key의 맞는 데이터를 탐색하는 데 사용되고

2행은 최종 작성되는 Table.cs 파일의 변수 타입이 된다.

 

 

 

private static void WriteCode(string tableName, string[] header, string[] types)
{
    StringBuilder sb = new StringBuilder();

    sb.AppendLine("using System;");
    sb.AppendLine("using System.Collections.Generic;");
    sb.AppendLine("using UnityEngine;");
    sb.AppendLine();

    sb.AppendLine($"public class {tableName} : SingletonScriptableObject<{tableName}>");
    sb.AppendLine("{");

    sb.AppendLine("\t[SerializeField]");
    sb.AppendLine("\tpublic List<TableData> datas = new List<TableData>();");
    sb.AppendLine();

    sb.AppendLine("\tpublic TableData this[int index]");
    sb.AppendLine("\t{");
    sb.AppendLine("\t\tget");
    sb.AppendLine("\t\t{");
    sb.AppendLine("\t\t\treturn datas.Find(x => x.ID == index);");
    sb.AppendLine("\t\t}");
    sb.AppendLine("\t}");

    sb.AppendLine();

    sb.AppendLine("\t[Serializable]");
    sb.AppendLine("\tpublic class TableData");
    sb.AppendLine("\t{");

    for (int i = 0; i < header.Length; i++)
    {
        sb.AppendLine($"\t\tpublic {types[i]} {header[i]};");
    }

    sb.AppendLine("\t}");
    sb.AppendLine();

    sb.AppendLine("\tpublic void AddData(TableData data)");
    sb.AppendLine("\t{");
    sb.AppendLine("\t\tdatas.Add(data);");
    sb.AppendLine("\t}");

    sb.AppendLine("}");
    sb.AppendLine();


    string textsaver = $"Assets/{tableName}.cs";

    if (File.Exists(textsaver))
    {
        File.Delete(textsaver);
    }

    File.AppendAllText(textsaver, sb.ToString());
}

 

cs 파일을 생성하고 미리 정해진 규칙대로 코드를 작성 후 저장

 

 

 

 

이렇게 결과 파일이 생성된다.

primary key의 역할을 하는 ID 값으로 데이터를 검색하게 하고 싶어서 인덱서를 사용했다.

 

 

ex)

string name = TestTableFirst.Instance[101].Name;
string desc = TestTableFirst.Instance[101].Desc;

 

 

 

4. EditorWindow

 

 

함수를 실행시켜 줄 에디터 창을 만들었다.

 

 

public class TableMakerWindow : EditorWindow
{
    [MenuItem("Custom/TableMakerWindow")]
    public static void Init()
    {
        TableMakerWindow window = (TableMakerWindow)EditorWindow.GetWindow(typeof(TableMakerWindow));
        window.minSize = new Vector2(500, 300);
        window.Show();
    }


    public void OnGUI()
    {
        GUILayout.Label("Path Settings", EditorStyles.boldLabel);

        TableMaker.CSVFolderPath = EditorGUILayout.TextField("CSV Folder Path", TableMaker.CSVFolderPath);
        TableMaker.ScriptableFolderPath = EditorGUILayout.TextField("Scriptable Folder Path", TableMaker.ScriptableFolderPath);

        EditorGUILayout.Space(20);

        GUILayout.BeginHorizontal();

        if (GUILayout.Button("Make Table Script"))
        {
            TableMaker.MakeTableScript();
        }
        else if (GUILayout.Button("Make Scriptable Object"))
        {
            TableMaker.MakeScriptableObject();
        }

        GUILayout.EndHorizontal();
    }
}

 

 

 

 

 

 

5. 실행 결과

 

 

 

 

 

 


후기 

 

초기 목표는 한번의 클릭으로 모든 작업이 완료되는 자동화였지만 방법을 잘 모르겠어서 기능을 나누게 되었다.

 

그리고 개선해야 될 점이 남아있어서 아직 사용은 할 수 없을 것 같지만 개선 or 다른 방법을 찾아서 좀 더 편하게 할 수 있도록 만들어봐야겠다.

 

 

※ 개선 사항

 

1. Excel -> CSV의 변환 개선

 

2. cs 파일 만들기 + ScriptableObject 파일 만들기가 한 번에 실행될 수 있도록 변경

  - 지금 상태에서도 jenkins를 이용하면 동작하게 할 수 있을 것 같지만 그건 추후에 테스트해보기로...

 

3. 예외 처리 + 예외 처리 테스트

  - 잘못된 규격의 테이블

  - 파일 오류

  - 빌드 테스트

  - 빌드 후 데이터 사용 가능 여부

 

 

 

 

프로젝트 링크

 

GitHub - mintchobab/CSVToScriptableObject

Contribute to mintchobab/CSVToScriptableObject development by creating an account on GitHub.

github.com

 

반응형