tanaka's Programming Memo

プログラミングについてのメモ。

UI BuilderによるEditorWindowのデザインと新規シーンを作るエディタ拡張

エディタ拡張機能を使うと手軽にUnityエディタに機能を追加することができます。エディタのウィンドウはUI Builderというビジュアルエディタで手軽に作ることができます。UI Builderを利用した簡単なエディタ拡張の導入と、新規シーンの作成に少々手間取ったのでその方法をまとめます。

UI Toolkitによるエディタ拡張の導入については以下のマニュアルが分かりやすいです。

docs.unity3d.com

目次

動作環境

このブログはUnity2021.3.4f1で操作しています。UI Builderは2019.4にはあったので、Unity2019.4.x以降のバージョンならおおよそ動くと思います。

やりたいこと

名前を指定してシーンを作成して、そのシーンにシーン名+Behaviourの名前のオブジェクトを作成します。シーンの作成先のフォルダーはダイアログを表示して選択できるようにします。

エディタウィンドウは、Toolsメニューから AM1 > Create New Scene を選択して表示するようにします。

エディタウィンドウにはシーン名の入力欄と Create ボタンを置きます。Createボタンはシーン名が入力されるまでは押せないようにします。

作りたいエディタウィンドウ

以上のようにエディタを拡張します.

エディタウィンドウに必要なファイルを生成

エディタウィンドウのためのC#スクリプトとUXMLファイルを作成します。

  • Projectウィンドウの + から UI Toolkit > EditorWindowを選択します

C#, UXML, USSの3種類のファイルを同時に作成することができます。UXMLはエディタウィンドウの構造、USSはスタイルを定義するものです。UXMLはHTML、USSはCSSと位置づけが近いものです。

EditorWindow作成ダイアログ

  • 今回はUSSは利用しないのでチェックを外します
  • C#欄にNewSceneEditorWindowと入力します。UXML欄も自動的に設定されます
  • Confirmボタンを押して、ファイルが作成されるまでしばらく待ちます

以上でEditorフォルダーが作成されて、その中に指定のファイル名でC#スクリプトとUXMLファイルが出力されます。

作成されたエディタスクリプトとUXML

作成されたC#スクリプトには確認用にメニュー呼び出しが実装されています。メニューで以下を選べば新規作成したエディタウィンドウを呼び出せます。

  • Windowメニュー > UI Toolkit > NewSceneEditorWindow

デフォルトのエディタウィンドウ

ウィンドウには、C#スクリプトで追加されたラベルとUXMLファイルで追加されたラベルが表示されます。

エディタウィンドウのデザイン

UI Builderでウィンドウのレイアウトを作ります。

  • ProjectウィンドウのEditorフォルダーからアイコンが</>になっている方のNewSceneEditorWindowをダブルクリックします

UXMLファイルをダブルクリックするとUI Builderウィンドウが開きます。Visual C#のようにデザインを確認しながらウィンドウを作ることができます。

UI Builderウィンドウ

エディタ機能を有効にする

UI Toolkitはランタイムでも使えて、エディタとは使える機能が違います。今回はエディタとして使うのでそのための設定をします。

  • UI Builderの左のHierarchyで NewSceneEditorWindow.uxml をクリックして選択します
  • UI Builderの右のInspectorの表示が NewSceneEditorWindow のものになるので、 Editor Extension Authoring 欄にチェックを入れます

「これ以降はこのウィンドウはエディタモードでしか実行できません」というような警告が表示されますが問題ないのでそのまま進めます。この設定をすることで、ウィンドウ左下のLibraryにエディタで使えるコントロールが追加されます。

画面を編集する

最初のラベルは不要なので消します。

  • Viewportで「Hello World! From UXML」と書いてあるラベルをクリックして選択します
  • Deleteキーを押して削除します

次にシーン名の入力欄を追加して設定します。

  • ウィンドウ左下のLibraryから Text Field をダブルクリックします。Viewport上にテキストボックスが追加されます
  • Inspectorで以下を設定します
    • Name欄にSceneNameと入力します
    • Label欄にシーン名と入力します
    • Value欄とText欄に入力されている文字を消して空欄にします

SceneName欄

これでシーン名の入力欄はできあがりです。同様にCreateボタンを作ります。

  • Library欄からButtonをダブルクリックして追加します
  • Inspectorで以下を設定します
    • Name欄にCreateButtonと入力します
    • Text欄にCreateと入力します

以上でエディタウィンドウは完成です。Ctrl + Sキーを押して保存したら、UI Builderウィンドウは閉じて構いません。

コントロールに機能を仕込む

Visual C#のようにUI Builderから直に機能を仕込むことはできません。先に作成したC#スクリプトを開いて実装します。

呼び出しメニューの設定

呼び出すメニューを予定の場所に変更します。

  • Projectウィンドウの Assets > Editorフォルダー内の NewSceneEditorWindowC#スクリプトをダブルクリックして開きます
  • メニューはMenuItem属性で編集できます。ShowExample()メソッドの上にある Window/UI Toolkit/NewSceneEditorWindow を呼び出しているMenuItemを変更します。ついでに ShowExample() という名前をShowNewSceneWindow()に変更します
    // 9:
    [MenuItem("Tools/AM1/Create New Scene")]
    public static void ShowNewSceneWindow()

上書き保存してUnityに戻ると、先ほどあった Windowメニューの UI Toolkit > NewSceneEditorWindow メニューが消えています。代わりに Tools メニューが追加されて、 AM1 > Create New Scene と選べるようになります。Create New Sceneを選べば UI Builder で作成したエディタウィンドウが表示されます。

エディタウィンドウの呼び出し

コントロールの状態管理とイベント登録

シーン名が入力されていない時はCreateボタンを無効にしたり、Createボタンを押した時にCreateScene()メソッドを呼び出すようにスクリプトを追加します。追加先はCreateGUI()メソッドです。

サンプルが用意したラベルを追加する以下の3行は不要なので消します。

// 21:
        // VisualElements objects can contain other VisualElement following a tree hierarchy.
        VisualElement label = new Label("Hello World! From C#");
        root.Add(label);

管理しやすいようにコントロールインスタンスを取得しておきます。9行目付近にインスタンス変数として以下を定義します。

// 9:
    TextField sceneNameText;
    Button createButton;

CreateGUI()メソッドの最後にあるroot.Add(labelFromUXML);の下に以下を追加します。

// 29:
        // コントロールのインスタンスを取得
        sceneNameText = root.Query<TextField>("SceneName").First();
        createButton = root.Query<Button>("CreateButton").First();

rootVisualElementのインスタンスからQueryを使ってコントロールインスタンスを取得することができます。このメソッドは結果をリストで返すので、最初の1つだけ取り出すために最後にFirst()を付けています。

NewSceneEditorWindowクラス内に以下のメソッドを追加します。

// 34:
    /// <summary>
    /// コントロールの状態を更新。
    /// </summary>
    void UpdateControl(InputEvent ievt)
    {
        createButton.SetEnabled(!string.IsNullOrEmpty(sceneNameText.text));
    }

    /// <summary>
    /// 新規シーンの作成
    /// </summary>
    void CreateScene(ClickEvent cevt)
    {
        Debug.Log($"シーン{sceneNameText.text}を作成する予定");
    }

UpdateControl()メソッドは、シーン名が未入力かどうかでCreateボタンの有効か無効化を切り替える処理です。CreateScene()メソッドにはあとでシーンの作成処理を実装することにして、とりあえずログにメッセージを表示しておきます。

メソッドができたらそれらを登録します。CreateGUI()メソッドに追加したインスタンスを取得するコードの下に以下を追加します。

// 33:
        // コントロールの状態を更新
        UpdateControl(null);

        // 処理を登録
        sceneNameText.RegisterCallback<InputEvent>(UpdateControl);
        createButton.RegisterCallback<ClickEvent>(CreateScene);

以上できたら上書きしてUnityへ切り替えます。NewSceneEditorWindowを表示するとCreateボタンが無効になります。シーン名を入力するとCreateボタンが押せるようになり、ボタンを押すとログにメッセージが表示されるようになります。

イベント登録

以上でウィンドウの動きが実装できました。

コントロールごとのRegisterCallbackで使えるイベントについて

UI Toolkitには様々なコントロールがありますが、各コントロールでどのイベントが登録できるかをどこで調べればよいかが分かりませんでした。

困った時はChangeEvent<>に扱う型をジェネリックで渡せばよさそうです。EnumFieldの変更を受け取りたい時に、ChangeEvent<System.Enum>とすることで登録することができました。

どこかに資料がまとまっていたらご教示いただけると助かります。

シーン作成

本丸であるシーン作成をCreateScene()メソッドに実装します。

保存先のフォルダーを選択

シーンの保存先のフォルダーを選択します。フォルダーの選択にはEditorUtility.SaveFolderPanel()を使います。フォルダーを選択すると選択したパスが文字列で返され、キャンセルしたら空文字が返されます。空文字が返されたら何も処理せずに戻るようにしておきます。

先に追加したCreateScene()メソッド内のDebug.Log()を削除して、以下のコードを追加します。

// 52:
            // フォルダー選択
            string folder = EditorUtility.SaveFolderPanel("保存先フォルダー", "" , "");
            if (string.IsNullOrEmpty(folder)) return;
            createButton.SetEnabled(false);

オブジェクトやシーンの作成中にアセットの再読み込みなどが発生するため、ボタンが押せる状態のままだと何かと問題が起きがちです。処理を始める前にボタンを無効にしています。

Unityのエディター拡張で新規シーンを保存しようとするとエラーが出る

シーン作成の時に苦戦したのがこの部分でした。当初は保存先とシーン名を指定するためにEditorUtility.SaveFilePanel()EditorUtility.SaveFilePanelInProject()を試したのですが、保存先を指定すると書き込み権限がないというようなエラーが出ることがあり、動作が不安定になりました。作成するシーン名は予め入力しているので、保存先のフォルダーさえ分かればいいということでSaveFolderPanel()に変更してSaveScene()メソッドで保存することで安定して動くようになりました。

新規シーンの作成

新規シーンはEditorSceneManager.NewScene()メソッドで作成できます。このメソッドを利用するためにスクリプトファイルの上の方に以下のusingを追加します。

// 5:
using UnityEditor.SceneManagement;

フォルダーの選択処理に続けて以下を追加します。

// 57:
            // 新しいシーンを作成
            var newScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);

空のシーンを追加シーンとして作成するコードです。引数でデフォルトオブジェクトを配置したり、現在のシーンを解放して作成したシーンのみを開くこともできます。作成したシーンのインスタンスは保存時に使うのでnewSceneに代入しておきます。

テンプレートからシーンを作成する方法はコガネブログさんの以下の記事が分かりやすいです。

baba-s.hatenablog.com

オブジェクトの作成

オブジェクトを作成します。上のコードに続けて以下を追加します。

// 60:
        // オブジェクト作成
        var go = new GameObject();
        go.name = sceneNameText.text+"Behaviour";
        Undo.RegisterCreatedObjectUndo(go, $"Created {go.name} Object.");

new GameObject();を実行すると、空のゲームオブジェクトがアクティブシーンに作られます。先にNewScene()で作成したシーンは自動的にアクティブシーンになっているので、このままで新しく作成したシーンに作ったゲームオブジェクトが配置されます。

オブジェクトを作成したことをUndoのシステムに登録するためにUndo.RegisterCreatedObjectUndo()を呼び出します。これによりオブジェクトの作成をUndoしたり、シーンが更新されたことをシステムが把握できます。

新規にオブジェクトを作成するのではなく、プレハブを読み込んで配置する場合はおもちゃラボさんの以下のブログをご覧ください。

nn-hokuson.hatenablog.com

シーンの保存

作成したゲームオブジェクトにAddComponent()で必要なコンポーネントをアタッチしたり、その他のオブジェクトやプレハブを配置してシーンを完成させたら保存します。シーンはEditorSceneManager.SaveScene()で保存します。保存先のフォルダーは作成済みである必要があります。またフォルダーはプロジェクトフォルダーからの相対パスで指定します。

Pathを利用するためにスクリプトファイルの上の方に以下のusingを追加します。

// 6:
using System.IO;

オブジェクトの作成後に以下を追加します。

// 66:
        // シーンの保存
        string scenePath = Path.Combine(folder, sceneNameText.text + ".unity");
        var relPath = "Assets/" + Path.GetRelativePath(Application.dataPath, scenePath);
        var path = AssetDatabase.GenerateUniqueAssetPath(relPath);
        EditorSceneManager.SaveScene(newScene, path);
        AssetDatabase.Refresh();

        sceneNameText.value = "";

選択したフォルダーとシーン名に.unity拡張子をくっつけて保存先のパスをscenePathに代入します。SaveScene()はプロジェクトフォルダーからの相対パスを指定しますが、scenePath絶対パスなのでApplication.dataPathからの相対パスを取得して、先頭にAssets/をくっつけた相対パスrelPathに代入します。シーン名が重なっていても問題が起きないようにAssetDatabase.GenerateUniqueAssetPath()を使ってファイル名を調整した後、シーンのインスタンスと作成したパスを指定してシーンを保存しています。最後に作成したシーンをエディタに認識させるためにAssetDatabase.Refresh()を実行して、シーン名を空にして完了です。

保存してUnityに切り替えれば作成したエディタ拡張が動作します。

まとめ

UI Builderでエディタウィンドウを作成して、UI ToolkitのC#スクリプトで機能を実装しました。また、保存先のフォルダーを選択して自動で新規シーンを作成してオブジェクトを配置するコードを紹介しました。

エディタ拡張については以下のドキュメントでざっと基本は把握しました。

docs.unity3d.com

UI Builderの始め方やコードでの呼び出し方、シーンの保存など、ドキュメントを探すのに時間がかかったあたりをこの記事でまとめました。現在、これらを使って自家用フレームワークを手軽に組み込めるエディタ拡張を開発しています。

参考・関連URL

今回作成したコード