csvからアセットファイルを作成する話

今回はUnityでゲームを開発中に、登場するキャラクターのマスターデータをScriptableObjectに詰めていくときに、膨大なデータをひとつひとつ詰めていくのは億劫で、Editor拡張を使って一括でアセットファイルを作ってみたので共有です。

## 環境
macOS Ventura 13.3.1

Unity 2021.3.20f1

## Editor拡張って？？
Editor拡張とは、Unityエディタの機能を拡張したり、そのためのスクリプトのことを言います。

今回はデータをインポートするために使いますが、独自のウィンドウを作成したり、ボタンの色を変えたり、作りたいゲームに応じて様々な拡張機能をつくることができます。

Editor拡張を書くときは`UnityEditor`クラスを使います。こちらはGitHubで公開されているので下から参考にできます。

https://github.com/Unity-Technologies/UnityCsReference

## 前準備

### ScriptableObjectの作成
マスタデータとなるキャラクターのデータをScriptableObjectとして作成します。(ポケモンのキャラクターを想定して書いていきます)

```c#:PokemonBase
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// ポケモンのマスターデータ
public class PokeonBase : ScriptableObject
{
    // 名前、タイプ
    public string Name;
    public PokemonType Type1;
    public PokemonType Type2;

    // ステータス
    public int Hp;
    public int Attack;
    public int Defense;
    public int SpecialAttack;
    public int SpecialDefense;
    public int Speed;

}

// タイプマスタ
public enum PokemonType
{
    None,
    ノーマル,
    ほのお,
    みず,
    でんき,
    くさ,
    こおり,
    かくとう,
    どく,
    じめん,
    ひこう,
    エスパー,
    むし,
    いわ,
    ゴースト,
    ドラゴン,
    あく,
    はがね
}
```

今回はキャラクターに「名前」「タイプ」「ステータス」を持たせます。
タイプは`PokemonType`というクラスを別で作成してそれを使うことにします。


### csvファイルの準備
ScriptableObjectに詰め込んでいくためのデータを準備します。

二度手間になるかもしれませんが、今回はポケモンの攻略サイトからExcelにデータを落とし込んで、csvでエクスポートしていきます。


お世話になるサイトは毎度お馴染み「ポケモン徹底攻略」さん。通称「ポケ徹」。

ポケモンの図鑑情報や詳細なデータを一覧で見れたり、バトルデータやポケモンの育成論など、ポケモンバトルには欠かせないサイトになっています。

作成したcsvの中身はこちら（一部）
```csvs:PokemonBase.csv
番号,名前,Type1,Type2,Hp,Attack,Defense,SpecialAttack,SpecialDefense,Spped,,,
1,フシギダネ,くさ,どく,45,49,49,65,65,45,,,
2,フシギソウ,くさ,どく,60,62,63,80,80,60,,,
3,フシギバナ,くさ,どく,80,82,83,100,100,80,,,
4,ヒトカゲ,ほのお,,39,52,43,60,50,65,,,
5,リザード,ほのお,,58,64,58,80,65,80,,,
6,リザードン,ほのお,ひこう,78,84,78,109,85,100,,,
7,ゼニガメ,みず,,44,48,65,50,64,43,,,
8,カメール,みず,,59,63,80,65,80,58,,,
9,カメックス,みず,,79,83,100,85,105,78,,,
```
今回は一旦ホウエン地方まで（図鑑番号386のデオキシス）作っていきます。


### インポートを実行するcsvファイルをセットできるScriptableObjectを作成

UnityのInspectorウィンドウに読み込むcsvをセットしてインポートできるようにしていきます。
データインポート用のScriptableObjectアセットを作成して行く感じです。

```c#:CsvImporter
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "MyScriptable/Create CSV Importer")]
public class CsvImporter : ScriptableObject
{
    public TextAsset csvFile;
}
```
インポート対象のcsvファイルへの参照をセットしたいのでそのためのフィールドを作成します。`TextAsset`で大丈夫です。

`CreateAssetMenu`でインポート用のアセットファイルを作成できるようにしておきます。

このアセットファイルを作成して選択してからInspectorウィンドウを見てみると
![スクリーンショット 2023-05-01 0.19.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917311/cdf7bf1c-2022-cf13-a825-c9869574b22f.png)
csvファイルをセットするフィールドが出てきています。ここにインポートしたいcsvデータをセットする感じです。

## Editor拡張で読み込みボタンを作成して、インポート処理を実装

Editor拡張を行うときはスクリプトファイルを`Editor`フォルダの中に置きます。この`Editor`フォルダは特殊なフォルダで、ここに置かれたスクリプトはUnityエディタでのみ使うスクリプトとして扱われます。

`Editor`フォルダはAssetフォルダより下であればどこに配置しても大丈夫です。

今回はAssetフォルダ直下に作成しました。
この`Editor`直下にEditor拡張スクリプトを作成していきます。

ざっと中身を先に書きます。

```c#:CsvImporterEditor
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(CsvImporter))]
public class CsvImporterEditor : Editor
{
    public override void OnInspectorGUI()
    {
        var csvImporter = target as CsvImporter;
        DrawDefaultInspector();

        if (GUILayout.Button("data create"))
        {
            Debug.Log(csvImporter.csvFile.name);

            Debug.Log("ポケモンデータの作成を開始します。");
            SetPokemonCsvDataToScritableObject(csvImporter
        }
    }

    void SetPokemonCsvDataToScritableObject(CsvImporter csvImporter)
    {
        // パースを実行
        if (csvImporter.csvFile == null)
        {
            Debug.LogWarning(csvImporter.name + " : 読み込むCSVファイルがセットされていません。");
            return;
        }

        // csvファイルをstring形式に変換
        string csvText = csvImporter.csvFile.text;

        // 改行ごとにパース
        string[] afterParse = csvText.Split("\n");
        Debug.Log(afterParse.Length);

        // ヘッダー行を除いてインポート
        for (int i = 1; i < afterParse.Length; i++)
        {
            string[] parseByComma = afterParse[i].Split(",");
            Debug.Log(parseByComma);
            int column = 1;

            if (parseByComma[0] == "")
            {
                continue;
            }

            // 行数をIDとしてファイルを作成
            string path = "Assets/Resources/Pokemons/" + i.ToString() + ".asset";

            // EnemyDataのインスタンスをメモリ上に作成
            var pokemonData = CreateInstance<PokeonBase>();

            // 名前
            pokemonData.Name = parseByComma[column];

            // Type1
            column += 1;
            string type1 = parseByComma[column];
            if (type1 == "ノーマル")
            {
                pokemonData.Type1 = PokemonType.ノーマル;
            }
            else if (type1 == "ほのお")
            {
                pokemonData.Type1 = PokemonType.ほのお;
            }
            else if (type1 == "みず")
            {
                pokemonData.Type1 = PokemonType.みず;
            }
            else if (type1 == "でんき")
            {
                pokemonData.Type1 = PokemonType.でんき;
            }
            else if (type1 == "くさ")
            {
                pokemonData.Type1 = PokemonType.くさ;
            }
            else if (type1 == "こおり")
            {
                pokemonData.Type1 = PokemonType.こおり;
            }
            else if (type1 == "エスパー")
            {
                pokemonData.Type1 = PokemonType.エスパー;
            }
            else if (type1 == "かくとう")
            {
                pokemonData.Type1 = PokemonType.かくとう;
            }
            else if (type1 == "どく")
            {
                pokemonData.Type1 = PokemonType.どく;
            }
            else if (type1 == "じめん")
            {
                pokemonData.Type1 = PokemonType.じめん;
            }
            else if (type1 == "ひこう")
            {
                pokemonData.Type1 = PokemonType.ひこう;
            }
            else if (type1 == "むし")
            {
                pokemonData.Type1 = PokemonType.むし;
            }
            else if (type1 == "いわ")
            {
                pokemonData.Type1 = PokemonType.いわ;
            }
            else if (type1 == "ゴースト")
            {
                pokemonData.Type1 = PokemonType.ゴースト;
            }
            else if (type1 == "ドラゴン")
            {
                pokemonData.Type1 = PokemonType.ドラゴン;
            }
            else if (type1 == "あく")
            {
                pokemonData.Type1 = PokemonType.あく;
            }
            else if (type1 == "はがね")
            {
                pokemonData.Type1 = PokemonType.はがね;
            }


            // Type2
            column += 1;
            string type2 = parseByComma[column];
            if (type2 == "ノーマル")
            {
                pokemonData.Type2 = PokemonType.ノーマル;
            }
            else if (type2 == "ほのお")
            {
                pokemonData.Type2 = PokemonType.ほのお;
            }
            else if (type2 == "みず")
            {
                pokemonData.Type2 = PokemonType.みず;
            }
            else if (type2 == "でんき")
            {
                pokemonData.Type2 = PokemonType.でんき;
            }
            else if (type2 == "くさ")
            {
                pokemonData.Type2 = PokemonType.くさ;
            }
            else if (type2 == "こおり")
            {
                pokemonData.Type2 = PokemonType.こおり;
            }
            else if (type2 == "エスパー")
            {
                pokemonData.Type2 = PokemonType.エスパー;
            }
            else if (type2 == "かくとう")
            {
                pokemonData.Type1 = PokemonType.かくとう;
            }
            else if (type2 == "どく")
            {
                pokemonData.Type2 = PokemonType.どく;
            }
            else if (type2 == "じめん")
            {
                pokemonData.Type2 = PokemonType.じめん;
            }
            else if (type2 == "ひこう")
            {
                pokemonData.Type2 = PokemonType.ひこう;
            }
            else if (type2 == "むし")
            {
                pokemonData.Type2 = PokemonType.むし;
            }
            else if (type2 == "いわ")
            {
                pokemonData.Type2 = PokemonType.いわ;
            }
            else if (type2 == "ゴースト")
            {
                pokemonData.Type2 = PokemonType.ゴースト;
            }
            else if (type2 == "ドラゴン")
            {
                pokemonData.Type2 = PokemonType.ドラゴン;
            }
            else if (type2 == "あく")
            {
                pokemonData.Type2 = PokemonType.あく;
            }
            else if (type2 == "はがね")
            {
                pokemonData.Type2 = PokemonType.はがね;
            }
            else if (type2 == "")
            {
                pokemonData.Type2 = PokemonType.None;
            }

            // Hp
            column += 1;
            pokemonData.Hp = int.Parse(parseByComma[column]);

            // Attack
            column += 1;
            pokemonData.Attack = int.Parse(parseByComma[column]);

            // Defense
            column += 1;
            pokemonData.Defense = int.Parse(parseByComma[column]);

            // sAttack
            column += 1;
            pokemonData.SpecialAttack = int.Parse(parseByComma[column]);

            // sDefense
            column += 1;
            pokemonData.SpecialDefense = int.Parse(parseByComma[column]);

            // Speed
            column += 1;
            pokemonData.Speed = int.Parse(parseByComma[column]);


            // インスタンス化したものをアセットとして保存
            var asset = (PokeonBase)AssetDatabase.LoadAssetAtPath(path, typeof(PokeonBase));
            if (asset == null)
            {
                // 指定のパスにファイルが存在しない場合は新規作成
                AssetDatabase.CreateAsset(pokemonData, path);
            }
            else
            {
                // 指定のパスに既に同名のファイルが存在する場合は更新
                EditorUtility.CopySerialized(pokemonData, asset);
                AssetDatabase.SaveAssets();
            }
            AssetDatabase.Refresh();
        }
        Debug.Log(csvImporter.name + " : ポケモンデータの作成が完了しました。");
    }
}
```
`data create`ボタンを作成し、それをトリガーにしてcsvファイルをインポートします。
中の処理は単純にcsvデータを行ごとにパースしていきアセットファイルを繰り返し作成しているだけです。

このスクリプトを保存しUnityエディタに戻ってみると`data create`ボタンが表示されているはずです。
`Csv File`に先ほど作成したcsvファイルをセットして、`data create`を押すとcsvのデータを元にPokmeonBaseからアセットファイルが指定したパスに作成されていきます。
![スクリーンショット 2023-05-01 0.37.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917311/96466a3c-c764-f7cb-838b-70b3ab3c62d3.png)

## まとめ
いやー、楽だw

開発中のめんどくさい作業も効率化するためにコードを書いていこうとする感じは、やっとプログラマーに慣れて来たかなと思いました。