1. サイトトップ
  2. ブログ
  3. Unity
  4. スプレッドシートで作成したデータを自動でScriptableObjectにしてみる

スプレッドシートで作成したデータを自動でScriptableObjectにしてみる

こんにちは、情熱開発部プログラム1課の前田です!

2025年も三寒四温の季節となり、体調管理には十分気を付けていきたいですね。

管理といえば、皆さんはUnityでのデータ管理はどうしていますか?

今回はUnityでのデータ管理するにあたり、スプレッドシートで作成したデータを自動でScriptableObjectにする方法を紹介します。

※各バージョンは以下となります。
Unity:2022.3.59f1
VisualStudio:2022

ScriptableObjectに対応しようと思ったきっかけ

ScriptableObjectはUnityの機能の一つで、大量のデータを保存するために使用できるデータコンテナになります。

メリットとして、エディター実行中でもインスペクター上の値を変更できる点にあります。

それにより、より細かなゲームバランスの調整が可能になります。

しかし、デメリットとして保存するデータの数が増えすぎると見にくくなる点があります。

場合によっては、スプレッドシートで管理した方が表になっているので見やすかったりします。

このデメリットを改善するために、スプレッドシートで作成したデータから自動でScriptableObjectを作成しようと思いました。

ScriptableObjectを自動生成する

以下のフローで実装してきます。

  1. スプレッドシートの作成
  2. GAS(Google Apps Script)の作成
  3. デプロイの作成
  4. エディター拡張の作成
  5. GASをUnityから実行し、スプレッドシートのデータを取得
  6. スプレッドシートのデータをCSVファイルとして保存
  7. GASから受け取ったデータからScriptableObjectクラスを作成
  8. 実行

スプレッドシートの作成

まず初めにスプレッドシートでデータを作成していきます。

データの作成にあたり、以下のようなデータ構造にします。

1列目に各データの名前(変数名)、2列目に各データの型情報を載せておきます。

この情報を元にScriptableObjectのスクリプトを作成します。

GAS(Google Apps Script)の作成

スプレッドシートへのアクセス方法はいろいろ存在しますが、今回はGASを使ってスプレッドシートの値を取得してみます。

以下はスプレッドシートの値をCSV形式で出力するコードになります。

function doGet(){
  const sheetName="シート名";
  var ss = SpreadsheetApp.openById("スプレッドシートのID");
  const sheet = ss.getSheetByName(sheetName);

  const data = sheet.getDataRange().getValues();

  // CSV形式に変換
  const csvContent = data.map(row => row.map(cell => `"${cell}"`).join(",")).join("\n");

  // UTF-8 BOMを追加
  const bom = "\uFEFF";
  const finalContent = bom + csvContent;

  return ContentService
    .createTextOutput(finalContent)
    .setMimeType(ContentService.MimeType.PLAIN_TEXT);
}

doGet()関数を使い戻り値としてCSV形式のスプレッドシートのデータを出力しています。

sheetNameにはスプレッドシートのシート名、SpreadsheetApp.openById関数にはスプレッドシートのIDを引数にセットします。

※スプレッドシートのIDはスプレッドシートのURLの以下の部分です。
https://docs.google.com/spreadsheets/d/スプレッドシートのID/edit?gid=0#gid=0

デプロイの作成

次にデプロイの作成を行います。

GASスクリプトのデプロイから新しいデプロイを選び、デプロイを作成します。

種類の選択の歯車からウェブアプリを選び、アクセスできるユーザーを全員にします。

デプロイを選択し、ウェブアプリのURLを作成します。

作成されたURLはUnity側からGASにリクエストを送るために使用するので、コピーしておきましょう。

エディター拡張の作成

GASの作成が終わったので、次にUnity側の実装に入ります。

まず初めに、ScriptableObjectを自動生成するエディター拡張を作っていきます。

以下がそのコードになります。

void OnGUI()
{
    // TextFieldでGASのURLと自動生成するScriptableObjectの名前を入力
    gasUrl = EditorGUILayout.TextField("gasUrl", gasUrl);
    scriptableObjectName = EditorGUILayout.TextField("scriptableObjectName", scriptableObjectName);

    // GASから取得したCSVデータを保存するファイル名と自動生成するScriptableObjectのスクリプトとパスを設定
    outPutCsvFilePath = Application.dataPath + "/Scripts/" + scriptableObjectName + ".csv";
    outPutCSFilePath = Application.dataPath + "/Scripts/" + scriptableObjectName + ".cs";

    if (GUILayout.Button("GenerateScriptableObject"))
    {
        EditorCoroutineUtility.StartCoroutine(GenerateScriptableObject(), this);
    }
}

GASのURLと自動生成するScriptableObjectの名前をTextFieldで設定できるようにしています。

outPutCsvFilePathoutPutCSFilePathにはこの後作成するCSVファイルのパスと自動生成するScriptableObjectのスクリプトのパスを設定しています。

コルーチンの引数に指定しているGenerateScriptableObject関数で、ScriptableObjectのスクリプトを自動生成していきます。

GASをUnityから実行し、スプレッドシートのデータを取得

次にUnity側から先ほど作成したGASを呼び出し、スプレッドシートのデータを取得してみます。

以下がその関数になります。

    IEnumerator GenerateScriptableObject()
    {
        using (UnityWebRequest request = UnityWebRequest.Get(gasUrl))
        {
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                var csvData = request.downloadHandler.text;
            }
        }
    }

UnityWebRequestを使い、先ほど作成したGASにリクエストを送っています。
リクエストが成功すると、先ほど作成したGASが実行され、request.downloadHandler.textで戻り値を取得できます。

スプレッドシートのデータをCSVファイルとして保存

ScriptableObjectアセットに、スプレッドシートのデータを流し込めるようにCSVファイルとして保存しておきます。

以下がコードになります。

    void SaveCsvFile(string data)
    {
        File.WriteAllText(outPutCsvFilePath, data, Encoding.UTF8);
        AssetDatabase.Refresh();
    }

GenerateScriptableObject関数から呼び出し、取得したデータをCSVファイルとして保存します。

    IEnumerator GenerateScriptableObject()
    {
        using (UnityWebRequest request = UnityWebRequest.Get(gasUrl))
        {
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                var csvData = request.downloadHandler.text;

                // 先頭の\uFEFFを削除
                if (csvData[0] == '\uFEFF')
                {
                    csvData = csvData.Substring(1);
                }

                // CSVファイルとして保存
                SaveCsvFile(csvData);
            }
        }
    }

保存したCSVファイルは、ScriptableObjectにデータを入れるために使っていきます。

GASから受け取ったデータからScriptableObjectクラスを作成

スプレッドシートのデータをCSVファイルとして保存できたので、最後にScriptableObjectのスクリプトを自動生成するコード作成していきます。

今回は以下のスクリプトを自動生成します。

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName ="EnemyDataList",menuName = "ScriptableObject/EnemyDataList")]
public class EnemyDataList : ScriptableObject
{
    [SerializeField]
    public List<EnemyData> DataList;

    private void OnEnable()
    {
        LoadCsvData();
    }
    public void LoadCsvData()
    {
        DataList = new List<EnemyData>();
        var filePath="保存したCSVファイルのパス";
        string[,] data = GenerateScriptableObjectMenu.LoadCsvAs2DArray(filePath);
        for (int i = 2; i < data.GetLength(0); i++)
        {
            var enemyData = new EnemyData();
            enemyData.id = int.Parse(data[i, 0]);
            enemyData.EnemyName = data[i, 1];
            enemyData.Lv = int.Parse(data[i, 2]);
            enemyData.Hp = int.Parse(data[i, 3]);
            enemyData.Mp = int.Parse(data[i, 4]);
            enemyData.Speed = float.Parse(data[i, 5]);
            DataList.Add(enemyData);
        }
    }
}

[System.Serializable]
public class EnemyData
{
    [SerializeField]
    public int id;
    [SerializeField]
    public string EnemyName;
    [SerializeField]
    public int Lv;
    [SerializeField]
    public int Hp;
    [SerializeField]
    public int Mp;
    [SerializeField]
    public float Speed;
}

GenerateScriptableObjectMenu.LoadCsvAs2DArray関数で先ほど保存したCSVファイルから2次元配列でデータを取得しています。
そして取得したデータをDataList変数に入れていっています。

以下が自動生成するためのコードになります。

    void GenerateScriptableObjectCS(string[,] ParseCsvData)
    {
        using (var sw = new StreamWriter(outPutCSFilePath)) 
        {
            sw.WriteLine("using System.Collections.Generic;");
            sw.WriteLine("using UnityEngine;\n");
            sw.WriteLine($"[CreateAssetMenu(fileName =\"{scriptableObjectName}List\",menuName = \"ScriptableObject/{scriptableObjectName}List\")]");
            sw.WriteLine($"public class {scriptableObjectName}List : ScriptableObject");
            sw.WriteLine("{");
            sw.WriteLine("    [SerializeField]");
            sw.WriteLine($"    public List<{scriptableObjectName}> DataList;\n");
            sw.WriteLine($"    private void OnEnable()");
            sw.WriteLine("    {");
            sw.WriteLine("        LoadCsvData();");
            sw.WriteLine("    }");
            sw.WriteLine("    public void LoadCsvData()"); 
            sw.WriteLine("    {");
            sw.WriteLine($"        DataList = new List<{scriptableObjectName}>();");
            sw.WriteLine($"        var filePath=\"{outPutCsvFilePath}\";"); 
            sw.WriteLine("        string[,] data = GenerateScriptableObjectMenu.LoadCsvAs2DArray(filePath);"); 
            sw.WriteLine("        for (int i = 2; i < data.GetLength(0); i++)"); 
            sw.WriteLine("        {");

            var dataName = char.ToLower(scriptableObjectName[0]) + scriptableObjectName.Substring(1);
            sw.WriteLine($"            var {dataName} = new {scriptableObjectName}();");

            for (int i = 0; i < ParseCsvData.GetLength(1); i++)
            {
                var parseDatastr = ParseCsvData[1, i] == "string" ? $"data[i, {i}]" : $"{ParseCsvData[1, i]}.Parse(data[i, {i}])";
                sw.WriteLine($"            {dataName}.{ParseCsvData[0, i]} = {parseDatastr};");
            }

            sw.WriteLine($"            DataList.Add({dataName});");
            sw.WriteLine("        }"); 
            sw.WriteLine("    }");
            sw.WriteLine("}\n");

            sw.WriteLine("[System.Serializable]");
            sw.WriteLine($"public class {scriptableObjectName}");
            sw.WriteLine("{");
            for (int i = 0;i < ParseCsvData.GetLength(1);i++)
            {
                // スプレッドシートの1行目(変数名)、2行目(型)を参照し、変数を作成
                sw.WriteLine("    [SerializeField]");
                sw.WriteLine($"    public {ParseCsvData[1,i]} {ParseCsvData[0, i]};");
            }
            sw.WriteLine("}\n");
        }

        AssetDatabase.Refresh();
    }

引数のParseCsvDataはスプレッドシートから取得したCSVデータを2次元配列にしたデータを引数に取ります。

実行

最後に先ほど作成したGenerateScriptableObjectCS関数をGenerateScriptableObject関数から呼び出してあげます。

    IEnumerator GenerateScriptableObject()
    {
        using (UnityWebRequest request = UnityWebRequest.Get(gasUrl))
        {
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                //......省略

                // CSVファイルとして保存
                SaveCsvFile(csvData);

                // GASから受け取ったCSVデータを2次元配列に変換
                var ParseCsvData = ParseCsv(csvData);

                // ScriptableObjectのC#スクリプトを生成
                GenerateScriptableObjectCS(ParseCsvData);
            }
        }
    }

Tool/GenerateScriptableObjectMenuを選び、TextFieldにGASのURLとScriptableObjectの名前を入力し、GenerateScriptableObjectを押し実行してみましょう!

Scriptsフォルダに、TextFieldに入力した名前のCSVファイルとCSファイルが作成されていれば成功です。

最後にScriptableObjectアセットを作ってみます。

任意のフォルダで、右クリック>Create>ScriptableObject>TextFieldに入力した名前を選択しScriptableObject(.asset)を作成してみます。

スプレッドシートで定義したデータ通りのパラメータになっていたら成功です。

まとめ

今回はスプレッドシートからScriptableObjectを自動生成する方法を紹介しました。

スプレッドシートからScriptableObjectを作成できれば、チーム開発の時にUnityに慣れていない人でもスプレッドシートからデータを変えられるのでより開発の効率が上がります。

またScriptableObject側から変更したデータをスプレッドシートに反映させるようにできれば、よりデータ管理が楽になると思います。

最後にGenerateScriptableObjectMenu.csの全体コードを載せておきます。

using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Unity.EditorCoroutines.Editor;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;

public class GenerateScriptableObjectMenu : EditorWindow
{
    private string gasUrl = "";
    private string scriptableObjectName = "";
    private string outPutCsvFilePath = "";
    private string outPutCSFilePath = "";

    [MenuItem("Tool/GenerateScriptableObjectMenu")]
    public static void ShowWindow()
    {
        EditorWindow.GetWindow(typeof(GenerateScriptableObjectMenu));
    }

    void OnGUI()
    {
        // TextFieldでGASのURLと自動生成するScriptableObjectの名前を入力
        gasUrl = EditorGUILayout.TextField("gasUrl", gasUrl);
        scriptableObjectName = EditorGUILayout.TextField("scriptableObjectName", scriptableObjectName);

        // GASから取得したCSVデータを保存するファイル名と自動生成するScriptableObjectのスクリプトとパスを設定
        outPutCsvFilePath = Application.dataPath + "/Scripts/" + scriptableObjectName + ".csv";
        outPutCSFilePath = Application.dataPath + "/Scripts/" + scriptableObjectName + ".cs";

        if (GUILayout.Button("GenerateScriptableObject"))
        {
            EditorCoroutineUtility.StartCoroutine(GenerateScriptableObject(), this);
        }
    }

    IEnumerator GenerateScriptableObject()
    {
        using (UnityWebRequest request = UnityWebRequest.Get(gasUrl))
        {
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                var csvData = request.downloadHandler.text;

                // 先頭の\uFEFFを削除
                if (csvData[0] == '\uFEFF')
                {
                    csvData = csvData.Substring(1);
                }

                // CSVファイルとして保存
                SaveCsvFile(csvData);

                // GASから受け取ったCSVデータを2次元配列に変換
                var ParseCsvData = ParseCsv(csvData);

                // ScriptableObjectのC#スクリプトを生成
                GenerateScriptableObjectCS(ParseCsvData);
            }
        }
    }

    // CSVファイルに保存する
    void SaveCsvFile(string data)
    {
        File.WriteAllText(outPutCsvFilePath, data, Encoding.UTF8);
        AssetDatabase.Refresh();
    }

    // 2次元配列でCSVファイルをロードする関数
    public static string[,] LoadCsvAs2DArray(string filePath)
    {
        string[] lines = File.ReadAllLines(filePath); // 行ごとに読み込む
        int rows = lines.Length;
        int cols = lines[0].Split(',').Length; // 1行目の列数を基準にする
        string[,] array = new string[rows, cols];

        for (int i = 0; i < rows; i++)
        {
            string[] cells = lines[i].Split(','); // カンマ区切りで分割
            for (int j = 0; j < cols; j++)
            {
                array[i, j] = cells[j].Trim('\"'); // 余分な " を削除
            }
        }

        return array;
    }

    // CSVデータを2次元配列に変換する関数
    string[,] ParseCsv(string csvData)
    {
        // 行ごとに分割(\r\n か \n を考慮)
        string[] rows = csvData.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);

        // 1行目の列数を基準にする
        string[] firstRow = SplitCsvLine(rows[0]);
        int rowCount = rows.Length;
        int colCount = firstRow.Length;

        // 2次元配列を作成
        string[,] result = new string[rowCount, colCount];

        for (int i = 0; i < rowCount; i++)
        {
            string[] cols = SplitCsvLine(rows[i]);

            for (int j = 0; j < colCount; j++)
            {
                // 配列の範囲を超えた場合は空文字をセット
                result[i, j] = j < cols.Length ? cols[j] : "";
            }
        }

        return result;
    }

    // CSVの1行を分割する関数
    string[] SplitCsvLine(string line)
    {
        // 正規表現でCSVのフィールドを抽出
        MatchCollection matches = Regex.Matches(line, "\"([^\"]*)\"|([^,]+)");

        string[] fields = new string[matches.Count];

        for (int i = 0; i < matches.Count; i++)
        {
            fields[i] = matches[i].Value.Trim('"'); // ダブルクォートを削除
        }

        return fields;
    }

    void GenerateScriptableObjectCS(string[,] ParseCsvData)
    {
        using (var sw = new StreamWriter(outPutCSFilePath)) 
        {
            sw.WriteLine("using System.Collections.Generic;");
            sw.WriteLine("using UnityEngine;\n");
            sw.WriteLine($"[CreateAssetMenu(fileName =\"{scriptableObjectName}List\",menuName = \"ScriptableObject/{scriptableObjectName}List\")]");
            sw.WriteLine($"public class {scriptableObjectName}List : ScriptableObject");
            sw.WriteLine("{");
            sw.WriteLine("    [SerializeField]");
            sw.WriteLine($"    public List<{scriptableObjectName}> DataList;\n");
            sw.WriteLine($"    private void OnEnable()");
            sw.WriteLine("    {");
            sw.WriteLine("        LoadCsvData();");
            sw.WriteLine("    }");
            sw.WriteLine("    public void LoadCsvData()"); 
            sw.WriteLine("    {");
            sw.WriteLine($"        DataList = new List<{scriptableObjectName}>();");
            sw.WriteLine($"        var filePath=\"{outPutCsvFilePath}\";"); 
            sw.WriteLine("        string[,] data = GenerateScriptableObjectMenu.LoadCsvAs2DArray(filePath);"); 
            sw.WriteLine("        for (int i = 2; i < data.GetLength(0); i++)"); 
            sw.WriteLine("        {");

            var dataName = char.ToLower(scriptableObjectName[0]) + scriptableObjectName.Substring(1);
            sw.WriteLine($"            var {dataName} = new {scriptableObjectName}();");

            for (int i = 0; i < ParseCsvData.GetLength(1); i++)
            {
                var parseDatastr = ParseCsvData[1, i] == "string" ? $"data[i, {i}]" : $"{ParseCsvData[1, i]}.Parse(data[i, {i}])";
                sw.WriteLine($"            {dataName}.{ParseCsvData[0, i]} = {parseDatastr};");
            }

            sw.WriteLine($"            DataList.Add({dataName});");
            sw.WriteLine("        }"); 
            sw.WriteLine("    }");
            sw.WriteLine("}\n");

            sw.WriteLine("[System.Serializable]");
            sw.WriteLine($"public class {scriptableObjectName}");
            sw.WriteLine("{");
            for (int i = 0;i < ParseCsvData.GetLength(1);i++)
            {
                // スプレッドシートの1行目(変数名)、2行目(型)を参照し、変数を作成
                sw.WriteLine("    [SerializeField]");
                sw.WriteLine($"    public {ParseCsvData[1,i]} {ParseCsvData[0, i]};");
            }
            sw.WriteLine("}\n");
        }

        AssetDatabase.Refresh();
    }
}

参考


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。