[Unity Visual Scripting] 自作ユニットの見た目をカスタマイズする

# 自作ユニットのインスペクターをカスタマイズする
自作ユニット作成 ～ ポート追加 ～ ポート情報追加 ～ といった基本的なところは[公式リファレンス](https://docs.unity3d.com/ja/Packages/com.unity.visualscripting@1.8/manual/vs-creating-visual-script-graph-node.html)に丁寧に書かれている。ここではその他のカスタマイズを逆引き的に置いておく。

Visual Scripting では Graph Inspector という独自のインスペクターに情報が表示される。カスタマイズには`Editor`クラスではなく Visual Scripting で独自に実装されている属性やクラスを使用する。

## メンバーをインスペクターに表示する
~~~cs
public class MyUnit : Unit {
    [Inspectable]
    string MyMember;
}
~~~
`InspectableAttribute()`
プライベートなメンバーでもOK

## 条件付きで表示する
~~~cs
[InspectableIf(nameof(IsVisible))]
string MyMember;

bool IsVisible = false;
~~~
`InspectableIfAttribute(string conditionMember)`
第一引数に条件となるメンバー名を指定する
`nameof`式を使うと楽、動的には反映されないみたい

## 値を保存（シリアライズ）させる
`SelizalizeAttribute()`
プライベートなメンバーでもOK
`SerializeAsAttribute(string name)`
シリアライズする名前を指定する
プロパティのプライベートフィールドとか
`DoNotSerializeAttribute()`
シリアライズさせない、ポートのメンバーとかに必要

## ツールチップ、アイコンを追加する
`InspectorLabelAttribute(string text, string tooltip, {Texture icon})`
ラベル、ツールチップ、アイコンを指定する
`InspectorExpandTooltipAttribute()`
ツールチップをマウスオーバーではなくラベル下に常に表示させる

## テキストエリアを複数行にする
`InspectorTextAreaAttribute({float maxLines, float minLines})`
最小行数と最大行数を指定できる、なぜかfloat型

## 入力エリアを幅いっぱいにする
`InspectorWideAttribute(bool toEdge)`
ラベルが非表示になる
toEdge による変化はよくわからない、型によっては変わるかも

## 保存タイミングを遅らせる
`InspectorDelayedAttribute()`
エンター押したときやフォーカス外れたときにデータが更新される

## スライダーを使う
`InspectorRangeAttribute(float min, float max)`
最小値と最大値を指定する

## トグルを左側に表示する
`InspectorToggleLeftAttribute()`
あまり見栄えしない

## IMGUIでカスタマイズする
Graph Inspector にはユニット名などが表示されるヘッダー部、メンバーのインスペクターが表示される部分、ポートの情報が表示されるフッター部がある。この方法ではメンバー部分の全てを独自の IMGUI で記述することができる。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512900/df68343c-e23f-8c7b-c74e-17f03f154b8f.png)
ここでは、独自の `ScriptableObject` をインスペクター上で新たに作成するボタンを追加するサンプルを示す。スクリーンショットの通り "New" ボタンが追加されている。 `Descriptor` クラスと同様に**ファイルは `Editor` フォルダ内に置く**必要がある。

~~~cs
[Inspector(typeof(MyUnit))]
public class MyUnitInspector : UnitInspector {
    public MyUnitInspector(Metadata metadata) : base(metadata) {}
   // GUIを描画する
    protected override void OnMemberGUI(Metadata member, Rect memberPosition) {
        if (member.path.EndsWith("." + nameof(MyUnit.MyMember))) {
            MyMemberGUI(member, memberPosition);
        } else { 
            base.OnMemberGUI(member, memberPosition);
        }
    }
    // GUIのHeightを事前計算する
    protected override float OnMemberHeight(Metadata member, float width) {
        if (member.path.EndsWith("." + nameof(MyUnit.MyMember)))
            return MyMemberHeight(member, width);
        return base.GetMemberHeight(member, width);
    }
    // メンバーを描画する
    private void MyMemberGUI(Metadata member, Rect memberPosition) {
        var fieldPosition = BeginLabeledBlock(member, memberPosition, member.label);
        var buttonContent = new GUIContent(" New ");
        var buttonSize = EditorStyles.miniButtonRight.CalcSize(buttonContent);
        EditorStyles.miniButtonRight.CalcMinMaxWidth(buttonContent, out float minButtonWidth, out float maxButtonWidth);
        buttonSize = new Vector2(Mathf.Clamp(buttonSize.x, minButtonWidth, maxButtonWidth), buttonSize.y);

        var pickerPosition = new Rect(
            fieldPosition.x,
            fieldPosition.y,
            fieldPosition.width - buttonSize.x,
            EditorGUIUtility.singleLineHeight);
        var buttonPosition = new Rect(
            fieldPosition.x + fieldPosition.width - buttonSize.x,
            fieldPosition.y,
            buttonSize.x,
            EditorGUIUtility.singleLineHeight);

        var newValue = EditorGUI.ObjectField(pickerPosition, member.value as MyScriptableObject, typeof(MyScriptableObject), false);
        if (GUI.Button(buttonPosition, buttonContent)) {
            newValue = MyScriptableObject.CreateAsset(); // 独自に実装
        }

        if (EndBlock(member)) {
            member.RecordUndo();
            member.value = newValue;
        }
    }
    // メンバーの高さを事前計算する
    private float MyMemberHeight(Metadata member, float width) {
        return EditorGUIUtility.singleLineHeight;
    }
}
~~~
`UnitInspector` を継承して、そのクラスに `InspectorAttribute(typeof(MyUnit))` を付与する。あとは内部でごにょごにょしてくれている。
`Metadata.path` は `Root.FlowGraph.elements.MyUnit.MyMember` といったルートからのパスになっているのでメンバー名の判定に使用した。

記述次第で `InspectableIf()` を動的に反映、 `Enum` タイプで表示/非表示を切り替え、読み取り専用のラベル、 `GUIStyle` を触ると押せないボタンなどを作ることができる。

# 自作ユニットのノードをカスタマイズする
基本的には `UnitWidget`クラスを継承してカスタマイズする。作成したファイルは `Descriptor` クラスと同様に **`Editor` フォルダ内に置く**必要がある。

## ヘッダーにインスペクターを表示する (settings)
`UnitHeaderInspectableAttribute(string label)`
Enumタイプのドロップダウン以外見栄えよくない

## ヘッダーに任意のコンテンツを表示する (HeaderAddon)
`UnitHeaderInspectableAttribute()` を付与して追加するヘッダーのインスペクターとは別に好きなコンテンツを MUGUI で追加できる。標準ユニットでは `LiteralWidget` のみで使用されている。
```cs
[Widget(typeof(MyUnit))]
public class MyWidget : UnitWidget<MyUnit> {
    public MyWidget(FlowCanvas canvas, MyUnit unit) : base(canvas, unit) {}
    // フラグを立てる
    protected override bool showHeaderAddon => true;
    // 幅を事前計算する
    protected override float GetHeaderAddonWidth() {
        return 200; // サンプルなので適当
    }
    // 高さを事前計算する
    protected override float GetHeaderAddonHeight() {
        return EditorStyles.label.lineHeight; // サンプルなので適当
    }
    // コンテンツを描画する
    public override void DrawHeaderAddon() {
        // MyUnit に Message メンバーがあるとしてラベルで表示する
        GUI.Label(this.headerAddonPosition, new GUIContent(this.unit.Message));
    }
}
```
`UnitWidget`クラスを継承して MUGUI でコンテンツを作成する。ヘッダーにラベルを追加するサンプル。

## フッターに任意のコンテンツを表示する
![footeraddon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512900/d68f86aa-0f46-3303-d4e8-3fb82ebcb66e.png)
```cs
[Widget(typeof(MyUnit))]
public class FooterAddonWidget : UnitWidget<MyUnit> {
    public FooterAddonWidget(FlowCanvas canvas, MyUnit unit) : base(canvas, unit) {}
    // 領域の事前計算 CachePosition() に上手く割り込めないので HeaderAddon を使って幅を指定する
    // (ports background 領域が指定できない)
    protected override bool showHeaderAddon => true;
    protected override float GetHeaderAddonWidth() {
        // HeaderAddon width + Icon width = Header width = FooterAddon width
        return 200;
    }
    // === HeaderAddon 風にメンバーを定義する === //
    protected virtual bool showFooterAddon { get; } = true;
    protected virtual Rect footerAddonPosition { get; set; }
    protected GUIContent footerContent = new GUIContent();
    protected virtual float GetFooterAddonHeight(float width) {
        // UnitWidget のスタイルを流用
        // settingsLabel は UnitHeaderInspectable で追加される部分のこと
        return Styles.settingLabel.CalcHeight(footerContent, width);
    }
    protected virtual void DrawFotterAddon() {
        GUI.Label(footerAddonPosition, footerContent, Styles.settingLabel);
    }
    // ========================================= //
    public override void DrawForeground() {
        base.DrawForeground();
        if (showFooterAddon) {
            BeginDim(); // スタイルを流用してるので Dim スタイルに簡易対応する
            DrawFotterAddon();
            EndDim();
        }
    }
    public override void CachePosition() {
        // 領域計算前にコンテンツを更新する
        // MyUnit に Message メンバーが定義されているとする
        footerContent.text = unit.Message;
        Styles.settingLabel.wordWrap = true; // 文字列折り返しを有効
        base.CachePosition();
        if (showFooterAddon) {
            var space = 5;
            var width = Mathf.Max(_position.width, 100);
            footerAddonPosition = new Rect(
                _position.x + space,
                _position.y + _position.height + space,
                width - space * 2,
                GetFooterAddonHeight(width)
            );
            _position = new Rect(
                _position.x,
                _position.y,
                _position.width,
                _position.height + footerAddonPosition.height + space * 2
            );
        }
    }
}
```
`UnitWidget`クラスを継承して MUGUI でコンテンツを作成する。フッターにラベルを追加するサンプル。

## ヘッダー上部の色を変える
```cs
[Widget(typeof(MyUnit))]
public class MyUnitWidget : UnitWidget<MyUnit>
{
    public MyUnitWidget(FlowCanvas canvas, MyUnit unit) : base(canvas, unit) { }
    protected override NodeColorMix baseColor => NodeColor.Yellow;
}
```
`UnitWidget`クラスを継承して、色を変更できる。通常カテゴリごとに分けれているので無暗に触る部分でもない。独自のノードシステムを作るときなどに使用したい。

## ポートにインスペクターを表示する
自作ユニットでポート `ValueInput` を追加したとき標準ユニットのようにポートの隣にインスペクターが表示されないことに気付くと思う。初期値を設定すると表示されるようになる。
```cs
protected Definition() {
    base.Definition();
    // 型を直接指定できる場合は第二引数で定義する
    MyInput = ValueInput<string>(nameof(MyInput), "");
    // 型を Type で指定する場合はメンバーの辞書に直接指定する
    var anyType = typeof(string);
    MyAnyTypeInput = ValueInput(anyType, nameof(MyAnyTypeInput));
    defaultValue[nameof(MyAnyTypeInput)] = anyType.Default();
}
```