【Unity】エディター拡張で、読み込むアセットのパスをハードコードしないために【Editor Window】

最近のバージョンの Unity ではエディター拡張向けの色々な機能が追加され、エディター拡張がだいぶ作りやすくなりました。
エディターウィンドウの画面配置は `Visual Elements`、`UI Toolkit` などで UGUI のように組めて、C# のコードは処理に専念できるようになりました。

しかし、それでもエディターウィンドウは使いづらい部分はあります。
具体的に、`MonoBehaviour` と比べて、エディターウィンドウは<font color="#3333FF">「`[SerializeField]` をつけて、生成時のデフォ値を簡単で任意操作」</font>することはできないため、簡単に初期アセットや初期数値を設定できず、少し使いづらい印象がまだ残っています。

この記事では簡単なエディターウィンドウを作りながら、上記の問題を `ScriptableObject` を利用して改善する手法を紹介します。

# 前書き
この記事では Unity 2021.3.0f1 LTS を使用しています。

# 1. エディターウィンドウを作る

エディターウィンドウの制限を説明するために、最近の機能である「UI Toolkit」を使用して、簡単なエディターウィンドウを作ります。

Project ウィンドウを右クリックして、`Create > UI Toolkit > Editor Window` を順に選択します。
![エディターウィンドウ作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295675/d54b4d81-5304-4dae-a796-eabbd93c8ddc.png)

すると３つのファイルが出てきます：
![ファイル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295675/6ef2ebbd-05b9-0e15-20ca-bbd72a7d6958.png)
それぞれエディターウィンドウの
- UI 配置アセット（uxml）
- UI スタイルシート（uss）
- エディターの処理を書くコードファイル（cs）

です。

この記事は UI Toolkit の使い方がメインではありませんので、ここでは関連解説を省略します。
（この状態ではすでに、`Window > UI ToolKit > TestWindow` で、作ったエディターウィンドウを起動できます）
　![TestWindow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295675/0b95a8fa-8266-fcbf-35a6-dbb261a7acd5.png)

---

生成された C# ファイルを見てみましょう。
C# ファイルを見てみると、下記のような方法で UI を表示する uxml を読み込んでいることが分かります：

![パスハードコード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295675/bdb865c9-a09c-eead-4e74-4b3d0eb14d61.png)

なんと、**UI アセットのパスがハードコートされています**。

パスをハードコートをしても、ちゃんとロードできれば処理的に問題はありませんが、
パス直書きのデメリットとしては、該当アセットを移動したり 名前を変えたりすると、アセットをロードできなくなり、エディターウィンドウが壊れやすくなります。

上記のデメリットがありますので、出来ればパスをハードコートをせずに書きたいです。

# 2. アセットを [SerializeField] でアタッチする

この記事の最初で<font color="#3333FF">「EditorWindow は MonoBehaviour のように `[SerializeField]` がつけて、デフォ値を設定することができない」</font>と書いていましたが、
実はちょっとやり方と制限が違うだけで、
**エディターウィンドウでも `[SerializeField]` を介して、アセットをアタッチすることが可能** です。

例に、下記のコードを書いて、プロジェクトで TestWindow.cs を選んでみると、
```TestWindow.cs
public class TestWindow : EditorWindow
{
    // uxml の C# クラス
    [SerializeField] private VisualTreeAsset visualTreeAsset;

    // ...
}
```

![SerializeField.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295675/daf5b77a-d2b5-5c3f-a1bc-b8b8a96639be.png)

なんと、スクリプト自身にアタッチ可能なフィールドが表示されました。

**これは「Default Reference」というもので、エディターウィンドウが Instantiate された時に、ここに設定された値が自動的に代入されます。**
EditorWindow に限らず、MonoBehaviour でも同じことが可能で、Editor でエディターウィンドウや `MoneBehaviour` のインスタンスの生成時に、自動的に代入されます。（※`AddComponent()` など、Editor を介さずに生成したものには代入されません）

この方法を使えば、
（`MonoBehaviour` のように、各 Instance で別々のデフォ値を持つことは出来ませんが、）
パス直書きの代わりにエディター上でアセットをアタッチすることが出来、ハードコードの部分を消すことが出来ました。


```TestWindow.cs
public class TestWindow : EditorWindow
{
    [SerializeField] private VisualTreeAsset visualTreeAsset;

    public void CreateGUI()
    {
        VisualElement root = rootVisualElement;
        // Import UXML
        // アセットパスのハードコートが不要！
        // var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Test/TestWindow.uxml");
        var uxml = visualTreeAsset.Instantiate();
        root.Add(uxml);
    }

    [MenuItem("Window/UI Toolkit/TestWindow")]
    public static void ShowExample()
    {
        TestWindow wnd = GetWindow<TestWindow>();
        wnd.titleContent = new GUIContent("TestWindow");
    }
}

```

アタッチ結果→
![UIアセットアタッチ成功.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295675/ba9ce6fe-f0a6-b023-0233-c98901f611bf.png)

## 2.1. デフォ値の制限

しかし、デフォ値の設定ができるクラスの Type に制限があります。
試しに同じ方法を倣って `[SerializeField] int intField;` を書いても、`VisualTreeAsset` のようには表示されませんでした。

どうやら **デフォ値が設定できるクラスは `Unity.Object` から継承したクラスのみ** のようで、int や通常の自作クラスなどはアタッチ出来ません。

```C#
    // 表示され、アタッチできる
    [SerializeField] private VisualTreeAsset visualTreeAsset;

    [SerializeField] private GameObject defaultPrefab;
    [SerializeField] private Sprite defaultSprite;

    // 表示されない
    [SerializeField] private int defaultIntValue;
    [SerializeField] private TestPlainClass defaultTestPlainClass; // 自作クラス
```

# 3. SciptableObject を利用して、制限を回避

`Unity.Object` を継承したものであれば付けられますので、
上記の「`int` 型 field が設定できない」問題は　**`Unity.Object` を継承している `ScriptableObject` を利用すれば解決できます**。

具体的なやり方は、<font color="#3333FF">「`[SerializeField] int` を ScriptableObject に書いて、該当 ScriptableObject をエディターウィンドウにアタッチ」</font>することです。

エディターウィンドウは ScriptableObject を介して int にアクセスできますので、
実質エディターウィンドウに int 型のデフォ値が設定できます。

下の例で、`TestWindowSettings` という名の ScriptableObject を作って、TestWindow で使用する予定の num デフォ値を保存しています。

```TestWindowSettings.cs
[CreateAssetMenu(fileName = "TestWindowSettings", menuName = "Editor Window Settings/Test Window Settings")]
public class TestWindowSettings : ScriptableObject
{
    public int num;
}
```

```TestWindow.cs
public class TestWindow : EditorWindow
{
    [SerializeField] private VisualTreeAsset visualTreeAsset;
    [SerializeField] private TestWindowSettings settings;

    public void CreateGUI()
    {
        // TestWindowSettings を介して、num にアクセスできる！
        var defaultNum = settings.num;

        // ... 後続処理
    }
}
```

![ScriptableObjectを介して色々をシリアライズする.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/295675/2a91a9bc-d004-7bd8-37b1-3f431e74af0e.png)

`ScriptableObject` を作ってアタッチすることで、
VisualTreeAsset のみならず、数値や画像ファイル、その他自作クラスなどでも簡単にデフォ値としてロードすることができます。

また、ScriptableObject の各値を調整して、エディターウィンドウを再度開けばすぐ反映されますので、
ハードコードのように再コンパイルの時間を待つ必要はありません。

## 3.1. ScriptableObject を利用した、エディターウィンドウのプリセット

ScriptableObject が大量に作れることを利用すれば、様々なデフォ値のプリセットが作れます。

使い方として、使用したい ScriptableObject プリセットを使用前にアタッチすれば、エディターウィンドウ起動時はそちらをロードするようになりますので、違うバリエーションのエディターウィンドウが起動できます。

# 制限

時々 Default Reference をアタッチしても、前回のキャッシュが残っていることがあるようで、
変な値が入っているのであれば、Unity Editor を再起動してみましょう。

しかし ScriptableObject を利用すれば、ScriptableObject にある値・アセットを調整すれば良くて、エディターウィンドウに追加でアタッチすることはなくなりますので、そんなに気になることはありません。

# 結論
エディターウィンドウは `MonoBehaviour` のように柔軟にデフォ値設定ができませんが、`ScriptableObject` をアタッチすれば近いことが出来ますので、エディターウィンドウの開発がだいぶ捗るようになるかと思います。
機能もりもりのエディターウィンドウを作る際に、是非上記の手法を利用してください。

それでは、良いエディター拡張ライフを！

　
<font color="#939393">※ 節 2 で、「生成された C# ファイルにアセットパスが直書きされている」と書いていますが、最新の Unity 2022.2 では自動的に `[SerializeField] VisualTreeAsset` が付けられて、エディター上でアタッチ済みの状態で生成されます