1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-07-15

自作ユニットのインスペクターをカスタマイズする

自作ユニット作成 ~ ポート追加 ~ ポート情報追加 ~ といった基本的なところは公式リファレンスに丁寧に書かれている。ここではその他のカスタマイズを逆引き的に置いておく。

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

メンバーをインスペクターに表示する

public class MyUnit : Unit {
    [Inspectable]
    string MyMember;
}

InspectableAttribute()
プライベートなメンバーでもOK

条件付きで表示する

[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 で記述することができる。

ここでは、独自の ScriptableObject をインスペクター上で新たに作成するボタンを追加するサンプルを示す。スクリーンショットの通り "New" ボタンが追加されている。 Descriptor クラスと同様にファイルは Editor フォルダ内に置く必要がある。

[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.pathRoot.FlowGraph.elements.MyUnit.MyMember といったルートからのパスになっているのでメンバー名の判定に使用した。

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

自作ユニットのノードをカスタマイズする

基本的には UnitWidgetクラスを継承してカスタマイズする。作成したファイルは Descriptor クラスと同様に Editor フォルダ内に置く必要がある。

ヘッダーにインスペクターを表示する (settings)

UnitHeaderInspectableAttribute(string label)
Enumタイプのドロップダウン以外見栄えよくない

ヘッダーに任意のコンテンツを表示する (HeaderAddon)

UnitHeaderInspectableAttribute() を付与して追加するヘッダーのインスペクターとは別に好きなコンテンツを MUGUI で追加できる。標準ユニットでは LiteralWidget のみで使用されている。

[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

[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 でコンテンツを作成する。フッターにラベルを追加するサンプル。

ヘッダー上部の色を変える

[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 を追加したとき標準ユニットのようにポートの隣にインスペクターが表示されないことに気付くと思う。初期値を設定すると表示されるようになる。

protected Definition() {
    base.Definition();
    // 型を直接指定できる場合は第二引数で定義する
    MyInput = ValueInput<string>(nameof(MyInput), "");
    // 型を Type で指定する場合はメンバーの辞書に直接指定する
    var anyType = typeof(string);
    MyAnyTypeInput = ValueInput(anyType, nameof(MyAnyTypeInput));
    defaultValue[nameof(MyAnyTypeInput)] = anyType.Default();
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?