こんにちは、情熱開発部プログラム1課の前田です!
2025年も三寒四温の季節となり、体調管理には十分気を付けていきたいですね。
管理といえば、皆さんはUnityでのデータ管理はどうしていますか?
今回はUnityでのデータ管理するにあたり、スプレッドシートで作成したデータを自動でScriptableObjectにする方法を紹介します。
※各バージョンは以下となります。
Unity:2022.3.59f1
VisualStudio:2022
目次
ScriptableObjectに対応しようと思ったきっかけ
ScriptableObjectはUnityの機能の一つで、大量のデータを保存するために使用できるデータコンテナになります。
メリットとして、エディター実行中でもインスペクター上の値を変更できる点にあります。
それにより、より細かなゲームバランスの調整が可能になります。
しかし、デメリットとして保存するデータの数が増えすぎると見にくくなる点があります。
場合によっては、スプレッドシートで管理した方が表になっているので見やすかったりします。
このデメリットを改善するために、スプレッドシートで作成したデータから自動でScriptableObjectを作成しようと思いました。
ScriptableObjectを自動生成する
以下のフローで実装してきます。
- スプレッドシートの作成
- GAS(Google Apps Script)の作成
- デプロイの作成
- エディター拡張の作成
- GASをUnityから実行し、スプレッドシートのデータを取得
- スプレッドシートのデータをCSVファイルとして保存
- GASから受け取ったデータからScriptableObjectクラスを作成
- 実行
スプレッドシートの作成
まず初めにスプレッドシートでデータを作成していきます。
データの作成にあたり、以下のようなデータ構造にします。

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で設定できるようにしています。
outPutCsvFilePathとoutPutCSFilePathにはこの後作成する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();
}
}
参考
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。