はじめに
Unity2021で Visual Scripting (旧 Bolt) が標準搭載になりました。
この記事では Visual Scripting で自作のUnitを作成する方法を解説していきます。
※Visual Scripting自体の使い方は省略しています。
開発環境
- Unity 2021.1.4f1
- Visual Scripting 1.5.1
参考リンク
データUnitの作成
まずは、データ Unit の作り方です。
今回は、 Main Camera
を取得するデータUnitを作成していきます。
サンプルコード
using Unity.VisualScripting;
using UnityEngine;
/// <summary>
/// メインカメラを取得するUnitクラス
/// </summary>
[UnitCategory("自作Unit")] // Script Graph で表示されるUnitカテゴリ
[UnitTitle("メインカメラ")] // Unit名
public class MainCameraUnit : Unit
{
/// <summary>
/// メインカメラ
/// </summary>
[DoNotSerialize]
[PortLabelHidden] // Script Graph で戻り値名が表示されないようにする
public ValueOutput MainCamera { get; private set; }
/// <summary>
/// 定義
/// </summary>
protected override void Definition()
{
MainCamera = ValueOutput(nameof(MainCamera), _ => Camera.main); // 他のUnitからMainCameraプロパティにアクセスされた時の処理を設定
}
}
Unitの再生成
Project Settings
→Visual Scripting
→Regenerate Units
をクリックすると、Unitの再生成が開始されます。

たまに、以下の画像のように再生成に失敗することがあるみたいです。
その場合は、Unityのリフレッシュ等をしてから再度実行してみてください。

Script Graphに追加
Unitの再生成の後でScript GraphウィンドウでUnitの一覧を表示すると 自作 Unit
というカテゴリが追加されています。

このカテゴリの中に作成した メインカメラ Unit
があります。

以下のように、Startのタイミングで Main Camera
を取得して、オブジェクト名をログに出力するScript Graphを作成しました。

シーンを実行
自作のUnitが正常に実装できていれば、シーンを実行するとログが以下のように出力されます。

フローUnitの作成
次は、入出力ポートのあるフローUnitの作り方です。
今回は、引数で渡されたオブジェクトの方を向く Look At Unit
を作成していきます。
サンプルコード
using Unity.VisualScripting;
using UnityEngine;
/// <summary>
/// Look At Unit クラス
/// </summary>
[UnitCategory("自作Unit")]
[UnitTitle("Look At")]
public class LookAtUnit : Unit
{
/// <summary>
/// 入力ポート
/// </summary>
[DoNotSerialize]
[PortLabelHidden]
public ControlInput InputTrigger { get; private set; }
/// <summary>
/// 出力ポート
/// </summary>
[DoNotSerialize]
[PortLabelHidden]
public ControlOutput OutputTrigger { get; private set; }
/// <summary>
/// 対象のオブジェクト
/// </summary>
[DoNotSerialize]
[PortLabel("対象のオブジェクト")] // Script Graph で表示される引数名
public ValueInput Target { get; private set; }
/// <summary>
/// 定義
/// </summary>
protected override void Definition()
{
InputTrigger = ControlInput(nameof(InputTrigger), lookAt); // 入力ポートに入力が来た時の処理を設定
OutputTrigger = ControlOutput(nameof(OutputTrigger));
Target = ValueInput<Transform>(nameof(Target), null);
}
/// <summary>
/// Look At
/// </summary>
/// <param name="flow"></param>
/// <returns></returns>
private ControlOutput lookAt(Flow flow)
{
var self = flow.stack.ToReference().self; // 回転させるオブジェクト
var target = flow.GetValue<Transform>(Target); // 対象のオブジェクト
self.transform.LookAt(target); // Look At 処理
return OutputTrigger;
}
}
Script Graphに追加
データUnitの時と同様にUnitの再生成をすると、以下のように Look At Unit
が一覧に追加されます。

以下のように、StartのタイミングでMain Camera
を取得して、 Main Camera
の方向を向き、その後ログを出力するようにScript Graphに変更を加えました。

シーンを実行
回転させるオブジェクトとしてXYZ軸がついたCubeをシーンに追加しました。
このCubeにScript Machineを追加して、作成したScript Graphを設定します。

このシーンを実行すると、以下の動画のようになります。
取得したカメラの方にCubeの正面(青色のZ軸)が向いています。
その後、次のフローUnit(Debug.Log
)に遷移しています。
イベントUnitの作成
最後にイベントUnitの作り方です。
今回は、オブジェクトをクリックしたら次のフローUnitに遷移するイベントUnitを作成していきます。
サンプルコード
イベントUnit本体クラスと、イベントUnitを発火するBehaviourクラスの2つを作成します。
まずは、Unit本体クラスです。
EventUnitクラスを継承しています。
また、UnitCategoryを Events
配下にしないと一覧に表示されないので、 Events/自作Unit
としています。
using Unity.VisualScripting;
/// <summary>
/// オブジェクトクリック時のイベント Unit クラス
/// </summary>
[UnitCategory("Events/自作Unit")]
[UnitTitle("オブジェクトクリック時")]
public class OnClickObjectUnit : EventUnit<EmptyEventArgs>
{
/// <summary>
/// Event Bus に登録するか
/// </summary>
protected override bool register => true;
/// <summary>
/// フックを取得する
/// </summary>
/// <param name="reference"></param>
/// <returns></returns>
public override EventHook GetHook(GraphReference reference)
{
return new EventHook(nameof(OnClickObjectUnit), reference.self);
}
/// <summary>
/// 実行時に1回だけ呼び出される処理
/// </summary>
/// <param name="instance">インスタンス</param>
public override void Instantiate(GraphReference instance)
{
base.Instantiate(instance);
var self = instance.self; // Script Machine がアタッチされたオブジェクトを取得
// すでに UnitEventTrigger が追加されているかを確認
if(!self.TryGetComponent(out UnitEventTrigger _))
{
self.AddComponent<UnitEventTrigger>(); // まだ追加されていない場合は追加
}
}
/// <summary>
/// Should Trigger
/// </summary>
/// <param name="flow">Flow</param>
/// <param name="args">イベント引数</param>
/// <returns>発火するか</returns>
protected override bool ShouldTrigger(Flow flow, EmptyEventArgs args)
{
return true;
}
}
次に、発火用のBehaviourクラスです。
オブジェクトにRayが当たった時に、EventBusに登録されたイベントUnitを発火しています。
上述の OnClickObjectUnit
クラスの Instantiate
関数内で、このコンポーネント UnitEventTrigger
をアタッチしています。
using Unity.VisualScripting;
using UnityEngine;
/// <summary>
/// イベント発火クラス
/// </summary>
public class UnitEventTrigger : MonoBehaviour
{
/// <summary>
/// Update
/// </summary>
private void Update()
{
if (!Input.GetMouseButtonDown(0)) return;
var inputPos = Input.mousePosition;
var ray = Camera.main.ScreenPointToRay(inputPos);
if (!Physics.Raycast(ray, out var hit)) return;
if (hit.collider.gameObject.Equals(gameObject))
{
EventBus.Trigger(nameof(OnClickObjectUnit), gameObject, new EmptyEventArgs()); // このコンポーネントがアタッチされたオブジェクトの Script Machine に通知する
}
}
}
Script Graphに追加
Unitの再生成をすると、以下のように Events/自作Unit
配下に オブジェクトクリック時Unit
が追加されます。

先程までのScript GraphのStart Unitを オブジェクトクリック時Unit
に差し替えました。

シーンを実行
この状態で実行してみます。
Cubeをクリックすると、Cubeの正面がカメラの方を向きます。
ちゃんとオブジェクトごと個別に オブジェクトクリック時Unit
が動作しているかを確認するためにCubeを3つに複製してみました。
この状態で実行してみると、クリックしたCubeだけがカメラの方を向き、それ以外のCubeは何も起きないので、それぞれ個別に オブジェクトクリック時Unit
が動作していることが確認できます。
クリックしたオブジェクトを EventBus.Trigger
関数の第2引数で指定しているので、クリックしたCube以外は動作しないようになっています。
関連性の紐づけ
ここまでで最低限動作するUnitを作れるようになりました。
ここからは自作Unitをより使いやすくするための設定を追加していきます。
入出力ポートの紐づけ
↓の画像の赤枠の部分、実際には使用しているUnitなのですが、まるで使われていないかのようにグレーアウトされています。

このようになってしまっている原因は、Look At Unit
の入力ポートと出力ポートの関連性が紐づけられていないからです。
この問題を解決するために LookAtUnit
クラスの Definiton
関数に処理を追加します。
/// <summary>
/// 定義
/// </summary>
protected override void Definition()
{
InputTrigger = ControlInput(nameof(InputTrigger), lookAt); // 入力ポートに入力が来た時の処理を設定
OutputTrigger = ControlOutput(nameof(OutputTrigger));
Target = ValueInput<Transform>(nameof(Target), null);
Succession(InputTrigger, OutputTrigger); // 入出力ポートを紐づける
}
これで入出力ポートが紐づけられて、以降のUnitがグレーアウトされなくなりました。

引数と入力ポートの紐づけ
また、引数と入力ポートを紐づけることもできます。
/// <summary>
/// 定義
/// </summary>
protected override void Definition()
{
InputTrigger = ControlInput(nameof(InputTrigger), lookAt); // 入力ポートに入力が来た時の処理を設定
OutputTrigger = ControlOutput(nameof(OutputTrigger));
Target = ValueInput<Transform>(nameof(Target), null);
Succession(InputTrigger, OutputTrigger); // 入出力ポートを紐づける
Requirement(Target, InputTrigger); // 引数と入力ポートを紐づける
}
これで、紐づけた引数が未設定の場合、Script Graph上で警告してくれるようになりました。

おわりに
Nintendo SwitchのNintendo Laboでもノードベースのソフトが発表されたことをはじめ、今後プログラミング教育などの場面でノードを扱うことも多くなりそうです。
これを機に自作Unit作りに挑戦してみてはいかがでしょうか。
また、弊社では、自作UnitでARコンテンツを作ることができるツール「CFA」を開発しました。
よろしければこちらもご確認ください。