【Unity】自作のVisualScriptingグラフユニットを作成する

はじめに

グラフ内のユニットをスクリプトを使用して自分で作成できます
今回はそのカスタムユニットの作成をしてみます

※以下公式リファレンスを参照 docs.unity3d.com

ケルトンユニット作成

何もしない空ユニットを作成してみます
適当なスクリプトに以下を記述

using Unity.VisualScripting;

public class MyUnit : Unit
{
    // このユニットの挙動を記述するメソッド
    protected override void Definition()
    {
    }
}

自分でユニットを作成した場合、使用するためには登録をしなければいけません。
以前 TextMeshPro を使用する為にしたことをします

Edit > ProjectSettings > VisualScripting

Regenerate Unitsをクリックして登録します
スクリプト内を走査して Unit を継承しているスクリプトを見つけて登録してくれます)

f:id:toshizabeth:20210927191748p:plain

(自分の環境 Unity2021.2.0b13 だと Regenerate Nodes と名前が違うんですよね.. おそらく次のバージョンで変わるのかな)

適当なグラフ内で検索ボックスを開くと一番下に自分が作成したユニットが配置されます f:id:toshizabeth:20210927194328p:plain

入力も出力も指定してないので配置してもただの箱になってます

f:id:toshizabeth:20210927195217p:plain

次に

  1. 入力ポートと出力ポートを付ける
  2. 値の受け取りと送信をする
  3. ユニット内にロジックを入れる
  4. 検索の改装を変更する

を順を追ってやっていこうと思います

1. 入力ポートと出力ポートを付ける

  • ControlInput : ユニットを実行するためのエントリポイントポート
  • ControlOutput 次ユニットに処理を移動させるポート

変数に各ポートを宣言し、Definitionでそれぞれ定義します

using UnityEngine;
using Unity.VisualScripting;

public class MyUnit : Unit
{
    private ControlInput _inputTrigger;        // 入力側のポート
    private ControlOutput _outputTrigger;  // 出力側のポート


    // このユニットの挙動を記述するメソッド
    protected override void Definition()
    {
        // 入力側
        _inputTrigger = ControlInput("InputTrigger", _ => _outputTrigger);

        // 出力側
        _outputTrigger = ControlOutput("OutputTrigger");
    }
}

これでポートが付いたユニットが完成します
これだけではただつなぐことができる箱です

f:id:toshizabeth:20210928003711p:plain

2. 値の受け取りと送信をする

  • ValueInput : ユニットに渡す入力値
  • ValueOutput : 次のユニットに渡す出力値

次に、値を複数、入力と出力に定義します
これは関数の引数と戻り値と考えるとしっくり来るのではないかと思います

using UnityEngine;
using Unity.VisualScripting;

public class MyUnit : Unit
{
    private ControlInput _inputTrigger;        // 入力側のポート
    private ControlOutput _outputTrigger;  // 出力側のポート

    private ValueInput _valueA; // 入力値
    private ValueInput _valueB; // 入力値
    private ValueOutput _resultA; // 出力値
    private ValueOutput _resultB; // 出力値


    // このユニットの挙動を記述するメソッド
    protected override void Definition()
    {
        // 入力側
        _inputTrigger = ControlInput("InputTrigger", _ => _outputTrigger);

        // 出力側
        _outputTrigger = ControlOutput("OutputTrigger");


        // 文字列の入力
        _valueA = ValueInput<string>("StringValueA", "Hello");     

        // 数値の入力
        _valueB = ValueInput<int>("IntValueB", 0);

        // 文字列の出力
        _resultA = ValueOutput<string>("StringResultA", _ => "固定文字列を返す");

        // 数値の捨つ力
        _resultB = ValueOutput<float>("FloatResultB", _ => 1.23f);
    }
}

このように、入力側と出力側の設定をしてみました
出力側はラムダで返す値を設定します。今回は固定値を返しています

ユニットの見た目は以下のようになりました
値の連結ができるようになっています

f:id:toshizabeth:20210928004356p:plain

3. ユニット内にロジックを入れる

現在はただ値を受け取って固定の値を返すだけですが、値を足し合わせる処理を入れてみます。
入力ポートのラムダ内に処理を記述していきます

using UnityEngine;
using Unity.VisualScripting;

public class MyUnit : Unit
{
    private ControlInput _inputTrigger;        // 入力側のポート
    private ControlOutput _outputTrigger;  // 出力側のポート

    private ValueInput _valueA; // 入力値
    private ValueInput _valueB; // 入力値
    private ValueOutput _resultA; // 出力値
    private ValueOutput _resultB; // 出力値

    private string _resultValue;


    // このユニットの挙動を記述するメソッド
    protected override void Definition()
    {
        // 入力側
        _inputTrigger = ControlInput("InputTrigger", flow => 
            {
                // 入力値を足し合わせるだけ
                _resultValue = $"{flow.GetValue<string>(_valueA)}_{flow.GetValue<int>(_valueB)}";
                
                return _outputTrigger;
            });

        // 出力側
        _outputTrigger = ControlOutput("OutputTrigger");


        // 文字列の入力
        _valueA = ValueInput<string>("StringValueA", "Hello");     

        // 数値の入力
        _valueB = ValueInput<int>("IntValueB", 0);

        // 文字列の出力
        _resultA = ValueOutput<string>("StringResultA", _ => _resultValue); // 連結した文字列を返す

        // 数値の捨つ力
        _resultB = ValueOutput<float>("FloatResultB", _ => 1.23f);
    }
}

inputTrigger の初期化時のラムダ内で入力値と出力値を足し合わせています。
そして
resultA の戻り値には足し合わせた文字列を返します。
これで次のようにして実行します

f:id:toshizabeth:20210928094505p:plain

結果、内部で変換した文字列がログに出るようになりました

f:id:toshizabeth:20210928094531p:plain

関係性の追加(視覚的サポート)

Requirement

入力値に何も設定されていない時、または何もつながっていない時に警告を出してくれます
例えば、以下のような入力値を宣言した時

protected override void Definition()
{       
.....
    // これを追加     
    var test = ValueInput<string>("Test", null);
}

グラフ上に変化はありません。

f:id:toshizabeth:20211002193309p:plain

Testという入力値は string型で、デフォルト引数は null が指定されています。
nullの時、つまり何も値がない場合には警告を出すようにします

protected override void Definition()
{       
.....
    // これを追加     
    var test = ValueInput<string>("Test", null);

    Requirement(test, _inputTrigger);
}

Requirement で test を _inputTrigger と関係を付けることで、何も値がないときはユニットが黄色くなり Graph Inspector 上でも警告が出るようになりました

f:id:toshizabeth:20211002193514p:plain

(個人的にはもう少し強めの警告色でも良かったかも。気づかずにスルーしそう)

Assignment

... これがよくわからず.. リファレンス見る限りアウトプット側に対して何らかの表示があるっぽいですが特に変化がわからない..

Succession

自分のユニットを作成して他のユニットと結びつけた時、結びつけ先のユニットがグレイアウトしていると思います

f:id:toshizabeth:20211002185950p:plain

グレイアウトしているユニットの詳細を見てみると警告が出ています。

f:id:toshizabeth:20211002190443p:plain

「前ユニットの出力(Output Trigger)からこのユニットに来ることがない」と判断されてグレイアウトされています。
そこで、『自分のユニットは入力があれば必ず出力します!』ということを教えてあげます。

Definitionメソッドに以下を追加

protected override void Definition()
{       
.....
    // これを追加     
    Succession(_inputTrigger, _outputTrigger);
}

これで入力ポートになにか接続されていればoutput先がグレイアウトされなくなります

入力ポートが接続されている時
f:id:toshizabeth:20211002191034p:plain

入力ポートに何も接続されてないと自分含めグレイアウトします

入力ポートが接続されていない時
f:id:toshizabeth:20211002191112p:plain

ドキュメントの追加

グラフでユニットを選択した時に出る Graph Inspector にコメントが表示されるようになります
ユニットとは別のスクリプトに記述する必要があるので MyUnitDescriptor というスクリプトを新規に作成します

※ Editorフォルダ配下に配置する必要があります

using Unity.VisualScripting;

[Descriptor(typeof(MyUnit))] //対象指定
public class MyUnitDescriptor : UnitDescriptor<MyUnit>
{
   public MyUnitDescriptor(MyUnit unit) 
    : base(unit) 
   {}

   protected override void DefinedPort(IUnitPort port, UnitPortDescription description)
   {
       base.DefinedPort(port, description);

       switch (port.key)
       {
           case "InputTrigger":
               description.summary = "ユニットに接続した時の機能説明";
               break;
               
           case "OutputTrigger":
               description.summary = "ユニットからの出力先に接続した時の説明";
               break;

           case "StringValueA":
               description.summary = "入力値に対しての説明";
               break;
               
           case "IntValueB":
               description.summary = "入力値に対しての説明";
               break;

           case "StringResultA":
               description.summary = "出力値に対しての説明";
               break;

           case "FloatResultB":
               description.summary = "出力値に対しての説明";
               break;
       }
   }
}

これでGraph Inspectorを見ると..

f:id:toshizabeth:20211002201454p:plain

Unitだけ表示されても何を入力してどう出力するのかがわかりにくいため、コメントによる補佐は積極的に使っていきたいですね


長くなったので残りは次回