PONOS Advent Calendar 2020の2日目の記事です。
昨日は@kerimekaさんの【合格】Googleの認定資格、PCA(Professional Cloud Architect)を取得した話でした。
はじめに
最近、エディタ拡張機能のScriptableWizard
を使用しアセットの作成を効率化する機会があったのですが、作業の中で少し工夫が必要な部分があったので記録として残しておこうと思います。
なお、本記事の実行環境はUnity 2019.4.8f1
となります。
作成するもの
この記事で取り上げる成果物は**「画面UIのプレハブと制御用のコンポーネントを同時に作成するウィザード」**です。
例えば、ゲームを開発している中でタイトル画面表示用のプレハブと、その制御用のコンポーネントのスクリプトを同時に作成したいシチュエーションがあると思います。
単純な操作なので手作業で作成していくのもアリなのですが、「タイトル画面」以外にも「ホーム画面」や「メニュー画面」のように画面数が増えていくとその単純作業のコストも馬鹿にならず、また作成されるプレハブの構造やコンポーネントの内容を統一したい、など作成ルールが複雑化すると量産作業に余計な時間と神経を使うことになってしまいます。
そこで今回は、画面名(たとえばTitle
)を指定して作成処理を実行するだけで、その画面名にあったプレハブ(Title.prefab
)と制御用のコンポーネント(TitleViewController.cs
)が同時に作成される機能を目指しました。プレハブへの制御用コンポーネントの追加を手動で実施する必要が無いように、制御用のコンポーネントがあらかじめプレハブに追加された状態にしておきます。
この機能を使用することで画面の量産作業は画面名を入力してボタンをクリックするだけとなり、負担を大幅に軽減できます。
画像でも補足しておきます。
以下のような作成ウィザードが立ち上がり、作成したい画面名を「View Name」フィールドに入力して「Create」ボタンをクリックすることで、
以下のようにプレハブとそれを制御するためのコンポーネントが作成されます。
ScriptableWizardについて
まずはじめにScriptableWizard
について少し触れておきます。
UnityEditor.ScriptableWizard - Unity スクリプトリファレンス
ScriptableWizard
は「何かを作成するエディタウインドウ」を作成することに特化したエディタウインドウです。
以下のような特徴を持ちます。
- シリアライズ可能なフィールドがエディタウインドウ上の入力フィールドとして表示される
- 作成処理を実行するための「Create」ボタンがエディタウインドウ上に予め設置されている
ゲームオブジェクトやプレハブ、その他のアセットなどをパラメータ指定して作成するのに便利な機能となっています。
作成の手順
手順1. ScriptableWizardを作成する
まずは、ScriptableWizard
クラスを継承したクラスを作成します。
ScriptableWizard
クラスはEditorWindow
クラスを継承しているため、通常のエディタウインドウを作成する時と作業はほとんど同じです。エディタウインドウの作成を経験されている場合、ここは特に難しい点はないと思います。
今回は画面を作成するウィザードということで「ViewCraeteWizard」というクラス名で以下のようなコードを作成しました。
using UnityEngine;
using UnityEditor;
public class ViewCreateWizard : ScriptableWizard
{
[SerializeField]
string viewName = string.Empty;
[MenuItem("View/Create View")]
static void CreateWizard()
{
// 作成ウィザードを表示する。
DisplayWizard<ViewCreateWizard>("View Create Wizard");
}
}
とてもシンプルなコードですが、簡単に作成ウィザードのエディタウインドウを作成することができました。
GUIについてコーディングせずとも入力フィールドや作成ボタンが自動的に実装されており、非常にお手軽です。
手順2. コンポーネントを作成する
StringBuilder
を利用してコードの文字列を作成し、UnityプロジェクトのAssetsフォルダ以下にC#スクリプトを作成します。
ScriptableWizard
クラスを継承したクラスでは、OnWizardCreate()
メソッド内にウィザードの「Create」ボタンがクリックされた時の処理を記述することができます。
void OnWizardCreate()
{
string className = $"{viewName}ViewController";
string path = $"{Application.dataPath}/{className}.cs";
var builder = new StringBuilder();
builder.AppendLine("using UnityEngine;");
builder.AppendLine();
builder.AppendLine($"public class {className} : MonoBehaviour");
builder.AppendLine("{");
builder.AppendLine("}");
// 文字列を指定のパスに書き出す。
File.WriteAllText(path, builder.ToString());
// Unityのプロジェクトに反映する。
AssetDatabase.Refresh();
}
File.WriteAllTtext()
メソッドでスクリプトを書き出した後にAssetDatabase.Refresh()
を呼び出しておかないと、UnityのProjectウインドウ上に即時反映されないため注意してください。
これで、以下のようにウィザードを入力して「Create」ボタンをクリックすると、
以下のようなファイルが作成されるようになりました。
また、ゲームオブジェクトの「Add Component」から選択することも可能です。
手順3. プレハブを作成する
次に、プレハブを作成します。
プレハブの作成には以下のようなコードを用意します。手順2のOnWizardCreate()
メソッドに追記しています。
var gameObject = new GameObject(viewName);
var prefabPath = $"{viewName}.prefab";
// 指定したゲームオブジェクトをプレハブ化する。
PrefabUtility.SaveAsPrefabAsset(gameObject, prefabPath);
// プレハブ化したゲームオブジェクトをHierarchyから破棄する。
GameObject.DestroyImmediate(gameObject);
PrefabUtility
クラスのSaveAsPrefabAsset()
を使用することで、引数のゲームオブジェクトをプレハブ化することができます。
なお、SaveAsPrefabAsset()
を実行するためにはHierarchy上にゲームオブジェクトを作成する必要があるのでnew GameObject()
していますが、プレハブ化した後は不要となるのでGameObject.DestroyImmediate()
で削除しておきましょう。
この状態で作成処理を実行すると、コンポーネントとともにプレハブが作成されるようになりました。
手順4. プレハブへコンポーネントを追加する
さて、手順3でプレハブは作成できましたが、手順2で作成したコンポーネントをまだ追加できていません。
ここで少し工夫が必要となります。この手順を進めるためにはさらに2点の対応が必要となりました。
- 動的に作成されたコンポーネントの型を特定し、コンポーネントを追加する
- コンパイルが終了してからコンポーネントを取得する
動的に作成されたコンポーネントの型を特定し、コンポーネントを追加する
ViewCreateWizard
で動的に作成された画面制御用のコンポーネントをプレハブへ追加したいのですが、動的に作成されるコンポーネントの型を直接参照することができないため、ジェネリックで追加するコンポーネントの型を指定するGameObject.AddComponent<T>()
は利用できません。
そこで今回は、Type
オブジェクトを引数にしてコンポーネントの追加が行えるGameObject.AddComponent(Type componentType)
を使用していこうと思います。Type
オブジェクトはクラス名の文字列から取得する想定です。このアプローチであれば、コンポーネントの型を直接参照できなくても、指定した型のコンポーネントの追加を実現できます。
(なお、同様にタイプ名文字列からコンポーネントの追加を行えるGameObject.AddComponent(string className)
も存在しますが、こちらは非推奨になっているため採用しません)
以下のコードではAssembly
クラスを利用して、クラス名をキーにType
オブジェクトを取得しています。
OnWizardCreate()
メソッドに追記しています。
var gameObject = new GameObject(viewName);
// Assembly-CSharpアセンブリからクラスを取得する。
var assembly = Assembly.Load("Assembly-CSharp");
var componentType = assembly.GetType(className);
gameObject.AddComponent(componentType);
この処理でならプレハブへコンポーネントの追加ができそうです。
しかし、実はこの時点では動的に作成されたコンポーネントのコンパイルが完了していないため、アセンブリからType
オブジェクトを取得することができません。コンポーネントの追加に**「AddComponent asking for invalid type」**の警告が発生して処理に失敗します。
コンパイルが終了してからコンポーネントを取得する
続いてはコンパイルが通ったあとにこれらのType
オブジェクトの取得処理がが実行されるように修正していきます。
主な方針は、以下です。
- コンパイルの完了後に
ViewCreateWizard
クラスが再読み込みされるので、その時に作成処理を再開する。 - クラスの再読み込み時にクラスに設定されたパラメータが破棄されてしまうので、EditorPrefsへ作成に関する情報を保存しておく。
まずは、ViewCreateWizard
クラスへInitializeOnLoad
属性を付与します。
これにより、再コンパイル完了後にstaticコンストラクタによる初期化処理が実行されるようになります。
staticコンストラクタ内ではプレハブ作成処理を再開するためのメソッド(ここではOnCompilationFinished()
メソッドとする)を呼び出すようにしておきます。
[InitializeOnLoad]
public class ViewCreateWizard : ScriptableWizard
{
static ViewCreateWizard()
{
OnCompilationFinished();
}
クラスの再読み込みが実施された時にクラス内のパラメータが破棄されてしまうので、作成処理の再開時に情報を引き継ぐためにあらかじめEditorPrefsへ作成に関する情報を逃がしておきます。
今回、画面の作成に必要な情報は画面名だけなので、そちらをEditorPrefs.SetString()
で保存しておきます。
void OnWizardCreate()
{
// 作成情報をEditorPrefsへ保存しておく。
EditorPrefs.SetString("CreatingViewName", viewName);
string className = $"{viewName}ViewController";
string path = $"{Application.dataPath}/{className}.cs";
var builder = new StringBuilder();
builder.AppendLine("using UnityEngine;");
builder.AppendLine();
builder.AppendLine($"public class {className} : MonoBehaviour");
builder.AppendLine("{");
builder.AppendLine("}");
File.WriteAllText(path, builder.ToString());
AssetDatabase.Refresh();
}
プレハブの作成処理を再開するメソッドは以下のように記述します。
上で提示したアセンブリからのType
オブジェクトの取得処理を利用し、コンポーネントをプレハブへ追加しています。
static void OnCompilationFinished()
{
if (!EditorPrefs.HasKey("CreatingViewName"))
{
return;
}
// 作成情報をEditorPrefsから読み込む。
var creatingViewName = EditorPrefs.GetString("CreatingViewName");
string className = $"{creatingViewName}ViewController";
var gameObject = new GameObject(creatingViewName);
var assembly = Assembly.Load("Assembly-CSharp");
var classType = assembly.GetType(className);
gameObject.AddComponent(classType);
var prefabPath = $"{creatingViewName}.prefab";
PrefabUtility.SaveAsPrefabAsset(gameObject, prefabPath);
GameObject.DestroyImmediate(gameObject);
// 作成が済んだので、EditorPrefsから削除する。
EditorPrefs.DeleteKey("CreatingViewName");
}
ここで改めてウィザードから作成を実行すると、
- コンポーネントのクラスの作成
- コンパイルを待機
- コンパイル終了後にプレハブの追加
が順に実行されます。
以下の画像のように、目的としていた動的に作成されたコンポーネントが追加された状態のプレハブを作成することができました!
まとめ
ScriptableWizard
はパラメータ入力に関するGUIの配置をサポートしてくれるので、エディタウインドウのGUI操作に関するコーディングを最低限で済ませることができます。その分、作成処理のコーディングへ集中することができるので、今回の例のように多少複雑な作成処理を実装したいときには有用な機能であると感じました。
これを機に今後もScriptableWizard
を使って量産作業の効率化を図っていきたいと思います。
明日は@nissy_gpさんです!