今回はUnityでゲームを開発中に、登場するキャラクターのマスターデータをScriptableObjectに詰めていくときに、膨大なデータをひとつひとつ詰めていくのは億劫で、Editor拡張を使って一括でアセットファイルを作ってみたので共有です。
環境
macOS Ventura 13.3.1
Unity 2021.3.20f1
Editor拡張って??
Editor拡張とは、Unityエディタの機能を拡張したり、そのためのスクリプトのことを言います。
今回はデータをインポートするために使いますが、独自のウィンドウを作成したり、ボタンの色を変えたり、作りたいゲームに応じて様々な拡張機能をつくることができます。
Editor拡張を書くときはUnityEditor
クラスを使います。こちらはGitHubで公開されているので下から参考にできます。
前準備
ScriptableObjectの作成
マスタデータとなるキャラクターのデータをScriptableObjectとして作成します。(ポケモンのキャラクターを想定して書いていきます)
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の中身はこちら(一部)
番号,名前,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アセットを作成して行く感じです。
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ウィンドウを見てみると
csvファイルをセットするフィールドが出てきています。ここにインポートしたいcsvデータをセットする感じです。
Editor拡張で読み込みボタンを作成して、インポート処理を実装
Editor拡張を行うときはスクリプトファイルをEditor
フォルダの中に置きます。このEditor
フォルダは特殊なフォルダで、ここに置かれたスクリプトはUnityエディタでのみ使うスクリプトとして扱われます。
Editor
フォルダはAssetフォルダより下であればどこに配置しても大丈夫です。
今回はAssetフォルダ直下に作成しました。
このEditor
直下にEditor拡張スクリプトを作成していきます。
ざっと中身を先に書きます。
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からアセットファイルが指定したパスに作成されていきます。
まとめ
いやー、楽だw
開発中のめんどくさい作業も効率化するためにコードを書いていこうとする感じは、やっとプログラマーに慣れて来たかなと思いました。