ポ〇モン的対戦RPGゲームの作り方-2.外部データのインポートとEditor拡張

暇つぶしがてらポ〇モン的対戦RPGゲームを開発したので、その中身の作り方の概要を記事にしたいと思います。Qiitaに投稿するの初めてで書くの慣れてないので読みにくかったらすいません。また、Unity触って1年の初心者が独学で作っただけなのであくまで方法の一つ程度として考えてください。

全部で何記事になるか分かりませんが、
1.ゲーム進行の管理
2.外部データのインポートとEditor拡張
3.簡単なAIの設計
くらいまで書けたらと思っています。

1.↓

https://qiita.com/evebursterrorR/items/033907d163db3dad135a

3.↓

https://qiita.com/evebursterrorR/items/dd3b734195b54a5ad2a3

また、今回の内容について、エクスプラボさんの記事を参考にさせて頂いた(というかほぼまんまパクった)ので、参考文献としてリンクを張らせて頂きます。ぶっちゃけこれ読むだけでほぼ全部終わる内容なのですが、一応解説していきます。

参考文献

https://ekulabo.com/csv-to-scriptable-object

___
# ・ScriptableObject
ScriptableObjectとは、Unity内で扱うデータの形式の一つで、パラメータの種類を共有するデータを多く管理する時などに便利です。

今回作るゲームでは、
①モンスターの図鑑データ(モンスターの種類ごとに固有のデータ)
②育成モンスターのデータ(育成方法や技構成など)
③技データ(威力、命中率、タイプなど)
の3つのデータをScriptableObjectで管理しています。

取り敢えずScriptableObjectを作っていきます。Assetメニュー内でCreate/C# Scriptから新規スクリプトPokeDexを作っていきます

```csharp
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName ="PokeDex",menuName ="Create PokeDex")]
public class PokeDex : ScriptableObject
{
    public enum Type
    {
              Normal
            , Fire
            , Water
            , Lightning
            , Grass
            , Ice
            , Fighting
            , Poison
            , Land
            , Fly
            , Psy
            , Insect
            , Rock
            , Ghost
            , Dragon
            , Dark
            , Metal
    }

    public int BaseH;
    public int BaseA;
    public int BaseB;
    public int BaseC;
    public int BaseD;
    public int BaseS;
    public List<Type> TypeList;
    public List<string> AbilityList;
    public Sprite FrontImage;
    public Sprite BackImage;
    public Sprite BenchImage;
    public int PokeDexNo;
    public AudioClip Nakigoe;
}
```

普通のスクリプトと違う所は、MonoBehaviourを継承せずにScriptableObjectを継承している所です。これでUnity側がScriptableObjectと認識してくれます。

モンスター図鑑データに必要なパラメータをpublicでインスペクターから入力できるようにしておきます。

一番上の[CreateAssetMenu(fileName ="PokeDex",menuName ="Create PokeDex")]ですが、これをつけておくとAssetフォルダ上で右クリック→CreateにCreate PokeDexというメニューが追加されるので、これで個別データを生成していきます。
![createpokedex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/59cbeda9-1c50-e7e7-00f3-65925eb53d09.png)

Create PokeDexをクリックするとAssetフォルダ傘下にPokeDexという名前のオブジェクトが一つ生成されます。これ一つがモンスター1種類のデータに対応していて、インスペクターからデータを入力することでデータを一つ一つ作っていけます。
![createpokedex2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/20b314a6-cd67-90f9-b37f-3018e04dd6d2.png)

こんな感じに
![createpokedex3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/eb407619-7dee-716d-12d6-daa013a6d32e.png)

でも待って下さい、モンスター数が少ないうちはデータを手入力していっても大丈夫でしょうが、今回は百種類以上のモンスターデータが必要です。そんな大量のデータをいちいち手入力でインスペクターに入れていったり画像や音声データをアタッチしていくのはあまりにも大変ですし、ヒューマンエラーが起きた時に気付けません。

そこで、外部データをUnityとは別の所で管理して、そこからデータをインポートして自動でScriptableObjectにアタッチしてくれるようなシステムを作りましょう。
___
# ・CSVからのロード

今回は外部データとしてCSVファイルを使いました。GoogleスプレッドシートでもExcelでも良いので適当に以下のような表ファイルを作成してみてください。
![csv1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/71a17d4a-4434-196b-6841-13f1c9f79c6a.png)

こんな感じでモンスター毎にデータを入力していきます。次に、このデータを元にScriptableObjectを生成するスクリプトを作成していきます。ちなみに、以下の内容は↑の参考文献に書かれている内容を自分のゲーム用にアレンジしただけです。

下のLoadCSV_Pokedexスクリプトを作成してください。

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "CSVLoader_Pokedex", menuName = "Create CSVLoader/Create CSVLoader_Pokedex")]
public class LoadCSV_Pokedex : ScriptableObject
{
	public TextAsset csvFile;
}
```

これも先ほどと同じくScriptableObjectで、CSVファイルを読み込むための入れ物です。Assetメニューで右クリックしてCreate/Create CSVLoader/Create CSVLoader_PokedexからCSVLoader_PokedexのScriptableObjectを作成します。
![createcsvloaderPokedex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/d66d36b0-3d19-98b1-62ac-9b573f36a314.png)



次に、下のCsvLoaderEditor_Pokedexスクリプトを作成してください。(参考文献より引用)


```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;

[CustomEditor(typeof(LoadCSV_Pokedex))]
public class CsvLoaderEditor_Pokedex : Editor
{
	public override void OnInspectorGUI()
	{
		var LoadCSV = target as LoadCSV_Pokedex;
		DrawDefaultInspector();

		if (GUILayout.Button("ポケモン図鑑データの作成"))
		{
			SetCsvDataToScriptableObject(LoadCSV);
		}
	}

	void SetCsvDataToScriptableObject(LoadCSV_Pokedex loadCSV)
	{
		string pokemonName = null;

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

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

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

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

			int column = 0;

			// 先頭の列が空であればその行は読み込まない
			if (parseByComma[column] == "")
			{
				continue;
			}

			// ファイルを作成
			string fileName = parseByComma[column] + ".asset";

			pokemonName = parseByComma[column];
			column += 1;

			//一応今回はResourcesに生成
			string path = "Assets/Resources/" + fileName;

			// PokeDexのインスタンスをメモリ上に作成
			PokeDex pokedex = CreateInstance<PokeDex>();

			pokedex.TypeList = new List<PokeDex.Type>();
			pokedex.AbilityList = new List<string>();

			// タイプ1
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.TypeList.Add(FromTypeNameToType(parseByComma[column]));
			}
			column += 1;

			// タイプ2
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.TypeList.Add(FromTypeNameToType(parseByComma[column]));
			}
			column += 1;

			//H種族値
			if (!String.IsNullOrEmpty(parseByComma[column]))
            {
				pokedex.BaseH = int.Parse(parseByComma[column]);
			}
			column += 1;

			//A種族値
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.BaseA = int.Parse(parseByComma[column]);
			}
			column += 1;

			//B種族値
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.BaseB = int.Parse(parseByComma[column]);
			}
			column += 1;

			//C種族値
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.BaseC = int.Parse(parseByComma[column]);
			}
			column += 1;

			//D種族値
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.BaseD = int.Parse(parseByComma[column]);
			}
			column += 1;

			//S種族値
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.BaseS = int.Parse(parseByComma[column]);
			}
			column += 1;

			// 特性1
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.AbilityList.Add(parseByComma[column]);
			}
			column += 1;

			// 特性2
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.AbilityList.Add(parseByComma[column]);
			}
			column += 1;

			// 特性3
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.AbilityList.Add(parseByComma[column]);
			}
			column += 1;


			//図鑑No.
			if (!String.IsNullOrEmpty(parseByComma[column]))
			{
				pokedex.PokeDexNo = int.Parse(parseByComma[column].TrimStart(new Char[] { '0' }));
			}
			column += 1;

			// インスタンス化したものをアセットとして保存
			var asset = (PokeDex)AssetDatabase.LoadAssetAtPath(path, typeof(PokeDex));
			if (asset == null)
			{
				// 指定のパスにファイルが存在しない場合は新規作成
				AssetDatabase.CreateAsset(pokedex, path);
			}
			else
			{
				// 指定のパスに既に同名のファイルが存在する場合は更新
				EditorUtility.CopySerialized(pokedex, asset);
				AssetDatabase.SaveAssets();
			}

			AssetDatabase.Refresh();
		}
		Debug.Log(loadCSV.name + " : ポケモン図鑑データの作成が完了しました。");
	}


	PokeDex.Type FromTypeNameToType(string TypeName)
	{
		PokeDex.Type Type = PokeDex.Type.Normal;

		switch (TypeName)
		{
			case "ノーマル":
				Type = PokeDex.Type.Normal;
				break;

			case "炎":
				Type = PokeDex.Type.Fire;
				break;

			case "水":
				Type = PokeDex.Type.Water;
				break;

			case "電気":
				Type = PokeDex.Type.Lightning;
				break;

			case "草":
				Type = PokeDex.Type.Grass;
				break;

			case "氷":
				Type = PokeDex.Type.Ice;
				break;

			case "格闘":
				Type = PokeDex.Type.Fighting;
				break;

			case "毒":
				Type = PokeDex.Type.Poison;
				break;

			case "地面":
				Type = PokeDex.Type.Land;
				break;

			case "飛行":
				Type = PokeDex.Type.Fly;
				break;

			case "エスパー":
				Type = PokeDex.Type.Psy;
				break;

			case "虫":
				Type = PokeDex.Type.Insect;
				break;

			case "岩":
				Type = PokeDex.Type.Rock;
				break;

			case "ゴースト":
				Type = PokeDex.Type.Ghost;
				break;

			case "ドラゴン":
				Type = PokeDex.Type.Dragon;
				break;

			case "悪":
				Type = PokeDex.Type.Dark;
				break;

			case "鋼":
				Type = PokeDex.Type.Metal;
				break;
		}

		return Type;
	}
}
```

このスクリプトは、CSVファイルに書かれている内容をPokeDexスクリプトのパラメータにそれぞれ当てはめていくスクリプトになっています。

using UnityEditorとなっていてEditorを継承しているこのスクリプトはビルド時には無視され、Editor内でのみ使用できるスクリプトになります。このようなEditor用のスクリプトを使うためには、Asset傘下のどこでも良いので"Editor"という名前のフォルダを作って、その中にこのスクリプトを入れてください。

↓こんな感じ
![editorfolder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/02e45391-ba01-56cb-7e57-5bc2734cd395.png)


さて、このスクリプトですが

```csharp
[CustomEditor(typeof(LoadCSV_Pokedex))]
```

の部分でLoadCSV_Pokedexを指定しています。

```csharp
if (GUILayout.Button("ポケモン図鑑データの作成"))
```

は、LoadCSV_Pokedexのインスペクター上にボタンを配置して、そのボタンが押されるとtrueを返します。

このボタンが押されると、SetCsvDataToScriptableObject(LoadCSV)が読み込まれて、CSVファイルを1区切りずつ読んでいってPokeDexのパラメータに変換して、ファイルを生成してくれます。

このスクリプトをEditorフォルダに配置したらLoadCSV_Pokedexにボタンが生成されていると思うので、上で作ったCSVファイルをアタッチしてボタンをポチッ!
![makepokedex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/58105018-696e-0599-a05a-bbfb47444551.png)

しばらく待つと...

![madepokedex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/de1930d2-2aa2-47d7-44da-2949d464621f.png)

Resourcesフォルダに無事モンスター図鑑データが生成されました!
ですが画像データとか音声データのアタッチが出来てないので、それもついでにアタッチ出来るようにしてあげましょう。

そのために私がまず使ったのが、こちらのサイトのNonResources関数(製作者様に圧倒的感謝)

https://kan-kikuchi.hatenablog.com/entry/NonResources

これにより、Editor内でResources以外のパスからでも全ファイルを取得することが出来ます。NonResourcesをEditorフォルダ内に置いて、CsvLoaderEditor_Pokedexに以下のように追記しておきましょう。

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;

[CustomEditor(typeof(LoadCSV_Pokedex))]
public class CsvLoaderEditor_Pokedex : Editor
{
    public override void OnInspectorGUI()
    {
        var LoadCSV = target as LoadCSV_Pokedex;
        DrawDefaultInspector();

        if (GUILayout.Button("ポケモン図鑑データの作成"))
        {
            SetCsvDataToScriptableObject(LoadCSV);
        }
    }

    void SetCsvDataToScriptableObject(LoadCSV_Pokedex loadCSV)
    {
        string pokemonName = null;

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

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

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

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

            int column = 0;

            // 先頭の列が空であればその行は読み込まない
            if (parseByComma[column] == "")
            {
                continue;
            }

            // ファイルを作成
            string fileName = parseByComma[column] + ".asset";

            pokemonName = parseByComma[column];
            column += 1;

            //一応今回はResourcesに生成
            string path = "Assets/Resources/" + fileName;

            // PokeDexのインスタンスをメモリ上に作成
            PokeDex pokedex = CreateInstance<PokeDex>();

            pokedex.TypeList = new List<PokeDex.Type>();
            pokedex.AbilityList = new List<string>();

           //タイプ～図鑑Noまで取得(省略)

			List<Sprite> SpriteList = NonResources.LoadAll<Sprite>("Assets/Image/PokeDex");

			List<AudioClip> NakigoeList = NonResources.LoadAll<AudioClip>("Assets/Sound/Nakigoe");

			foreach (Sprite sprite in SpriteList)
			{
				string spriteName = sprite.name;
				string pokedexNo = String.Format("{0:D3}", pokedex.PokeDexNo);

				if (spriteName == pokedexNo)
				{
					pokedex.FrontImage = sprite;
				}


				else if (spriteName == pokedexNo + "_2")
				{
					pokedex.BackImage = sprite;

				}

				else if (spriteName == pokedexNo + "_")
				{
					pokedex.BenchImage = sprite;
				}
			}

			foreach (AudioClip nakigoe in NakigoeList)
			{
				if (nakigoe.name == pokemonName)
				{
					pokedex.Nakigoe = nakigoe;
				}
			}


            // インスタンス化したものをアセットとして保存
            var asset = (PokeDex)AssetDatabase.LoadAssetAtPath(path, typeof(PokeDex));
            if (asset == null)
            {
                // 指定のパスにファイルが存在しない場合は新規作成
                AssetDatabase.CreateAsset(pokedex, path);
            }
            else
            {
                // 指定のパスに既に同名のファイルが存在する場合は更新
                EditorUtility.CopySerialized(pokedex, asset);
                AssetDatabase.SaveAssets();
            }

            AssetDatabase.Refresh();
        }
        Debug.Log(loadCSV.name + " : ポケモン図鑑データの作成が完了しました。");
    }
```

画像データと音声データを指定のパスに配置して、指定のファイル名にすることでそれぞれ対応させるようにしています。

今回の場合だと以下のような配置・ファイル名になっています。

![pokedexfolder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/7f56e6b9-4673-e280-adbf-ba30a9ce9312.png)

![nakigoefolder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/28439ecc-bf23-782e-0fca-5098a9038d4c.png)

これでもう一度LoadCSV_Pokedexのボタンをポチッ!
すると...![perfectpokedex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1736681/637e8bc9-0cc8-edfa-8688-ada4462a7bcc.png)
画像データと音声データの登録が出来ました!
後は種類を増やしても同じ方法でデータを追加して行けます。
モンスター個体データや技データについても保持するパラメータとCSVに入力するデータを変えるだけで同様にScriptableObjectとしてデータを生成できます。

---
以上がCSVファイル→ScriptableObjectへデータを入力させるエディタ拡張です。大量のデータをインポートする時には便利でした。