初めての自作Unityエディタ拡張

この記事は[NTTコムウェア Advent Calendar 2022](https://qiita.com/advent-calendar/2022/nttcomware) 20日目の記事です。

本記事ではエディタ拡張を自作してみたいけど難しそうという人向けに、例題に沿って簡単なUnity拡張の作り方を説明します。

UI Builderというものを使いますので比較的新しいUnityが必要です。
2021以降からコア機能となっていて、それ以前(2019.4～)はpackage installが必要だそうです。

https://docs.unity3d.com/Packages/com.unity.ui.builder@1.0/manual/index.html

本記事作成の環境はUnity Versionは2022.1.5f1.3268です。
## 例題
あるモデルのMesh RendererのMaterialを、他3つのモデルに一括コピーする事を目的とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/27eb5cba-a220-eb18-ec8b-9deb5ad03d23.png)

## 手順
1. ウィンドウを作る
2. ウィンドウの中身を作る
3. 処理を書く
4. 動作確認する

## ウィンドウを作る
ウィンドウを作ってみましょう。
Assets直下に適当なフォルダを作り、そのフォルダの中にEditorというフォルダを作ります。
Editorフォルダ内で新しいC# Scriptを作成します。頭を大文字にしてまた適当な名前をつけましょう。
![create_script.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/045c1d40-378f-ccdd-052d-8844f62b70e2.png)
![new_file.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/72cb900c-3fe4-8fcb-bdad-c4c1e8ece687.png)

C# Scriptファイルを作ったらダブルクリックして開きましょう。エディタ関連付けを済ませてない人はこちらなどを参考にして関連付けしましょう。

[UnityのC#エディタをVsCodeにして使う設定【Windows編】2022年8月](https://dianxnao.com/unity%e3%81%aec%e3%82%a8%e3%83%87%e3%82%a3%e3%82%bf%e3%82%92vscode%e3%81%ab%e3%81%97%e3%81%a6%e4%bd%bf%e3%81%86%e8%a8%ad%e5%ae%9a%e3%80%90windows%e7%b7%a8%e3%80%912022%e5%b9%b48%e6%9c%88/)

開いたら以下のような感じでコードを書きます。
```c#:Tomato.cs
using UnityEngine;
using UnityEditor;

public partial class Tomato : EditorWindow {

    [MenuItem("TOMATO/まとめてコピー")] // ヘッダメニュー名/ヘッダ以下のメニュー名
    private static void ShowWindow() {
        var window = GetWindow<Tomato>("UIElements");
        window.titleContent = new GUIContent("tomato"); // エディタ拡張ウィンドウのタイトル
        window.Show();
    }
}
```

エディタからUnityに戻るとメニューが増えています。クリックしてみましょう。
![added_menu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/e6ab28fd-5ff2-5c2c-8840-bf98c590468e.png)
![empty_window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/d9192816-da47-4ddb-e975-08868c4db1c4.png)

空の画面が表示されます。第一歩です！

## ウィンドウの中身を作る
UI Builderで部品をぺたぺた貼る感じで画面を作れます。

https://docs.unity3d.com/ja/2021.2/Manual/UIBuilder.html

ヘッダメニューのWindow -> UI Toolkit -> UI Builderを開きます。
![UI_Builder_Menu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/dd86d37e-bdca-f839-030a-6e3bab13e779.png)

開いたらUI Builder画面内のLibrary右上メニューからEditor Extension Authoringにチェックを付けましょう。UI Builderはエディタ以外の画面を作るのがメインなのでエディタ用部品は最初非表示になっています。
![Editor_Extension_Authoring.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/2e685975-b4a3-ef6c-1d67-0b2b74d2bacf.png)

あとは文字を表示するためのラベルとモデルを受け取るためのObject Fieldをドラッグ＆ドロップでぺたぺたしましょう。
![window1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/d50f1b16-8b72-70e6-5787-fbe95a2cbbf4.png)

LabelのText、Object FieldのLabelを編集しましょう。
![window2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/207bee79-2b8a-d9c6-bdd8-2f577682e5f5.png)

多分出来たと思うので先程作ったEditor内に保存しましょう。UI Builder内のViewportのFileメニューからUxml(構造情報)を、StyleSheetsの＋マークメニューからUss(見た目情報)を保存します。名前は適当につけてください。
![save.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/27184b45-b739-a00a-4459-ee3d89bcb02f.png)

![utml_uss.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/b8135ec9-2f4b-2a95-9ccc-e38ec5a3b6df.png)

次はUxmlとUssを先程のウィンドウに関連付けしましょう。先程のc# Scriptを開いて追記しましょう。```using UnityEngine.UIElements;```が追加されているのに気をつけて下さい。

```c#:Tomato.cs
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements; // 追加

public partial class Tomato : EditorWindow {

    [MenuItem("TOMATO/まとめてコピー")]
    private static void ShowWindow() {
        var window = GetWindow<Tomato>("UIElements");
        window.titleContent = new GUIContent("tomato");
        window.Show();
    }

    // 以下追加
    [SerializeField] private VisualTreeAsset _rootVisualTreeAsset;
    [SerializeField] private StyleSheet _rootStyleSheet;

    private void CreateGUI() {
        _rootVisualTreeAsset.CloneTree(rootVisualElement);
        rootVisualElement.styleSheets.Add(_rootStyleSheet);
    }
}
```

Unityで先程のC# ScriptをクリックするとRoot Visual Tree AssetとRoot Style Sheetが設定できるようになっているのでUxmlとUssをドラッグ＆ドロップして設定しましょう。
![set_uxml_uss.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/fbad14f5-9dc7-dc82-c615-dc7b17abbb40.png)

もう一度メニューを選ぶと作った画面が開かれます。
![window3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/838ac358-b217-6fe3-48dd-a3f55c9309bb.png)

## 処理を書く
次は処理を書きましょう。
各フィールドやボタンを指定するための識別子が欲しいのでまたUI Builderで設定します。（ついでにボタン忘れてたのでボタンも追加しましょう）
Uxmlファイルを選択してInspectorにあるOpenボタンをクリックしてUxmlをUI Builderで開きましょう。
![open_uxml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/39134bdd-2bf2-48a0-31b5-73afee124c55.png)

それぞれの部品を選択してInspectorからNameを設定しましょう。
![set_name.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/c9dcf34c-2e12-26fd-c086-86d63e0d7e11.png)

また、オブジェクトを設定できるように全てのObject FieldのTypeにGame Objectを設定します。
![set_type.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/b6a04acc-83c6-e195-d0f0-e69011ca5f76.png)

次はつけたNameを使って処理を書いていきます。```using UnityEditor.UIElements;```が追加されているのに気をつけて下さい。

```c#:Tomato.cs
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements; // 追加

public partial class Tomato : EditorWindow {

    [MenuItem("TOMATO/まとめてコピー")]
    private static void ShowWindow() {
        var window = GetWindow<Tomato>("UIElements");
        window.titleContent = new GUIContent("tomato");
        window.Show();
    }

    [SerializeField] private VisualTreeAsset _rootVisualTreeAsset;
    [SerializeField] private StyleSheet _rootStyleSheet;

    private void CreateGUI() {
        _rootVisualTreeAsset.CloneTree(rootVisualElement);
        rootVisualElement.styleSheets.Add(_rootStyleSheet);

        // 以下追加
        var copyButton = rootVisualElement.Q<Button>("copy_btn");
        copyButton.clicked += () => {
            var moto1 = (GameObject)rootVisualElement.Q<ObjectField>("moto1_obj").value;
            var saki1 = (GameObject)rootVisualElement.Q<ObjectField>("saki1_obj").value;
            var saki2 = (GameObject)rootVisualElement.Q<ObjectField>("saki2_obj").value;
            var saki3 = (GameObject)rootVisualElement.Q<ObjectField>("saki3_obj").value;
            saki1.GetComponent<Renderer>().sharedMaterial = moto1.GetComponent<Renderer>().sharedMaterial;
            saki2.GetComponent<Renderer>().sharedMaterial = moto1.GetComponent<Renderer>().sharedMaterial;
            saki3.GetComponent<Renderer>().sharedMaterial = moto1.GetComponent<Renderer>().sharedMaterial;
        };
    }
}
```

```rootVisualElement.Q<ObjectField>("付けたName")```で部品を指定できます。それぞれの部品がどんなプロパティを持つのかはリファレンスを見ましょう。

https://docs.unity3d.com/ScriptReference/UIElements.ObjectField.html

Materialをコピーする処理はただ上書きしてるだけです。

## 動作確認する
多分出来たと思うので動かしてみましょう。

HierarchyでCubeを作成してCtrl + Dで複製して適当に移動させます。
![create_cube.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/504373b3-49bf-148e-ff3e-95e06a343649.png)
![cubes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/103089b2-f06b-d09c-78f4-068515615cfe.png)

コピー元となるCubeに設定するMaterialを作成してドラッグ＆ドロップでコピー元にセットします。
![create_material.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/ce70eff3-a889-11e2-ec94-8a671b650e04.png)
![red_material.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/763ea323-f783-652a-62ec-be327b4de1ec.png)
![set_material.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/453e3227-27be-8479-f784-d62a733959a3.png)

作ったツールを開いてコピー元、コピー先のCubeをドラッグ＆ドロップでセットします。
![test1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/761e9064-78cd-4484-524f-4cd62ebe527a.png)

ボタンを押した時にコピー先3つにMaterialが反映されれば成功です。
![test2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074498/207a5d4f-e2cf-4d29-e177-a67b3e22a340.png)

## おわりに
お疲れ様でした！
思ったよりも簡単に、また少ないコードでエディタ拡張が作れたのではないでしょうか。Unity作業は地道な繰り返し作業が多かったりするので少し頑張って自分のための拡張を作ってみてはいかがでしょうか。