4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PONOSAdvent Calendar 2020

Day 2

ScriptableWizardからコンポーネントとプレハブを同時に作成する例

Last updated at Posted at 2020-12-01

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」ボタンをクリックすることで、
作成するもの-1.png
以下のようにプレハブとそれを制御するためのコンポーネントが作成されます。
作成するもの-2.png

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");
    }
}

このコードで作成されるウインドウがこちらです。
手順1.png

とてもシンプルなコードですが、簡単に作成ウィザードのエディタウインドウを作成することができました。
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」ボタンをクリックすると、
手順2-1.png
以下のようなファイルが作成されるようになりました。
手順2-2.png

また、ゲームオブジェクトの「Add Component」から選択することも可能です。
手順2-3.png

手順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()で削除しておきましょう。

この状態で作成処理を実行すると、コンポーネントとともにプレハブが作成されるようになりました。
手順3.png

手順4. プレハブへコンポーネントを追加する

さて、手順3でプレハブは作成できましたが、手順2で作成したコンポーネントをまだ追加できていません。
ここで少し工夫が必要となります。この手順を進めるためにはさらに2点の対応が必要となりました。

  1. 動的に作成されたコンポーネントの型を特定し、コンポーネントを追加する
  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");
}

ここで改めてウィザードから作成を実行すると、

  1. コンポーネントのクラスの作成
  2. コンパイルを待機
  3. コンパイル終了後にプレハブの追加

が順に実行されます。
以下の画像のように、目的としていた動的に作成されたコンポーネントが追加された状態のプレハブを作成することができました!
手順4.png

まとめ

ScriptableWizardはパラメータ入力に関するGUIの配置をサポートしてくれるので、エディタウインドウのGUI操作に関するコーディングを最低限で済ませることができます。その分、作成処理のコーディングへ集中することができるので、今回の例のように多少複雑な作成処理を実装したいときには有用な機能であると感じました。
これを機に今後もScriptableWizardを使って量産作業の効率化を図っていきたいと思います。

明日は@nissy_gpさんです!

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?