[Unity Visual Scripting] 型付き名前付きの引数を自由に追加できるユニットをつくる

# 導入
文字列フォーマットとかで動的に名前付き入力や出力が欲しくなる場面がある。
# 実装
まずは、 `InputValue` に必要な値を持つクラスを作る。
```cs
[Serializable]
public class ArgsMetadata {
    public string Name { get; set; }
    public Type Type { get; set; }
}
```
定義した `ArgsMetadata` クラスのリストから `InputValue` や `OutputValue` を生成する。
```cs
public class MyUnit : Unit {
    [DoNotSerialize] List<ValueInput> CustomInputsSource { get; } = new List<ValueInput>();
    [DoNotSerialize] List<ValueOutput> CustomsOutputsSource { get; } = new List<ValueOutput>();

    [DoNotSerialize] public ReadOnlyCollection<ValueInput> CustomInputs { get; }
    [DoNotSerialize] public ReadOnlyCollection<ValueOutput> CustomsOutputs { get; }

    [Inspectable, InspectorWide, InspectorLabel("Custom Inputs")]
    List<ArgsMetadata> CustomInputMetadata { get; }
    [Inspectable, InspectorWide, InspectorLabel("Custom Outputs")]
    List<ArgsMetadata> CustomOutputMetadata { get; }

    public MyUnit() {
        // ValueInput, ValueOuput を読み取り専用で公開する
        CustomInputs = new ReadOnlyCollection<ValueInput>(CustomInputsSource);
        CostomOutputs = new ReadOnlyCollection<ValueOutput>(CustomOutputsSource);
    }

    protected override void Definition() {
        CustomInputsSource.Clear();
        CustomsOutputsSource.Clear();

        // メタデータを元にポートを作成する
        if (CustomInputMetadata != null) {
            foreach (var c in CustomInputMetadata) {
                if (c.Type == null) continue;
                CustomInputsSource.Add(ValueInput(c.Type, c.Name));
            }
        }

        if (CustomOutputMetadata != null) {
            foreach (var c in CustomOutputMetadata) {
                if (c.Type == null) continue;
                CustomsOutputsSource.Add(ValueOutput(c.Type, c.Name));
            }
        }
        // フローや関連付けを適宜追加 //
    }
}
```
このままでは GraphInspector に `ArgsMetadata` のインスペクターが定義されていないと警告されてしまうため `Inspector` クラスを継承して自前で GUI を実装する。 `InspectorAttribute` 属性を付与するとインスペクターに設定できる。
```cs
[Inspector(typeof(ArgsMetadata))]
public class ArgsMetadataInspector : Inspector {
    private TypeFilter typeFilter;
    public ArgsMetadataInspector(Metadata metadata) : base(metadata) {
        typeFilter = metadata.Member("Type").GetAttribute<TypeFilter>() ?? TypeFilter.Any;
    }
    // GUIの高さを事前計算する
    protected override float GetHeight(float width, GUIContent label) {
        return EditorGUIUtility.singleLineHeight;
    }
    // GUIを描画する
    protected override void OnGUI(Rect position, GUIContent label) {
        var typeMetadata = metadata.Member("Type");
        var nameMetadata = metadata.Member("Name");
        // Type と Name で半々にする
        var typePosition = new Rect(position.x, position.y,
                                    position.width / 2, position.height);
        var namePosition = new Rect(typePosition.x + typePosition.width, typePosition.y, 
                                    typePosition.width, typePosition.height);
        // Type フィールド
        BeginBlock(typeMetadata, position);
        var newType = LudiqGUI.TypeField(typePosition, GUIContent.none,
                                         (Type)typeMetadata.value, GetOptions);
        if (EndBlock(typeMetadata)) {
            typeMetadata.RecordUndo();
            typeMetadata.value = newType;
        }
        // Name フィールド
        BeginBlock(nameMetadata, namePosition);
        var newNameValue = GUI.TextArea(namePosition, (string)nameMetadata.value);
        if (EndBlock(nameMetadata)) {
            nameMetadata.RecordUndo();
            nameMetadata.value = newNameValue;
        }
    }
    private IFuzzyOptionTree GetOptions() {
        return new TypeOptionTree(Codebase.GetTypeSetFromAttribute(metadata), typeFilter);
    }
}
```
`LudiqGUI` という Visual Scripting 向けの GUI ヘルパークラスがあるので利用するとよい。

# 結果
![namedport.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3512900/f1dfda1b-50be-8d9f-5da1-d7c3579faaa3.png)
名前の重複は確認していないが `ValueInput` を追加した段階で重複があれば警告される。