VisualScriptingとC#スクリプトとの比較

2022年1月8日

それぞれのサンプルで比較してみましょう

C#の基本

いくつかの構文を通して違いを見ていきましょう

変数

Visual Scriptでも変数の概念はあります。また、変数に型があるのもC#と同じです

C#では・・・

int型変数のdataAに初期値として1を代入しています

public class Variable
{
    int dataA = 1;
}

Visual Scriptingでは・・・

バックグラウンドタブ <x> を選択して、Graphに設定してみましょう。
これは、このグラフだけでしか利用できない変数であることを宣言していることになります。
C#ではフィールドと呼ばれています

グラフに表示するには、グラフ内でマウスを右クリックして選択します
Getは、変数を読み込む時に選択します
Setは、書き込む時に選択します

Startメソッドで実装

コンソール画面に結果を表示するメソッドを作ってみましょう

using UnityEngine;

public class Variable : MonoBehaviour
{
    private void Start()
    {
        int dataA = 1;

        Debug.Log(dataA);
    }
}

エディターを実行すると(Unityエディターの ▶︎ボタン)、このように途中のデータがラインに表示されます

グラフのサンプル

Debug.Logを置くには

グラフ内で右クリックし検索窓でサーチしてDebug.Logを選択するとGraph(グラフ)にDegugのUnitを置くことができます

参照型

List

例として、Listの場合を考えてみます

using System.Collections.Generic;

public class List
{
    List<int> ListA = new List<int>();
}

Startメソッドで実装

コンソール画面に結果を表示します

using UnityEngine;
using System.Collections.Generic;

public class List : MonoBehaviour
{
    List<int> ListA = new List<int>();

    private void Start()
    {
        foreach (var item in ListA)
        {
            Debug.Log(item);
        }
    }
}

移動

AddForce

using UnityEngine;

public class Move : MonoBehaviour
{
    float power = 10;

    void Update()
    {
        GetComponent<Rigidbody>().AddForce(new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")) * power);
    }
}

型変換・フォーマット出力

演算子

算術演算子

足し算

public class AddSample
{
    int a;
    int b;
    int c;

    void Add()
    {
        c = a + b;
    }
}

演算子の優先順位

制御構文

条件分岐

if文の基本形

UnityでStart()メソッドで実行したい場合

using UnityEngine;

public class Variable : MonoBehaviour
{
    private void Start()
    {
        int dataA = 1;
        int dataB = 2;

        if (dataA > dataB)
        {
            // 処理
        }
    }
}

計算式を変えることができます

台形の面積を求めるのは、次であらわせます
(上底+下底)× 高さ / 2

グラフで作成すると次のようになります

コントルーキキー(CTL)※Macでは、コマンドキーを押下しながらマウスをドラッグすると、グループ化することができますので、一種のコメントとしてみやすくできます

if文による多分岐

switch文による多分岐

数値による選択(Switch On Integer)

文字列による選択(Switch On String)

繰り返し

ループごとの実行

ループ終了時も考慮

平均値サンプル

コード

using System.Collections.Generic;
using UnityEngine;

public class Average : MonoBehaviour
{
    List<int> scores = new List<int> { 60, 70, 80 };

    int sun = 0;

    void Start()
    {
        foreach (var score in scores)
        {
            sun += score;
        }

        Debug.Log(sun / scores.Count);
    }
}

グラフ

様々な形式のfor文

多重ループ

ループの中断とスキップ

while文・do-while文

Whileの条件式によってループを終了させるパターン

Breakでループを終了させるパターン

foreach文

メソッド

足し算をするメソッドサンプル

2つの整数を与えると答えを返す処理をする

public class Add2DataSample
{
    public int Add(int data1, int data2)
    {
        return data1 + data2;
    }
}

足し算のメソッドを使ってみる
メソッド名(引数1, 引数2)を実行すると、結果が戻される
今回は、引数1が整数の1、引数2が整数の2としているので、結果の3が答えとして得られた

using UnityEngine;

public class Calc : MonoBehaviour
{
    Add2DataSample addSample = new Add2DataSample();

    private void Start()
    {
        Debug.Log(addSample.Add(1, 2));
    }
}

実行結果

Unity固有

コルーチン(数秒待つ)

ステートマシン

シーンのBuild画面

使われるシーンの登録をします
これを実施しないとシーン切り替え時、エラーになります

Titleシーン

ステートグラフ

マウスがクリックされたらGameシーンに切り替わる

5秒経つと自動的にGameシーンに切り替わる条件も追加

画面切り替え処理

Gameシーン

ステートグラフ

ボタンをクリックしたら画面を切り替え

画面切り替え処理

Resultシーン

C#からVisual Scriptを操作

C#からイベントを受け取るグラフ

グラフにトリガーで呼び出すスクリプト

CustomEvent.TriggerメソッドでイベントとしてVisualStudioをトリガーすることができます

using Unity.VisualScripting;
using UnityEngine;

public class CustomEventCSharp : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // 第1引数はオブジェクト名、第2引数は呼び出したいイベント名、第3引数以降はハンドラに渡したい引数
            CustomEvent.Trigger(gameObject, "ChangeScene", "Game");
        }
    }
}

Visual ScriptからC#を操作

グラフから呼び出されるスクリプト

CustomEvent.TriggerメソッドでイベントとしてVisualStudioをトリガーすることができます

using UnityEngine;

public class VisualScriptToCSharpSample : MonoBehaviour
{
    public void CallShowMessage()
    {
        Debug.Log("呼び出されたよ");
    }
}

VisualScriptで使えるように再構築

Visual Scriptingの設定画面

Editメニュー → Project Settings…Visual Scriptingのページ

VisualScriptのグラフからアクセスしたいC#のクラスを追加

+ボタンをクリックして作成したC#スクリプトを選択します

追加されたクラスを含め再度構築する

C#のコードを呼び出すグラフ

メソッドを選択して、コントロールのラインで繋ぎます

引数を渡すサンプル

using UnityEngine;

public class VisualScriptToCSharpSample : MonoBehaviour
{
    public void CallShowMessage(string msg)
    {
        Debug.Log(msg);
    }
}

自作ノード

グラフで作る自作ノード

グラフ内にブラックボックス化したメソッドを追加することで実現します
イメージとしては、グラフ内にUnitを追加するのですが、そのUnitも自作ノードになっている例になります

完成形サンプル(エディター全体)

グラフ部分拡大

自作ノード(Subgraph Base)をダブルクリックすると…
ブラックボックスになっている内容が表示されます
今回は学習用なので、さらにサブグラフが入っています

さらに、ダブルクリックすると…
実体が見えてきました。これ以上はありませんよ

作成手順

Projectウィンドウで、新しくScript グラフを作成します
今回は、SubgraphBaseという名前を付けています

空のグラフができますので、右クリックからSubgraphを選択します

Subgraphが作成されます

ダブルクリックすると、展開されます
2つのノードがありますが、これは、外部とのやりとり(入口と出口、引数と戻り値のようなもの)のために使います

入力ポートの情報を入力していきます

入力サンプルになります

アウトプットポートも同様に入力していきます

文字列加工のグラフを作っていきましょう
C#でいうところのメソッドにあたります

参考)C#で記述した場合のサンプル

using UnityEngine;

public class SubgraphBaseSample : MonoBehaviour
{
    public string 文字列を加工するノード(string inputMessage)
    {
        string outputMessage = inputMessage + "が入力されました";
        return outputMessage;
    }
}

ノードに名前を付けておきましょう

一つ上の階層に移動しましょう

ルートの階層になります。今回は学習目的のサンプルなので、もう一段サブグラフ化しています

作成したサブグラフを使ってみよう

細かな手順は割愛しますが、新しくUseSubgraphステートグラフを新規で作成し、そこでノードを追加します

必要なノードを追加して、全体として完成したノードになります

実行中エディター

  1. ヒエラルキーで、からのゲームオブジェクトを作成
  2. Script Machineコンポーネントを追加
  3. 作成したUseSubgraphをアウトレット接続
  4. エディターの実行(▶︎ボタン)
  5. コンソール画面に表示される結果を確認

実行結果

ステートマシンと組み合わせることもできます

C#で作る自作ノード

コード

using Unity.VisualScripting;

public class MyNode : Unit
{
    protected override void Definition()
    {

    }
}

ノードの作成

  • プロジェクトで初めてビジュアルスクリプトを使用する前
  • ノードライブラリからノードを追加または削除した後
  • タイプオプションからタイプを追加または削除した後
  • ノードの入力または出力を変更した後

グラフで選択

グラフで右クリックすると、上記で生成されたノードを選択できるようになります

グラフに追加

選択するとグラフに追加されます

出力のあるコードの作成

コード
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

// Script Graph で表示されるUnitカテゴリ
[UnitCategory("自作ノード")]
// Unit名
[UnitTitle("1を出力するノード")]
public class MyAddUnit : Unit
{
    public ValueOutput Output { get; private set; }

    protected override void Definition()
    {
        Output = ValueOutput(nameof(Output), _ => 1);
    }
}
ノードの作成

基本のところで実行した再生成の作業をします

グラフで選択

上記生成で選択できるようになります

グラフのサンプル

コンソールに結果(1)を表示します

ポートの追加とコードの実行

ノードを作成するときは、ポートを追加する必要があります。

  • ユーザーが解釈するデータを追加できるようにする。
  • 現在のノードアクションが完了した後、ユーザーがノードと他のノードの両方をトリガーできるようにコントロールポートを設定する。

これを達成するために、ノードに4種類のポートを作成できます。

ポート:

  • ControlInput: ノードでロジックを実行するエントリポイント。
  • ControlOutput: このポートに接続されている次のアクションをトリガーします。
  • ValueInput: ノードに渡すあらゆる種類のデータ。
  • ValueOutput: ノードの外部に渡すあらゆる種類のデータ。

コントロールポートの追加

ポートをノードに表示するには:

  1. ControlInput 型と ControlOutput タイプの ports 変数を追加します。
  2. Definitionメソッドでポート変数を使用します。

コード

using Unity.VisualScripting;

public class ContolSample : Unit
{
    // ポートはシリアライズする必要がないです
    [DoNotSerialize]
    // ポート変数ControlInputの追加
    public ControlInput inputTrigger;

    [DoNotSerialize]
    // ポート変数ControlOutputを追加
    public ControlOutput outputTrigger;

    protected override void Definition()
    {
        // ControlInputポートを可視化し、そのキーを設定し、匿名アクションメソッドを実行して、outputTriggerポートにフローを渡します
        inputTrigger = ControlInput("inputTrigger", (flow) => { return outputTrigger; });

        // ControlOutputポートを可視化し、そのキーを設定します
        outputTrigger = ControlOutput("outputTrigger");
    }
}

これで、他のコントロールポートに接続できる基本的で機能的なフロールーティングノードが必要です。フロールーティングノードを使用してグラフ内のフローの方向を変更し、非構造化およびメンテナンスが困難な視覚的なソースコードを回避できます。

ノードの生成

基本のところで実行した再生成の作業をします

グラフで選択

上記生成で選択できるようになります

グラフのサンプル

値ポートの追加

値ポートを追加するには、ノードに入力するデータの種類を定義する必要があります。データには2つのタイプがあります。

  • 汎用: 任意の型を入力し、コードにデータを処理させるために使用できるオブジェクトタイプ。
  • タイプ値:文字列、整数、浮動小数点数などの特定のデータ型を適用するために使用されます。

以下のコード例では、タイプ文字列の2つの入力値とタイプ文字列の出力値が追加されています。デフォルト値を追加することもできます。以下の例では、myValueAのデフォルト文字列値は「Hello」に設定されています。

コード

using Unity.VisualScripting;

public class ValuePort : Unit
{
    [DoNotSerialize]
    public ControlInput inputTrigger;

    [DoNotSerialize]
    public ControlOutput outputTrigger;

    [DoNotSerialize]
    // myValueA の ValueInput 変数の追加
    public ValueInput myValueA;

    [DoNotSerialize]
    // myValueB の ValueInput 変数を追加
    public ValueInput myValueB;

    [DoNotSerialize]
    // 結果用変数ValueOutputの追加
    public ValueOutput result;

    // 処理結果値用文字列変数の追加
    private string resultValue;

    protected override void Definition()
    {
        inputTrigger = ControlInput("inputTrigger", (flow) => { return outputTrigger; });
        outputTrigger = ControlOutput("outputTrigger");

        //入力値ポートmyValueAを可視化し、ポートラベル名をmyValueAに、デフォルト値をHelloに設定
        myValueA = ValueInput<string>("myValueA", "Hello ");

        //入力値ポートmyValueBを可視化し、ポートラベル名をmyValueBに、デフォルト値を空文字列に設定
        myValueB = ValueInput<string>("myValueB", string.Empty);

        //結果出力値ポートを可視化し、ポートラベル名をresultとし、そのデフォルト値をresultValue変数に設定
        result = ValueOutput<string>("result", (flow) => { return resultValue; });
    }
}

現在のノードを接続しようとすると、フローはそれを通過しますが、resultValue変数の値が設定されていないため、Result値はnullです。

ノードの生成

基本のところで実行した再生成の作業をします

グラフで選択

上記生成で選択できるようになります

グラフのサンプル

ノード内でロジックを実行する

作成されたノードは、ControlInput nameinputTriggerがトリガーされると、2つの文字列を連結し、1つの文字列として出力します。これを行うには、ControlInput inputTriggerの割り当てを処理するラムダ式内にロジックを追加します。

例: コードフロー内に、この簡単な操作 (myValueA + myValueB) を追加します。

コード
using System;
using Unity.VisualScripting;

public class ExecuteLogic : Unit
{
    [DoNotSerialize]
    public ControlInput inputTrigger;

    [DoNotSerialize]
    public ControlOutput outputTrigger;

    [DoNotSerialize]
    public ValueInput myValueA;

    [DoNotSerialize]
    public ValueInput myValueB;

    [DoNotSerialize]
    public ValueOutput result;

    private string resultValue;

    protected override void Definition()
    {
        // inputTriggerポートがトリガーされたときにノードのアクションを実行するためのラムダです。
        inputTrigger = ControlInput("inputTrigger", (flow) =>
        {
            // myValueAとmyValueBを連結してresultValueを入力値と等しくする。
            resultValue = flow.GetValue<string>(myValueA) + flow.GetValue<string>(myValueB) + "!!!";
            return outputTrigger;
        });
        outputTrigger = ControlOutput("outputTrigger");

        myValueA = ValueInput<string>("myValueA", "Hello ");
        myValueB = ValueInput<string>("myValueB", String.Empty);
        result = ValueOutput<string>("result", (flow) => resultValue);
    }
}

現在のノードを接続しようとすると、フローはそれを通過しますが、resultValue変数の値が設定されていないため、Result値はnullです。

ノードの生成

基本のところで実行した再生成の作業をします

グラフで選択

上記生成で選択できるようになります

グラフのサンプル

実行可能な構成

リレーションの追加

リレーションは、ノードが内部フローを表示する方法です。リレーションを設定していないと、ノードをコントロールポートに接続するときに視覚的な問題が発生する可能性があります。ノードに追加できるリレーションには3つの異なるタイプがあります。

  • 割り当て: 必要なデータを出力するために必要なポートを表します。
  • 継承: 入力ポートから出力ポートへの実行パスを表します。グラフでDimオプションが有効になっている場合、継承を設定しない場合、接続ノードがグレーアウトされます。注: 視覚的な表現に関係なく、実行は続行されます。
  • 要件: 制御入力ポートを実行するために必要なデータを表します。

コード

using System;
using Unity.VisualScripting;

public class Relation : Unit
{
    [DoNotSerialize]
    public ControlInput inputTrigger;

    [DoNotSerialize]
    public ControlOutput outputTrigger;

    [DoNotSerialize]
    public ValueInput myValueA;

    [DoNotSerialize]
    public ValueInput myValueB;

    [DoNotSerialize]
    public ValueOutput result;

    private string resultValue;
    protected override void Definition()
    {
        inputTrigger = ControlInput("inputTrigger", (flow) =>
        {
            resultValue = flow.GetValue<string>(myValueA) + flow.GetValue<string>(myValueB) + "!!!";
            return outputTrigger;
        });

        outputTrigger = ControlOutput("outputTrigger");

        myValueA = ValueInput<string>("myValueA", "Hello ");
        myValueB = ValueInput<string>("myValueB", String.Empty);
        result = ValueOutput<string>("result", (flow) => resultValue);

        // それを表示するために、myValueAの値を設定して、ノードに処理させる必要があります
        Requirement(myValueA, inputTrigger);

        // ノードで処理させるためにmyValueBの値が設定されている必要があることを表示します
        Requirement(myValueB, inputTrigger);

        // 入力トリガポートの入力が、出力トリガポートの出口で終了することを表示します。継承を設定しないことで、接続されているノードもグレーアウトするが、実行はされる
        Succession(inputTrigger, outputTrigger);

        // inputTriggerがトリガーされたときに書き込まれるデータをresult string outputに表示する。
        Assignment(inputTrigger, result);
    }
}

現在のノードを接続しようとすると、フローはそれを通過しますが、resultValue変数の値が設定されていないため、Result値はnullです。

ノードの生成

基本のところで実行した再生成の作業をします

グラフで選択

上記生成で選択できるようになります

グラフのサンプル

実行可能な構成

ノードにドキュメントを追加する

ノードにドキュメントを追加する必要はありませんが、ユーザーはノードの目的を理解するのに役立ちます。

ファジーファインダーとグラフインスペクタで読み取ることができるポートのサマリーを追加するには、現在のユニティ用に新しいC#スクリプトを作成し、エディタフォルダ内に配置しますMyNodeDescriptorという名前の新しいC#スクリプトを作成し、次のコードをコピー&ペーストします。

コード

using Unity.VisualScripting;

[Descriptor(typeof(MyNode))]
public class MyNodeDescriptor : UnitDescriptor<MyNode>
{
    public MyNodeDescriptor(MyNode unit) : base(unit) { }

    protected override void DefinedPort(IUnitPort port, UnitPortDescription description)
    {
        base.DefinedPort(port, description);
        switch (port.key)
        {
            case "inputTrigger":
                description.summary = "myValueA と myValueB の連結をトリガし、結果の文字列を Result ポートに返す";
                break;
            case "myValueA":
                description.summary = "myValueA + myValueB の返り値の最初の文字列値";
                break;
            case "myValueB":
                description.summary = "myValueA + myValueBで返される結果の2つ目の文字列値";
                break;
            case "outputTrigger":
                description.summary = "myValueAとmyValueBを連結した後に次のアクションを実行する";
                break;
            case "result":
                description.summary = "myValueA と myValueB を連結した結果の文字列";
                break;
        }
    }
}

ノードの生成

基本のところで実行した再生成の作業をします

グラフで選択

上記生成で選択できるようになります

コメントが表示されるようになります

グラフのサンプル

コメントが表示されるようになります

ノードのカスタマイズ

ノードのカスタマイズは、ユーザーがグラフで何が起こっているのかを理解するのに役立つものです。ノードをカスタマイズするには、その上に属性を設定する必要があります。以下は、ノードの外観を変更するために使用できる属性のリストです。

ノードクラスの属性

属性はノードをカスタマイズするために使用され、ノードの前に書き込まれます。


属性: [UnitTitle(“YourTitle")]

情報: クラス名以外のものを使用する場合のノードのタイトル。


属性: [UnitShortTitle(“YourShortTitle")]

情報: UnitShortTitleはノードに表示され、グラフビューのタイトルを非表示にします。


属性: [UnitSubtitle(“Your Subtitle")]

情報: 現在のノードタイトルの下部に行くノードの字幕。


属性: [UnitCategory(“FirstLevel\SecondLevel")]

情報: ファジーファインダーでノードを見つけるための仮想パス。

メモ

ファジーファインダーを更新し、ファインダー内のノードの位置を変更するには、ノードを再生成する必要があります。


属性: [TypeIcon(typeof(GameObject)]

情報: Visual Scripting iconsライブラリからアイコンを取得します。これらのアイコンはユニティタイプによってライブラリに保存されます。

メモ

この属性から独自のカスタムアイコンを指すことはできません。

ポート属性

ポート属性は、ノードクラス内のControlInput、ControlOutput、ValueInput、ValueOutput変数の初期化の上に配置する必要があります。

public class MyNode : Unit
{
   [DoNotSerialize] // Mandatory attribute, to make sure we don’t serialize data that should never be.
   [PortLabelHidden] // Hiding the port label because we normally hide the label for default Input and Output triggers.
   public ControlInput inputTrigger;

属性: [DoNotSerialize]

: 該当なし

:該当なし

情報: すべてのポートに必須属性ポート。シリアル化を意図していないデータがシリアル化されないように必要です。


属性: [PortLabelHidden]

情報: ポートアイコンの側面にあるテキストラベルを非表示にします。

メモ

ラベルが非表示でない場合は、ポート変数が Definition: メソッドのキーと同じであることを確認してください。

例えば:

myValueA = ValueInput<string>("myValueA", "Hello ");

参考

C#,Visual Scripting

Posted by hidepon