特殊すぎて情報がないUnityのVisual Scripting向け自作ノードの話

# 概要

理由あってFormulaノードみたいな動きをする自作ノードを作りたくなりました。
ただ、Googleさんに聞いてみてもそういった自作ノードを作るための情報がさっぱりなかったので
Formulaノードやその他のソースコードを眺めてみてなんとなくつかんだ結果をメモとして残しておこうって話です。

# Formulaノードの特徴

Formulaノードには他のよくあるノードとは大きく異なる特徴が2つありまして、

1. 入力データポートの数を変更できる
1. テキストボックスが複数行の入力に対応している

標準で用意されているノードの中でも1.はAddやCreateList等の限られたノードでしか使われておらず、
2.に至っては自分が見た限りおそらくFormulaノードしか使ってないんじゃないかと思われるためか、
こういうノードを作るための情報はさっぱり見当たりませんでした・・・

# ソースコード

Visual ScriptingはかつてBOLTという名前の有料のアセットだったそうなので、
ソースコード非公開なんじゃないかと思ってました。Githubにもそれっぽいレポジトリが見つからないし・・・
と思っていたらどうやら、

```
[Unityプロジェクトのフォルダ]\Library\PackageCache\com.unity.visualscripting@[バージョン]
```

にソースコードが収められており、
Formulaノードのソースコードも見つかったのでどうやって実装しているのか眺めることができました。

# テキストボックスの複数行入力

StringLiteralノードやstring型を入力データポートとするノードで確認できますが、
現在のところstring型に相当するテキストボックスは複数行入力に対応していません。

検索してもどうもはっきりした答えは見当たりませんでした。

ソースコードを眺めた結果、どうやらヘッダ内かつインスペクタに設定したテキストボックス限定で複数行入力にするためのオプションがあるようです。
```c#
[Inspectable, InspectorTextArea, UnitHeaderInspectable]
```
上記のアトリビュートを相当するstring型の変数に設定するとノード上で複数行入力ができるようになるようです。
InspectorTextAreaが複数行入力できるように設定するアトリビュートです。
Inspectableじゃない変数にInspectorTextAreaを設定しても複数行入力はできないようです。
Formulaノードのために作られた感が満載です。

# 入力データポート数の動的変更

### どうやって実現している？

FormulaノードにはInputsというテキストボックスがあり、そこに数値を入力すると、
入力した数に応じて入力データポートを増えたり減ったりさせる事ができるようになっています。

ソースコードを見るまではこれはどうやらMultiInputUnitというクラスを継承して実現していることはわかりましたが、
MultiInputUnitでGoogleさんに聞いても公式のクラスリファレンスしか出てこないという惨憺たる有様で、
何も情報は得られませんでした・・・

なんとかソースコードを眺めた結果、以下のような感じでコーディングすれば実現できるようです。
思ったより簡単

```C#
using Unity.VisualScripting;

//MultiInputUnitクラスを継承、MultiInputUnitクラスの型パラメータは入力データポートの型になる
public class FormulaModoki : MultiInputUnit<object>
{
    //------------------------中略---------------------------------//

    //Inputsの最低値(デフォルトは2)を変更する場合は以下のコードを追加
    protected override int minInputCount => 0;

    //------------------------中略---------------------------------//

    protected override void Definition()
    {
        //MultiInputUnitクラスのDefinitionメソッドを実行
        base.Definition();

        //入力データポートのデータにNullが入るを許可するのかな？
        InputsAllowNull();

        //------------------------中略---------------------------------//

        //出力ポートがある場合は全ての入力ポートに対してRequirementを設定
        foreach (var input in multiInputs)
        {
            Requirement(input, result);
        }
    }

    //------------------------中略---------------------------------//
}

```

### 問題点

しかし、個人的にはいくつか問題点がありminInputCountのように特定の変数への代入やアトリビュート等で設定できないか探してみたが、
どうにも見つからなかった。ソースコードを見る限りそりゃそうだろうなぁという結論に至ったが、具体的には

- Inputsの最小値はminInputCountで設定できるが最大値は10で固定になっており設定できない

```(一部)MultiInputUnit.cs
//InputCountテキストボックス部分
[DoNotSerialize]
[Inspectable, UnitHeaderInspectable("Inputs")]
public virtual int inputCount
{
    get
    {
        return _inputCount;
    }
    set
    {
        _inputCount = Mathf.Clamp(value, minInputCount, 10);  //←最大値はここでハードコーディングされている
    }
}
```

- 入力データポートのポート名が0,1,2,3...と0から始まる数字になり変更できない

```(一部)MultiInputUnit.cs
for (var i = 0; i < inputCount; i++)
{
    //入力データポートの作成及びポート名の設定部分
    //i.ToString()で名前を設定してるのでポート名は0から始まる数字固定、設定等で変えることはできないっぽい
    _multiInputs.Add(ValueInput<T>(i.ToString()));
}
```
- ノードを配置した直後の初期Inputsの値が2になっており変更できない

```(一部)MultiInputUnit.cs
//InputCountの値を格納している変数
[SerializeAs(nameof(inputCount))]
private int _inputCount = 2;   //←初期値はここでハードコーディングされている
```

FomulaノードはInputsの初期値は2であるものの、最大値は9、入力ポート名はA～Iのアルファベットとなっているが、
ソースコードを眺めた感じそのような動作に変更するためのコードや設定は見当たらなかったので、
これはもうコードをコピーしてFormulaノードっぽく動くようにカスタマイズするしかなさそうという結論に。
プロジェクト内にあるソースコードと実際のノードを作るためのソースコードは異なっているんじゃないかと疑ってます。

# カスタマイズ

というわけでFormulaっぽいUNITを作るコードはこんな感じになりました。

```C#
using Unity.VisualScripting;

//MultiInputUnitではなくUNITとIMultiInputUnitを継承
public class FormulaModoki : Unit, IMultiInputUnit
{
    //------------------------中略---------------------------------//

    //コード入力部分、複数行入力対応
    [Inspectable, InspectorTextArea, UnitHeaderInspectable]
    public string Code { get; private set; }

    //Inputsの値を格納する変数、ここで初期値を0に設定する
    [SerializeAs(nameof(inputCount))]
    private int _inputCount = 0;

    //最大値の設定
    [DoNotSerialize]
    protected virtual int maxInputCount => 9;

    //最小値の設定
    [DoNotSerialize]
    protected virtual int minInputCount => 0;

    //IMultiInputUnitインターフェースの実装
    //入力データポートに接続されたノードのデータはここに格納されます
    [DoNotSerialize]
    public ReadOnlyCollection<ValueInput> multiInputs { get; protected set; }

    //IMultiInputUnitインターフェースの実装
    //Inputsテキストボックスの設定
    [DoNotSerialize]
    [Inspectable, UnitHeaderInspectable("Inputs")]
    public virtual int inputCount
    {
        get
        {
            return _inputCount;
        }
        set
        {
            //最小値だけでなく、最大値も設定できるよう変数にする
            _inputCount = Mathf.Clamp(value, minInputCount, maxInputCount);
        }
    }

    //------------------------中略---------------------------------//

    protected override void Definition()
    {
        //------------------------中略---------------------------------//

        //base.Definition()相当のコード
        var mi = new List<ValueInput>();
        multiInputs = mi.AsReadOnly();

        for (var i = 0; i < inputCount; i++)
        {
            //入力データポートの名前をアルファベットに設定
            //MultiInputUnitで設定していた型パラメータは
            //ここのValueInputの型パラメータで設定する
            mi.Add(ValueInput<object>(((char)('a' + i)).ToString()));
        }

        //InputsAllowNull()相当のコード
        foreach (var input in multiInputs)
        {
            input.AllowsNull();
        }

        //------------------------中略---------------------------------//

        foreach (var input in multiInputs)
        {
            Requirement(input, Result);
        }

        //------------------------中略---------------------------------//
    }

    //------------------------中略---------------------------------//
```

### データの取得

前項で入力ポートの名前をアルファベットに変更できましたが、実体はListなので
ポート名で値を取得したい場合はアルファベットから数値に変換する必要があります。

```c#
char c = 'a';
object data = flow.GetValue<object>(multiInputs[c - 'a']);
```

# なんでこんなことを調べようと思ったのか

[これ](https://qiita.com/riafeed/items/8d043d01f6a1f4ca2611)を作りたかったから。