【Unity】Visual Scripting(旧Bolt)でScript Machineのカスタムユニットを作成する方法まとめ

UnityのVisual Scripting(旧Bolt)でScript Machineのカスタムユニットを作成する方法をまとめました。

Unity2021.1.11f1
Visual Scripting 1.6.1

はじめに

Visual ScriptingはUnityでビジュアルスクリプティングを可能にするための公式パッケージです。
元々Boltという名前のサードパーティライブラリでしたが、Unityが買収して公式化・無料化・改名されました。
この辺りの話は以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

インストールはPackage Managerを用いて行います。
Package Managerの使い方は以下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

本記事ではVisual ScriptingのScript Machine(旧Flow Machine)においてカスタムユニットを作成する方法についてまとめます。

ログ出力を行うユニットを作成する

早速カスタムユニットを作成していきます。
まずはログ出力だけを行うユニットを作成してみます。

using Unity.VisualScripting;
using UnityEngine;

[UnitTitle("Example01")] // ユニットの検索に使われる名前
[UnitShortTitle("Example01")] // ユニットの表示に使われる名前
[UnitSurtitle("surtitle")] // ユニットの表示名の上に表示される
[UnitSubtitle("subtitle")] // ユニットの表示名の下に表示される
[UnitCategory("Test")] // カテゴリ
[UnitOrder(1)] // カテゴリ内での表示順
public class Example01Unit : Unit
{
    [DoNotSerialize] [PortLabelHidden] public ControlInput Input { get; private set; }

    [DoNotSerialize] [PortLabelHidden] public ControlOutput Output { get; private set; }

    [DoNotSerialize] public ValueInput Value { get; private set; }

    protected override void Definition()
    {
        Input = ControlInput(nameof(Input), OnEnter);
        Output = ControlOutput(nameof(Output));
        Value = ValueInput<string>(nameof(Value)); // string型の入力値ポートを作成する

        // 入力と出力のリレーションを設定する
        Requirement(Value, Input); // InputがトリガーされたときにValueが取得される
        Succession(Input, Output); // OutputがトリガーされたときにInputがトリガーされる
    }

    private ControlOutput OnEnter(Flow flow)
    {
        // 処理をしてControlOutputを返す
        var value = flow.GetValue<string>(Value);
        Debug.Log(value);
        return Output;
    }
}

ここまではほぼボイラープレートで、説明はコメントに書いた通りです。

Definition()で行っている入出力のリレーション構築に関しては、デバッグの用途で必要になります。
例えば上記の例では、ValueとInputとのリレーションが構築されているため、Valueポートに何も接続されていなかったら以下のように警告が表示されます。

f:id:halya_11:20210704233048p:plain
警告表示

もしリレーションを設定していなかった場合、警告は表示されずランタイムエラーとなります。
リレーションについては詳しくはマニュアルの以下のページを参照してください。

docs.unity3d.com

このように新しいユニットを作ったらProject Settings > Visual Scripting > Regenerate Unitsを実行します。
これでログ出力だけを行うユニットを作成できました。

f:id:halya_11:20210704233330p:plain
ログ出力ユニット

加算するユニットを作る

次に値を加算するユニットを作ります。
前節のユニットと違い後続のユニットに値を受け渡すユニットとなります。

using Unity.VisualScripting;

[UnitTitle("Example02")]
[UnitShortTitle("Example02")]
[UnitCategory("Test")]
public class Example02Unit : Unit
{
    [DoNotSerialize] public ValueInput Value1 { get; private set; }

    [DoNotSerialize] public ValueInput Value2 { get; private set; }

    [DoNotSerialize] public ValueOutput Result { get; private set; }

    protected override void Definition()
    {
        // ポートを作成
        Value1 = ValueInput<float>(nameof(Value1));
        Value2 = ValueInput(nameof(Value2), 0.0f); // デフォルト値を指定することも可能
        // 計算を行うメソッドを第二引数に渡して出力ポートを作成
        Result = ValueOutput(nameof(Result), Add).Predictable(); // PredictableをつけるとEdit ModeでAddを実行してエラーが起きないかチェックする

        // リレーションを作成
        Requirement(Value1, Result);
        Requirement(Value2, Result);
    }

    private float Add(Flow flow)
    {
        var value1 = flow.GetValue<float>(Value1);
        var value2 = flow.GetValue<float>(Value1);
        return value1 + value2;
    }
}

大体の説明はコメントに書いた通りです。
またRegenerate Unitsを行うことでこのユニットが使えるようになります。

f:id:halya_11:20210705001410p:plain
加算ユニット

1秒後にログ出力する、コルーチン用のユニットを作る

さてUnityのVisual Scriptingではコルーチンに対応したユニットを作成できます。
そこで次に1秒後にログ出力する、コルーチン用のユニットを作成します。

using System.Collections;
using Unity.VisualScripting;
using UnityEngine;

[UnitTitle("Example03")]
[UnitShortTitle("Example03")]
[UnitCategory("Test")]
public class Example03Unit : Unit
{
    [DoNotSerialize] [PortLabelHidden] public ControlInput Input { get; private set; }

    [DoNotSerialize] [PortLabelHidden] public ControlOutput Output { get; private set; }

    [DoNotSerialize] public ValueInput Value { get; private set; }

    protected override void Definition()
    {
        // ControlInputではなくControlInputCoroutineを使う
        Input = ControlInputCoroutine(nameof(Input), OnEnterRoutine);
        Output = ControlOutput(nameof(Output));
        Value = ValueInput<string>(nameof(Value));

        Requirement(Value, Input);
        Succession(Input, Output);
    }

    private IEnumerator OnEnterRoutine(Flow flow)
    {
        // 1秒後にログ出力する
        yield return new WaitForSeconds(1.0f);
        var value = flow.GetValue<string>(Value);
        Debug.Log(value);
        
        // Outputを返すこと
        yield return Output;
    }
}

説明はコメントの通りです。
ControlInputの代わりにControlInputCoroutineを使っているのがポイントです。

コルーチンのチェックボックスにチェックを入れたうえでこのユニットを使用すると動作が確認できます。

f:id:halya_11:20210705002752p:plain
1秒後にログ出力するユニット

ユニットのヘッダで値を設定できるフィールドを表示する

さて次にユニットのヘッダ部分から値を設定できるようにしてみます。
ヘッダ部分に入力フィールドを表示するには以下のようにUnitHeaderInspectableアトリビュートを付けたプロパティを使います。

[Inspectable, UnitHeaderInspectable("Header Example")]
public string HeaderExample { get; private set; }

すると以下のようにユニットのヘッダから値を設定できるようになります。

f:id:halya_11:20210705102059p:plain
ヘッダで値を設定

ユニットのアイコンを設定する

次にカスタムユニットにアイコンを設定してみます。
まず他のユニットと同じアイコンを設定するには、カスタムユニットクラスに以下のアトリビュートを付けます。

[TypeIcon(typeof(Timer))] // 指定した型のユニットと同じアイコンが使われる

f:id:halya_11:20210705120738p:plain
アイコン

完全に独自のテクスチャを使いたい場合には以下のようにUnitDescriptorを定義することもできます。

using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;

[Descriptor(typeof(Example01Unit))]
public class Example01UnitDescriptor : UnitDescriptor<Example01Unit>
{
    private EditorTexture _icon;
    
    public Example01UnitDescriptor(Example01Unit unit) : base(unit)
    {
        var iconTex = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Example/tex_star.png");
        _icon = EditorTexture.Single(iconTex);
    }

    protected override EditorTexture DefinedIcon()
    {
        return _icon;
    }
}

関連

light11.hatenadiary.com

light11.hatenadiary.com

参考

docs.unity3d.com