エディタ拡張機能 を使うと手軽に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フォルダー内の NewSceneEditorWindow
のC# スクリプト をダブルクリックして開きます
メニューはMenuItem
属性で編集できます。ShowExample()メソッドの上にある Window/UI Toolkit/NewSceneEditorWindow を呼び出しているMenuItemを変更します。ついでに ShowExample() という名前をShowNewSceneWindow()
に変更します
[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行は不要なので消します。
VisualElement label = new Label("Hello World! From C#" );
root.Add(label);
管理しやすいようにコントロール のインスタンス を取得しておきます。9行目付近にインスタンス 変数として以下を定義します。
TextField sceneNameText;
Button createButton;
CreateGUI()
メソッドの最後にあるroot.Add(labelFromUXML);
の下に以下を追加します。
sceneNameText = root.Query<TextField>("SceneName" ).First();
createButton = root.Query<Button>("CreateButton" ).First();
rootVisualElementのインスタンス からQueryを使ってコントロール のインスタンス を取得することができます。このメソッドは結果をリストで返すので、最初の1つだけ取り出すために最後にFirst()
を付けています。
NewSceneEditorWindowクラス内に以下のメソッドを追加します。
< 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()
メソッドに追加したインスタンス を取得するコードの下に以下を追加します。
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()
を削除して、以下のコードを追加します。
string folder = EditorUtility.SaveFolderPanel("保存先フォルダー" , "" , "" );
if (string .IsNullOrEmpty(folder)) return ;
createButton.SetEnabled(false );
オブジェクトやシーンの作成中にアセットの再読み込みなどが発生するため、ボタンが押せる状態のままだと何かと問題が起きがちです。処理を始める前にボタンを無効にしています。
Unityのエディター拡張で新規シーンを保存しようとするとエラーが出る
シーン作成の時に苦戦したのがこの部分でした。当初は保存先とシーン名を指定するためにEditorUtility.SaveFilePanel()
とEditorUtility.SaveFilePanelInProject()
を試したのですが、保存先を指定すると書き込み権限がない というようなエラーが出ることがあり、動作が不安定になりました。作成するシーン名は予め入力しているので、保存先のフォルダーさえ分かればいいということでSaveFolderPanel()
に変更してSaveScene()
メソッドで保存することで安定して動くようになりました。
新規シーンの作成
新規シーンはEditorSceneManager.NewScene()
メソッド で作成できます。このメソッドを利用するためにスクリプト ファイルの上の方に以下のusingを追加します。
using UnityEditor.SceneManagement;
フォルダーの選択処理に続けて以下を追加します。
var newScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
空のシーンを追加シーンとして作成するコードです。引数でデフォルトオブジェクトを配置したり、現在のシーンを解放して作成したシーンのみを開くこともできます。作成したシーンのインスタンス は保存時に使うのでnewScene
に代入しておきます。
テンプレートからシーンを作成する方法はコガネブログさんの以下の記事が分かりやすいです。
baba-s.hatenablog.com
オブジェクトの作成
オブジェクトを作成します。上のコードに続けて以下を追加します。
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を追加します。
using System.IO;
オブジェクトの作成後に以下を追加します。
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
今回作成したコード