【Unity】ExcelのデータをScriptableObjectに取り込む

Excel で管理しているゲームデータを自作のプログラムを使って ScriptableObject にインポートする方法を紹介したいと思います。

これ系は既にライブラリとかアセットが色々と配布されていて、それらを使用したほうが時間の節約になると思いますが、今回はを全て自作してみたいと思います。

ツール類は全て C# で作成し、Excel VBA や Python は使用しません。

確認環境

  • Windows11 + VisualStudio2022
  • Unity 2022.3.5f1
  • .NET 6(コンソールアプリ作成用
    • ExcelDataReader 3.6.0
    • ExcelDataReader.DataSet 3.6.0
    • Newtonsoft.Json 13.0.3

エディター上で動作を確認しています。

Excelからデータを取り込む流れ

Excel データを Unity に取り込む手順は以下の通りです。

  1. エクセルでデータを作成する
  2. Unity上でデータ構造を定義する
  3. エクセルを ExcelDataReader で JSON ファイルに変換する
  4. Unity上からJSONファイルを読み込む

実装手順

エクセルでデータを作成する

まず Excel でデータを作成します。見た目はこんな感じで Excel を作成しておきます。

保存先 D:\Master\ItemData.json

キー アイテム名 重さ 説明
Item01 アイテム1 0.01 1 1 アイテム1です
Item02 アイテム2 0.02 1 2 アイテム2です
Item03 アイテム3 0.03 2 1 アイテム3です
Item04 アイテム4 0.04 2 2 アイテム4です
Item05 アイテム5 0.05 1 1 アイテム5です
Item06 アイテム6 0.06 1 2 アイテム6です
Item07 アイテム7 0.07 2 1 アイテム7です
Item08 アイテム8 0.08 2 2 アイテム8です
Item09 アイテム9 0.09 1 1 アイテム9です

Unity上でデータ構造を定義する

Unity 上にエクセルデータに対応するデータ構造を ScriptableObject で実装します。

これをマスターデータとしてプロジェクトにアセットとして新規作成します。

// ItemMaster.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;
using UnityEngine;

namespace Takap
{
    // マスターデータ保持用のオブジェクト
    [CreateAssetMenu(menuName = "MyData/ItemMaster")]
    public class ItemMaster : ScriptableObject
    {
        [SerializeField] List<ItemData> _itemList;
        public ReadOnlyCollection<ItemData> ItemList
            => new ReadOnlyCollection<ItemData>(_itemList);
    }
}

// ItemData.cs

using System;
using UnityEngine;

namespace Takap
{
    [Serializable]
    public class ItemData
    {
        [SerializeField] string _key = "";
        [SerializeField] string _name = "";
        [SerializeField] float _weight;
        [SerializeField] int _high;
        [SerializeField] int _width;
        [SerializeField] string _text;

        public string Key => _key;
        public string Name => _name;
        public float Weight => _weight;
        public int High => _high;
        public int Width => _width;
        public string Text => _text;

        public ItemData(string key, string name, float weight, int high, int width, string text)
        {
            _key = key;
            _name = name;
            _weight = weight;
            _high = high;
            _width = width;
            _text = text;
        }
    }
}

上記コードを追加するとプロジェクトのコンテキストメニューに以下のように MyData > ItemMaster という項目が追加されるのでそれを選択して、「ItemMaster」という名前でアセットを作成します。

作成すると以下のような状態になっていると思います。

ExcelをExcelDataReaderでJSONファイルに変換する

Excel を JSON に変換するために外部にコンソールアプリを作成します。このプログラムに Excel をドラッグ & ドロップするとJSONが指定したパスに出力されるようにします。

VisualStudio 上からコンソールアプリのプロジェクトを新規作成し .NET 6 を選択してコンソールアプリを新規作成します。

NuGet から以下の 3つをインストールしておきます。

  • ExcelDataReader
  • ExcelDataReader.DataSet
  • Newtonsoft.Json

そして、先ほど Unity 上に実装した ItemData.cs の内容をコンソールアプリ内にコピーし、コンソールアプリを以下のように実装します。

注意:1点制限があって ItemData クラスの読み書きしたい対象に JsonProperty 属性を追加しています。

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using ExcelDataReader;
using Newtonsoft.Json;

namespace MasterDataToJson
{
    internal class Program
    {
        static readonly ExcelToJson _excelToJson = new();

        static void Main(string[] args)
        {
            // パスを引数で受け取る
            string excelFilePath = args[0];
            _excelToJson.ExecExcelToJson(excelFilePath);
        }
    }

    // ExcelをJSONに変換するためのクラス
    public class ExcelToJson
    {
        static readonly JsonSerializer _jsonSerializer = new();

        public void ExecExcelToJson(string filePath)
        {
            using FileStream stream = 
                    File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

            // 拡張子がxlsx(新しいほう)のファイルの読み取りを指定
            // 古いxlsはCreateBinaryReaderメソッドを使う
            using IExcelDataReader reader = 
                    ExcelReaderFactory.CreateOpenXmlReader(stream, JpEncode());

            DataTable sheet = reader.AsDataSet().Tables[0];
            ItemData[] data = ReadContents(sheet).ToArray(); // オブジェクトに変換
            foreach (var item in data)
            {
                Console.WriteLine($"{item.Key}");
            }

            using TextWriter tw = new StreamWriter(GetOutputPath(sheet), false, Encoding.UTF8);
            _jsonSerializer.Serialize(tw, data);
        }

        // エンコードの設定を追加
        private static ExcelReaderConfiguration JpEncode()
        {
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
            return new() { FallbackEncoding = Encoding.GetEncoding("Shift_JIS") };
        }

        // 出力先パスの取得
        private string GetOutputPath(DataTable sheet) => sheet.Rows[0][1].ToString();

        // Excelのシートの内容をC#のオブジェクトに変換する
        private IEnumerable<ItemData> ReadContents(DataTable sheet)
        {
            for (int i = 3; i < sheet.Rows.Count; i++) // ヘッダーは読み飛ばす
            {
                DataRow row = sheet.Rows[i];
                yield return new ItemData(
                    row[0].ToString(),
                    row[1].ToString(),
                    float.Parse(row[2].ToString()),
                    int.Parse(row[3].ToString()),
                    int.Parse(row[4].ToString()),
                    row[5].ToString()
                    );
                    
            }
        }
    }

    // コピーしてきたクラス
    [Serializable]
    public class ItemData
    {
        // ★読みたい対象にJsonPropertyを追加する
        [SerializeField, JsonProperty] string _key = "";
        [SerializeField, JsonProperty] string _name = "";
        [SerializeField, JsonProperty] float _weight;
        [SerializeField, JsonProperty] int _high;
        [SerializeField, JsonProperty] int _width;
        [SerializeField, JsonProperty] string _text;

        public string Key => _key;
        public string Name => _name;
        public float Weight => _weight;
        public int High => _high;
        public int Width => _width;
        public string Text => _text;

        public ItemData(string key, string name, float weight, int high, int width, string text)
        {
            _key = key;
            _name = name;
            _weight = weight;
            _high = high;
            _width = width;
            _text = text;
        }
    }

    // Unity外だと存在しないので名前だけ追加しておく
    public class SerializeFieldAttribute : Attribute { }
}

このプログラムをコンパイルして、Excel ファイルをこのプログラムにドラッグ & ドロップすると Excel 内で指定した位置にファイルが出力されます。

中身は整形するとこんな感じになってます(余計な要素も出力されていますが参照しないのでこのまま使用します)

Unity上からJSONファイルを読み込む

最後に JSON をUnity 上から読み取るための処理を実装します。

最初に作成した ScriptableObject をまず以下のように修正します。

// ItemMaster.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;
using Takap.Utility;
using UnityEngine;

namespace Takap
{
    [CreateAssetMenu(menuName = "MyData/ItemMaster")]
    public class ItemMaster : ScriptableObject
    {
        [SerializeField] List<ItemData> _itemList;
        public ReadOnlyCollection<ItemData> ItemList
            => new ReadOnlyCollection<ItemData>(_itemList);

        // ★★★追加ここから --->
        [SerializeField] string _filePath;
        public void ReadJson()
        {
            if (string.IsNullOrEmpty(_filePath) || !System.IO.File.Exists(_filePath))
            {
                Debug.Log("JSONファイルが見つかりません。");
                return;
            }
            // 中身を読み取って処理用のメソッドに渡す
            string json = System.IO.File.ReadAllText(_filePath);
            ReadJson(json);
        }

        // JSON文字列を受け取ってScriptableObjectを更新する
        public void ReadJson(string json)
        {
            ItemData[] items = JsonHelper.FromJson<ItemData>(json);
            _itemList.Clear();
            _itemList.AddRange(items);

#if UNITY_EDITOR
            UnityEditor.EditorUtility.SetDirty(this);
            UnityEditor.AssetDatabase.SaveAssets();
#endif
            Debug.Log("インポートが完了しました。");
        }
        // <--- ★★★追加ここまで
    }
}

JsonHelper.FromJson はメソッドですがルート要素が配列のデータを扱うためのライブラリで以下のリンクから取得して記事中のコードをプロジェクトに取り込んでおいてください。

takap-tech.com

次に、ScriptableObject に Editor 拡張を追加して読み取りボタンを追加して読み取りを実行します。

// ItemMasterEditor.cs

#if UNITY_EDITOR

using UnityEditor;
using UnityEngine;

namespace Takap
{
    [CustomEditor(typeof(ItemMaster))]
    public class ItemMasterEditor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            // 通常のインスペクターを表示
            DrawDefaultInspector();

            // 対象のクラスのインスタンスを取得
            ItemMaster targetClass = (ItemMaster)target;

            // ボタンを追加
            if (GUILayout.Button("JSONの読み取り"))
            {
                targetClass.ReadJson();
            }
        }
    }
}
#endif

れこをビルドすると以下のようにJSONの読み取り実行ボタンと FilePath が表示されるので FilePath に Excel に入力したファイルパスを指定します。

これで実行を押すとコンソールに「インポートが完了しました」というメッセージが表示されて、ItemMaster が以下の通り更新されていると思います。

これで Excel のデータを ScriptableObject に取り込むことができました。

この ItemMaster をインスペクターから GameObject に設定するなどしてゲーム中で使用することができます。

記事中の実装はサンプルなので機能がチープです。各自で使いやすいように機能は追加していきましょう。

最後に

この記事で紹介した内容は基本的な処理の流れのサンプルとなります。アイデア次第でもっと便利にすることもできると思います。

例えば、Excel も Unity 内に格納して処理を Unity 内で完結するようにしたり、Excel の更新を監視し、変更があったら JSON ファイルを生成してGit に Push まで自動でする事もできると思います。

JSON さえあればそのデータを ScriptableObject に取り込めるのでデータの管理は JSON にエクスポートさえできれば Excel 以外でも扱えます。Editor の実行前に変更があったらマスターを更新して実行なども考えられると思います。

全て自作は、配布されているツールの他人が考えた仕組みを覚える必要が無くて、ツールでは対応できないワークフローやデータ形式も柔軟に対応することができるのがよい所だと思います。

各自で機能を追加してどんどん便利にしていきましょう。