【Unity】UIBuilderを使用してEditor拡張Window作成

今風にUIBuilderを使用してEditorWindowを作成してみることにしました。

  • Unity2021.2.3f1

UIBuilderとは

Overview | UI Builder | 1.0.0-preview.18

公式リファレンスを見てもらうのが一番

Editor拡張など、スクリプトで構築していたUIをビジュアルオーサリングツールであるUIBuilderでUIをぽちぽち置いて画面を構築します

Unity2021.1 以降は標準に含まれているようで今後UIBuilderをうまく使いこなすのがスタンダードな時代になってくるのかもしれないですね


画面構成をXMLである .umxl というファイルに保存し、見た目の細かい指定は スタイルシート(USS) で構築します

が、EditorWindowでは見た目を気にするユーザーもいないので今回USSは使用しません。

目標

目標として下画面を作成を目指します

f:id:toshizabeth:20211125001510p:plain

以下を達成させます

  • 使用者が任意にパスを指定できるようにする。
  • そのパスは手入力もできるし、「ドラッグ・アンド・ドロップ」エリアにフォルダorファイルを突っ込むと自動でそのパスが入力される
    • ファイルを突っ込んだ場合はファイルが有るフォルダパスになる。
  • 実行ボタンを押したら反応できるようにする
  • 実行時の確認用のログを出せるようにする
    • 実行ボタン下にログがリストで並ぶ

項目で分けると

  1. パスを入力するエリア
  2. デバッグログを表示するリストビュー

の2点の実装。

そしてA、Bは他EditorWindowでも使いまわしたいので一つの独立したクラスとして作成する。

です


前準備

UI画面構成のUXMLファイルを適用なところに作成します

Create > UI ToolKit > Editor Window

から UXML, CSS. .cs 全てまとめて作成できます

しかし自分の環境ではバージョンのせいか、MacOSだからかUnityがエラー吐くという.. (使えねえ

f:id:toshizabeth:20211125002528p:plain

とにかく UXML を作成したらそのファイルをダブルクリックするとUIBuilder が自動的に立ち上がります


↓ 目指すべき完成

f:id:toshizabeth:20211125002407p:plain

赤丸内の構成が中央のViewportに表示されてます


HierarchyViewとSceneView みたいな感じ。


A. パスを入力するエリア

テキスト入力UIである TextField というものはUIBuilderの標準に用意されてあります。

しかし、毎回パスの手入力は面倒くさいので「ファイルorフォルダ」を ドラッグ・アンド・ドロップ してそのパスを自動設定する機能がほしい。

ですが、もちろん標準にその機能はありません


ここは自分で機能を作成します

f:id:toshizabeth:20211125094925p:plain


UIBuilderのパーツとして使用する方法は VisualElement クラスを継承し、 uxmlファイルで使用できるように UxmlFactory クラスも定義します

VisualElement.UxmlTraits を継承したクラスも定義します

(UIBuilderのインスペクタ上に独自のパラメータを定義するために使用します)

using UnityEditor
using UnityEngine.UIElements;

// ドラッグ・アンド・ドロップによる操作も受け付ける
public class PathFieldElement : VisualElement
{
    // このようにクラス無いクラスで定義するだけで良い
    public new class UxmlFactory : UxmlFactory<PathFieldElement, UxmlTraits> { }

    // 独自パラメータを定義するために使う
    public new class UxmlTraits : VisualElement.UxmlTraits
    {
        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
        {
            base.Init(ve, bag, cc);
        }
    }
}

上記のクラスが基本形になります。ここに情報を肉付けしていきます


f:id:toshizabeth:20211128181232p:plain

まずはTextField。次にドラッグ・アンド・ドロップエリアを定義します


TextField

public class PathFieldElement : VisualElement
{
    public new class UxmlFactory : UxmlFactory<PathFieldElement, UxmlTraits> { }

    public new class UxmlTraits : VisualElement.UxmlTraits
    {
        public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
        {
            base.Init(ve, bag, cc);
        }
    }

    private TextField _sourceFullPath; // TextField

    // コンストラクタ
    public PathFieldElement()
    {
         _sourceFullPath = new TextField("パス");

        // ↓表示調整なのであってもなくても
         _sourceFullPath.style.marginTop = 10;
         _sourceFullPath.style.marginBottom = 10;

         Add(_sourceFullPath);
    }
}

これで PathFieldElement を配置したときにテキストフィールドが描画されます。とても楽


ドラッグ・アンド・ドロップエリア

同じように作成していきますが処理が長いため別メソッドにしました

private VisualElement CreateDragAndDropArea()
{
    return new IMGUIContainer(() =>
    {
        var evt = Event.current;

        var dragAndDropArea = GUILayoutUtility.GetRect(0.0f, 40.0f, GUILayout.ExpandWidth(true));

        var boxStyle = new GUIStyle(EditorStyles.textField)
        {
            alignment = TextAnchor.MiddleCenter
        };

        GUI.Box(dragAndDropArea, "ドラッグ・アンド・ドロップ", boxStyle);

        int id = GUIUtility.GetControlID(FocusType.Passive);

        if (!dragAndDropArea.Contains(evt.mousePosition)) return;

        switch (evt.type)
        {
            // 更新または実行
            case EventType.DragUpdated:
                {
                    DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
                    DragAndDrop.activeControlID = id;
                    break;
                }

            case EventType.DragPerform:

                DragAndDrop.AcceptDrag();

                // 最初のオブジェクトのみ判定する
                var obj = DragAndDrop.objectReferences.FirstOrDefault();
                if (obj != null)
                {
                    var assetPath = AssetDatabase.GetAssetPath(obj);

                    // ファイルの場合があるのでフォルダ名に変換する
                    if (!AssetDatabase.IsValidFolder(assetPath))
                    {
                        assetPath = System.IO.Path.GetDirectoryName(assetPath);
                    }

                    _sourceFullPath.value = assetPath;
                }

                DragAndDrop.activeControlID = 0;

                Event.current.Use();
                break;
        }
    });

これをTextFieldと同じくコンストラクタでAdd

    // コンストラクタ
    public PathFieldElement()
    {
         _sourceFullPath = new TextField("パス");

        // ↓表示調整なのであってもなくても
         _sourceFullPath.style.marginTop = 10;
         _sourceFullPath.style.marginBottom = 10;

         Add(_sourceFullPath);
        Add(CreateDragAndDropArea());
    }

これでOK


メソッド内の処理として、 IMGUIContainer を使用しています。

これは今までのEditor拡張で使用していた IMGUIをVisualElement として使えるように変換してくれるもので、これのおかげで今までの知識を活かしながらUIElementの実装ができます。

IMGUIContainer の初期化時のラムダの中で独自のEditor拡張UIを配置することでそれがそのまま描画されます。


つまりUIElementの機能にないものはIMGUIで作れば良い。ということです


これで UIBuilder のLibrary -> Project 内に 自分が定義した独自VisualElementが表示されます!

f:id:toshizabeth:20211128182829p:plain

あとはこれをUIBuilder上から配置します。


.uxml ファイル内を見てみると、UIBuiilderで配置した内容が反映されています。

このuxmlファイルをEditorWindowで使用します

        // 何らかのEditorWindow
    public class Hoge : EditorWindow
    {
        private const string UXMLPath = "{.uxmlファイルまでのパス}.uxml";

        [MenuItem("Utility/UI/Hoge")]
        public static void ShowExample()
        {
            var wnd = GetWindow<Hoge>();
            wnd.titleContent = new GUIContent("Hoge");
        }

        public void OnEnable()
        {
            // EditorWindowのRootにUXMLファイルの内容をぶら下げるイメージで
            var root = rootVisualElement;

            var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UXMLPath);
            var uxml = visualTree.CloneTree();

            // RootにAddする
            root.Add(uxml);
                }
        }

これで UIBuilder で編集した内容を EditorWindow で表示できます。


UIBuilderでEditorWindowの見た目を編集出来るのはとても便利になったと思います(逆に今までコード上で定義してたのが疑問に思える)

少しテキストフォントを拡大したい。ボタンを大きくしたい。といったこともコードで調整していましたが今後はBuilder上で触るだけで良い。

今回のように機能を自作できることで部品として使い回せるのも利点ではないでしょうか。


今回の実装だけではパスをEditorWindow上から触れないため、EditorWindowでパスを取得する処理を書く必要があります

その実装は次回書きたいと思います

【Unity】VisualScriptingにスキル攻撃のダメージ計算を任せてみる

VisualScripting(旧 BOLT)を利用してダメージ計算処理をノード設計できるようにし、誰でもさわれるようにする実験をしてみました

github.com

結論。

問題なく分離でき「ダメージ計算」「敵AI処理」など単一処理をVisual化するのはとても素晴らしいことだと認識できました。


前置き

ダメージ計算処理はよくある(?)計算クラスとして分離して書こうと思っていましたが

    /// <summary>
    /// スキルに関する計算関連
    /// </summary>
    public static class CharaSkillCalculator
    {
        /// <summary>
        /// unitがopponentに与えるダメージを計算して返す
        /// </summary>
        public static long Calculate(ICharaController unit, ICharaController opponent)
        {
            // 今は単純に引くか
            return Math.Max(unit.Status.TotalAttack - opponent.Status.TotalDefence, 0);
        }
    }

まだ大したコードを書いていなかったこと、 そして計算部分をVisualScriptingに任せることで利点が出るのではと思いVisualScriptingで計算処理を組み立てることにしました


考えうる利点

  1. 計算式のコード変更がいらなくなるのでコンパイルが走らない
    1. 「倍率値いじるためにスクリプト変更→コンパイルイライラタイム」が無い
  2. VisualScriptingのUnitをつなぎを変えるだけで処理の変更ができる。Visual化による処理の可視化
  3. ログユニット付けてログ出したり、デバッグ処理を付けたりも簡単にできる
  4. プログラマー以外が触れる

プログラマーとしては1だけでもやる価値あった


実装

ダメージ計算処理は別で切り離したいので SkillCalculater というGameObjectに ScriptMachine を付ける。

そしてスクリプト上から計算処理を呼び出すために SkillCalculater MonoBehaviour クラスを作成。

f:id:toshizabeth:20211114192219p:plain

計算処理呼び出しSkillCalculaterクラスはZenjectでDI管理。

/// <summary>
/// スキル計算について持つ
/// </summary>
public interface ISkillCalculater
{
    /// <summary>
        /// スキルダメージを計算する
        /// </summary>
    UniTask<int> ExecuteAsync(ICharaController unit, ICharaController target, SkillDamageEntity entity);
}

/// <summary>
/// ダメージスキル計算のイベント
/// </summary>
public interface IDamageCalculateEvent
{
    /// <summary>
        /// 与えるダメージ量
        /// </summary>
    void SetCalcDamageValue(int damage);
}


/// <summary>
/// スキル計算について持つ
/// </summary>
public class SkillCalculater : MonoBehaviour, ISkillCalculater, IDamageCalculateEvent
{
    private bool _isFinish;
    private int _value;


    async UniTask<int> ISkillCalculater.ExecuteAsync(ICharaController unit, ICharaController target, SkillDamageEntity entity)
    {
        // 呼び出し
        CustomEvent.Trigger(gameObject, "Execute", this, entity, unit, target);

        // 終了するまで待機する
        if (!_isFinish)
        {
            await UniTask.WaitUntil(() => _isFinish);
        }
        
        return _value;
    }

    void IDamageCalculateEvent.SetCalcDamageValue(int value)
    {
        _value = value;

        _isFinish = true;
    }
}

DI管理

   public class CharaInstaller : MonoInstaller
    {
        public override void InstallBindings()
        {
....

            Container
                .Bind<Skill.ISkillCalculater>()
                .FromComponentInHierarchy()
                .AsSingle();
        }
    }


メインは ExecuteAsync メソッドでここでVisualScripintのCustomTriggerを叩いています。

まだまだVisualScriptingについては疎いため、以下のようにTriggerの後は計算処理が帰ってくるまでフラグ待機をしています(他のやり方が思い浮かばない)

WaitUntil は強制1f待機するため即時終了する場合を考慮してif入れてます (ここらへんは雑に対応)

// VisualScriptingの"Execute"呼び出し
CustomEvent.Trigger(gameObject, "Execute", this, entity, unit, target);

// 終了するまで待機する
if (!_isFinish)
{
    await UniTask.WaitUntil(() => _isFinish);
}


計算処理に必要なのは

  • スキル情報
  • 攻撃者
  • ターゲット

上記をVisualScripting側に伝えてTrigger。

※ VisualScriptでUnitとして使うクラスはVisualScriptに登録すること


f:id:toshizabeth:20211114193537p:plain

CustomEvent Unit で Execute という名前で受けて。4つの引数をそれぞれ SetVariableしてどこでもさわれるようにする


ここまで来たら後は if 文で 「固定ダメージ」かそうじゃないかで分岐したりしてダメージ結果をスクリプト側に返すと。

f:id:toshizabeth:20211114193844p:plain

↑ 矢印先まで来たらこの処理は終わり。(今はif文分岐先どちらも同じ処理)


これだけで計算処理をVisualScript側に移動することができました


後は使用するクラスにまずは Inject して

       [Inject] private Chara.Skill.ISkillCalculater _skillCalculater;

実際に処理を書く部分で ExecuteAsync メソッドを await

       {
            foreach (var target in _targets)
            {
                // HPをへらす
                var damage = await _skillCalculater.ExecuteAsync(_chara, target, _entity);

                await target.Damage(damage);

                // 死亡しているか
                if (target.Status.IsDead.Value)
                {
                    _deadChara.Add(target);
                }

                await UniTask.Delay(500);
            }

こんな感じにする。

以上で楽に処理をVisual化できました。


作業量もほぼかからず、これだけで様々な利点を享受できるのであればVisualScriptingにロジック処理を書くのも全然ありだと感じました。


終わり

VisualScriptingにロジックを書く上でUniTask、Zenjectのパワフルさを利用することで更に便利に実装できる。


逆に大変なのはテストが書きにくいことかなと思いました

(どうしてもPlayModeテストになる?)

【Unity】VisualScripting バージョン管理

前回の記事の続きの前に

toshizabeth.hatenablog.com

VisualScriptingを使って色々遊んでいた後、Gitを確認すると大量のファイルが生成されていた
(Assets/Unity.VisaulScripting.Generated フォルダ配下)

ファイル数が多いのと、特に UnitOptions.db が30MB近く存在しているのが問題。
このままバージョン管理するのはいかがなものかと思いリファレンスを確認しました

docs.unity3d.com

バージョン管理から除外する必要があるんですね(最初からそうしてほしい)

注:プロジェクトにパブリックリポジトリを使用している場合は、以下の手順でビジュアルスクリプトファイルを除外する必要があります。そうしないと、ビジュアルスクリプトをオンラインで違法に再配布し、Unity AssetStoreのEULAとToSに違反することになります。

なかなか熱い注意文!!!
いや、これは気づかない人いそうですが...

おとなしく除外する設定を入れようと思います。
.gitignoreに設定を追加します

VisualScriptingの設定からUnitを生成した時に吐き出されるファイルすべて除外対象

f:id:toshizabeth:20211002231928p:plain

.gitignore に以下を記述れば良いようですが、標準のUnityの設定も混じっており

    ## Bolt

    # Optionally exclude these transient (generated) files, 
    # because they can be easily re-generated by the plugin

    Assets/Bolt.Generated/VisualScripting.Flow/UnitOptions.db
    Assets/Bolt.Generated/VisualScripting.Flow/UnitOptions.db.meta
    Assets/Bolt.Generated/VisualScripting.Core/Property Providers
    Assets/Bolt.Generated/VisualScripting.Core/Property Providers.meta

    ## Unity
    # From: https://github.com/github/gitignore/blob/master/Unity.gitignore

    [Ll]ibrary/
    [Tt]emp/
    [Oo]bj/
    [Bb]uild/
    [Bb]uilds/
    Assets/AssetStoreTools*

    # Visual Studio cache directory
    .vs/

    # Autogenerated VS/MD/Consulo solution and project files
    ExportedObj/
    .consulo/
    *.csproj
    *.unityproj
    *.sln
    *.suo
    *.tmp
    *.user
    *.userprefs
    *.pidb
    *.booproj
    *.svd
    *.pdb
    *.opendb

    # Unity3D generated meta files
    *.pidb.meta
    *.pdb.meta

    # Unity3D Generated File On Crash Reports
    sysinfo.txt

    # Builds
    *.apk
    *.unitypackage

VisaulScriptingだけであれば

    ## Bolt

    # Optionally exclude these transient (generated) files, 
    # because they can be easily re-generated by the plugin

    Assets/Bolt.Generated/VisualScripting.Flow/UnitOptions.db
    Assets/Bolt.Generated/VisualScripting.Flow/UnitOptions.db.meta
    Assets/Bolt.Generated/VisualScripting.Core/Property Providers
    Assets/Bolt.Generated/VisualScripting.Core/Property Providers.meta

ここだけですね。 ところがよく見たらBoltの名前のままですね
最新のVisualScriptingを使用している場合 namespaceも Unity.VisualScripting に変わっているため正しくは

## VisualScripting

# Optionally exclude these transient (generated) files, 
# because they can be easily re-generated by the plugin

Assets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db
Assets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta
Assets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers
Assets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta

です。( Unity2021.2.0b13 )
これをgitignoreに追加して保存すると大量に出ていたファイルが全て消えます

と思ったらなんか残ってます。

f:id:toshizabeth:20211002235410p:plain

f:id:toshizabeth:20211002235742p:plain

フォルダのmetaデータ。
ということでもう3つ追加します

## VisualScripting

# Optionally exclude these transient (generated) files, 
# because they can be easily re-generated by the plugin

Assets/Unity.VisualScripting.Generated.meta
Assets/Unity.VisualScripting.Generated/VisualScripting.Flow.meta
Assets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db
Assets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta
Assets/Unity.VisualScripting.Generated/VisualScripting.Core.meta
Assets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers
Assets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta

もしすでに追加しちゃった場合は git コマンド

git rm -r --cached . 
git add .
git commit -am "Remove ignored files"

で削除するコミットを作成できますとも書いてますね


注:ファイルをパブリックリポジトリにプッシュした場合、この手順ではコミット履歴からファイルが消去されません。あなたはまだビジュアルスクリプトファイルを違法に再配布しています。リポジトリをプライベートにするか、ビジュアルスクリプトファイルが含まれていない新しいツリーで新しいリポジトリを開始します。

そうなんですけど中々熱い警告文...
それならば初めからバージョン管理として吐き出されない方法は取れないのだろうか..

それかGithubに履歴からも完全に消し去る機能がほしいですね(無理か)

【Unity】自作のVisualScriptingグラフユニットを作成する

はじめに

グラフ内のユニットをスクリプトを使用して自分で作成できます
今回はそのカスタムユニットの作成をしてみます

※以下公式リファレンスを参照 docs.unity3d.com

ケルトンユニット作成

何もしない空ユニットを作成してみます
適当なスクリプトに以下を記述

using Unity.VisualScripting;

public class MyUnit : Unit
{
    // このユニットの挙動を記述するメソッド
    protected override void Definition()
    {
    }
}

自分でユニットを作成した場合、使用するためには登録をしなければいけません。
以前 TextMeshPro を使用する為にしたことをします

Edit > ProjectSettings > VisualScripting

Regenerate Unitsをクリックして登録します
スクリプト内を走査して Unit を継承しているスクリプトを見つけて登録してくれます)

f:id:toshizabeth:20210927191748p:plain

(自分の環境 Unity2021.2.0b13 だと Regenerate Nodes と名前が違うんですよね.. おそらく次のバージョンで変わるのかな)

適当なグラフ内で検索ボックスを開くと一番下に自分が作成したユニットが配置されます f:id:toshizabeth:20210927194328p:plain

入力も出力も指定してないので配置してもただの箱になってます

f:id:toshizabeth:20210927195217p:plain

次に

  1. 入力ポートと出力ポートを付ける
  2. 値の受け取りと送信をする
  3. ユニット内にロジックを入れる
  4. 検索の改装を変更する

を順を追ってやっていこうと思います

1. 入力ポートと出力ポートを付ける

  • ControlInput : ユニットを実行するためのエントリポイントポート
  • ControlOutput 次ユニットに処理を移動させるポート

変数に各ポートを宣言し、Definitionでそれぞれ定義します

using UnityEngine;
using Unity.VisualScripting;

public class MyUnit : Unit
{
    private ControlInput _inputTrigger;        // 入力側のポート
    private ControlOutput _outputTrigger;  // 出力側のポート


    // このユニットの挙動を記述するメソッド
    protected override void Definition()
    {
        // 入力側
        _inputTrigger = ControlInput("InputTrigger", _ => _outputTrigger);

        // 出力側
        _outputTrigger = ControlOutput("OutputTrigger");
    }
}

これでポートが付いたユニットが完成します
これだけではただつなぐことができる箱です

f:id:toshizabeth:20210928003711p:plain

2. 値の受け取りと送信をする

  • ValueInput : ユニットに渡す入力値
  • ValueOutput : 次のユニットに渡す出力値

次に、値を複数、入力と出力に定義します
これは関数の引数と戻り値と考えるとしっくり来るのではないかと思います

using UnityEngine;
using Unity.VisualScripting;

public class MyUnit : Unit
{
    private ControlInput _inputTrigger;        // 入力側のポート
    private ControlOutput _outputTrigger;  // 出力側のポート

    private ValueInput _valueA; // 入力値
    private ValueInput _valueB; // 入力値
    private ValueOutput _resultA; // 出力値
    private ValueOutput _resultB; // 出力値


    // このユニットの挙動を記述するメソッド
    protected override void Definition()
    {
        // 入力側
        _inputTrigger = ControlInput("InputTrigger", _ => _outputTrigger);

        // 出力側
        _outputTrigger = ControlOutput("OutputTrigger");


        // 文字列の入力
        _valueA = ValueInput<string>("StringValueA", "Hello");     

        // 数値の入力
        _valueB = ValueInput<int>("IntValueB", 0);

        // 文字列の出力
        _resultA = ValueOutput<string>("StringResultA", _ => "固定文字列を返す");

        // 数値の捨つ力
        _resultB = ValueOutput<float>("FloatResultB", _ => 1.23f);
    }
}

このように、入力側と出力側の設定をしてみました
出力側はラムダで返す値を設定します。今回は固定値を返しています

ユニットの見た目は以下のようになりました
値の連結ができるようになっています

f:id:toshizabeth:20210928004356p:plain

3. ユニット内にロジックを入れる

現在はただ値を受け取って固定の値を返すだけですが、値を足し合わせる処理を入れてみます。
入力ポートのラムダ内に処理を記述していきます

using UnityEngine;
using Unity.VisualScripting;

public class MyUnit : Unit
{
    private ControlInput _inputTrigger;        // 入力側のポート
    private ControlOutput _outputTrigger;  // 出力側のポート

    private ValueInput _valueA; // 入力値
    private ValueInput _valueB; // 入力値
    private ValueOutput _resultA; // 出力値
    private ValueOutput _resultB; // 出力値

    private string _resultValue;


    // このユニットの挙動を記述するメソッド
    protected override void Definition()
    {
        // 入力側
        _inputTrigger = ControlInput("InputTrigger", flow => 
            {
                // 入力値を足し合わせるだけ
                _resultValue = $"{flow.GetValue<string>(_valueA)}_{flow.GetValue<int>(_valueB)}";
                
                return _outputTrigger;
            });

        // 出力側
        _outputTrigger = ControlOutput("OutputTrigger");


        // 文字列の入力
        _valueA = ValueInput<string>("StringValueA", "Hello");     

        // 数値の入力
        _valueB = ValueInput<int>("IntValueB", 0);

        // 文字列の出力
        _resultA = ValueOutput<string>("StringResultA", _ => _resultValue); // 連結した文字列を返す

        // 数値の捨つ力
        _resultB = ValueOutput<float>("FloatResultB", _ => 1.23f);
    }
}

inputTrigger の初期化時のラムダ内で入力値と出力値を足し合わせています。
そして
resultA の戻り値には足し合わせた文字列を返します。
これで次のようにして実行します

f:id:toshizabeth:20210928094505p:plain

結果、内部で変換した文字列がログに出るようになりました

f:id:toshizabeth:20210928094531p:plain

関係性の追加(視覚的サポート)

Requirement

入力値に何も設定されていない時、または何もつながっていない時に警告を出してくれます
例えば、以下のような入力値を宣言した時

protected override void Definition()
{       
.....
    // これを追加     
    var test = ValueInput<string>("Test", null);
}

グラフ上に変化はありません。

f:id:toshizabeth:20211002193309p:plain

Testという入力値は string型で、デフォルト引数は null が指定されています。
nullの時、つまり何も値がない場合には警告を出すようにします

protected override void Definition()
{       
.....
    // これを追加     
    var test = ValueInput<string>("Test", null);

    Requirement(test, _inputTrigger);
}

Requirement で test を _inputTrigger と関係を付けることで、何も値がないときはユニットが黄色くなり Graph Inspector 上でも警告が出るようになりました

f:id:toshizabeth:20211002193514p:plain

(個人的にはもう少し強めの警告色でも良かったかも。気づかずにスルーしそう)

Assignment

... これがよくわからず.. リファレンス見る限りアウトプット側に対して何らかの表示があるっぽいですが特に変化がわからない..

Succession

自分のユニットを作成して他のユニットと結びつけた時、結びつけ先のユニットがグレイアウトしていると思います

f:id:toshizabeth:20211002185950p:plain

グレイアウトしているユニットの詳細を見てみると警告が出ています。

f:id:toshizabeth:20211002190443p:plain

「前ユニットの出力(Output Trigger)からこのユニットに来ることがない」と判断されてグレイアウトされています。
そこで、『自分のユニットは入力があれば必ず出力します!』ということを教えてあげます。

Definitionメソッドに以下を追加

protected override void Definition()
{       
.....
    // これを追加     
    Succession(_inputTrigger, _outputTrigger);
}

これで入力ポートになにか接続されていればoutput先がグレイアウトされなくなります

入力ポートが接続されている時
f:id:toshizabeth:20211002191034p:plain

入力ポートに何も接続されてないと自分含めグレイアウトします

入力ポートが接続されていない時
f:id:toshizabeth:20211002191112p:plain

ドキュメントの追加

グラフでユニットを選択した時に出る Graph Inspector にコメントが表示されるようになります
ユニットとは別のスクリプトに記述する必要があるので MyUnitDescriptor というスクリプトを新規に作成します

※ Editorフォルダ配下に配置する必要があります

using Unity.VisualScripting;

[Descriptor(typeof(MyUnit))] //対象指定
public class MyUnitDescriptor : UnitDescriptor<MyUnit>
{
   public MyUnitDescriptor(MyUnit unit) 
    : base(unit) 
   {}

   protected override void DefinedPort(IUnitPort port, UnitPortDescription description)
   {
       base.DefinedPort(port, description);

       switch (port.key)
       {
           case "InputTrigger":
               description.summary = "ユニットに接続した時の機能説明";
               break;
               
           case "OutputTrigger":
               description.summary = "ユニットからの出力先に接続した時の説明";
               break;

           case "StringValueA":
               description.summary = "入力値に対しての説明";
               break;
               
           case "IntValueB":
               description.summary = "入力値に対しての説明";
               break;

           case "StringResultA":
               description.summary = "出力値に対しての説明";
               break;

           case "FloatResultB":
               description.summary = "出力値に対しての説明";
               break;
       }
   }
}

これでGraph Inspectorを見ると..

f:id:toshizabeth:20211002201454p:plain

Unitだけ表示されても何を入力してどう出力するのかがわかりにくいため、コメントによる補佐は積極的に使っていきたいですね


長くなったので残りは次回

【Unity】VisualScripting Events unit について 2

はじめに

toshizabeth.hatenablog.com

これの続き
Event Unit についてのまとめ

Animation Events

アニメーションに付けたイベントトリガーを検知します

試しに実装してみます
以下のようにCubeがスケールするアニメーションを作成して

f:id:toshizabeth:20210927123445g:plain

半分の値のところにアニメーショントリガーを仕込みます

f:id:toshizabeth:20210927123532p:plain

インスペクタから TriggerAnimationEvent を選択します f:id:toshizabeth:20210927123603p:plain

今回は以下ようにパラメータを指定

f:id:toshizabeth:20210927145909p:plain

これでグラフ上からアニメーションイベントを参照できるようになりました。
起動したら速攻アニメーションするように以下のようにEntryから直つなげしています

f:id:toshizabeth:20210927145837p:plain

ユニット

アニメーションユニットは二種類あり Events > Animation

  • AnimationEvent
  • Named AnimationEvent

f:id:toshizabeth:20210927145147p:plain

名前を入れる欄があるユニットは、同じ文字列を持つアニメーションイベントのみを受け入れます
まずは名前がいらない方でテストしてみます。

f:id:toshizabeth:20210927150312p:plain

これで起動することでログが表示されます。

f:id:toshizabeth:20210927150327p:plain

イベントが有るたびに起きるようにしているので、アニメーションイベントを2つ配置した場合 f:id:toshizabeth:20210927150422p:plain

2回ログを出す処理が走ります

f:id:toshizabeth:20210927150449p:plain

Named Animation Event

イベントの区別をするために名前指定の Named Animation Event を使用します。

2つ目のアニメーショントリガーのstringに指定した「イベント2」という文字列を入れて

f:id:toshizabeth:20210927150752p:plain

f:id:toshizabeth:20210927150833p:plain

実行すると

f:id:toshizabeth:20210927150858p:plain

「イベント2」という文字列を入力したイベントトリガーのみにこのユニットが反応します
Named Animation Event は入力で文字列を受け取れるようになっているので条件によってフィルター名を変えることもできそうです

Event API

C#スクリプトからカスタムイベントを呼び出します

以下のような Custom Event (名前は TestEvent としてる)をグラフに配置して f:id:toshizabeth:20210927154104p:plain

スクリプト側からトリガーを叩いてみます
適当にスクリプトを作成して上記のグラフがついているGameObjectにアタッチします

using UnityEngine;
using Unity.VisualScripting;

public class CustomEventTriggerTest : MonoBehaviour
{
    void Start()
    {
        // 第一引数はVisualScriptがあるGameObject
        CustomEvent.Trigger(gameObject, "TestEvent", "スクリプトから呼び出した");
    }
}

f:id:toshizabeth:20210927154215p:plain

VisualScriptingのメソッド群は Unity.VisualScripting 内にあります。

実行すると呼び出されます

f:id:toshizabeth:20210927154319p:plain

終わり

今回はアニメーションイベントとスクリプト側のトリガーの紹介をしました
VisualScriptingを見ていくにつれて、完全にVisualScriptingでゲームをつくるというよりスクリプトとのハイブリットで書く使い方が良いように思えます

【Unity】VisualScripting Events unit について 1

はじめに

  • 画像をクリックしたときに反応する
  • ある文字列の通知が投げられたら反応する
  • 開始時に一度だけ呼び出される (Start)
  • 毎フレーム呼び出される (Update)

などなど
VisualScriptingにはいくつものイベントが存在しており Events unit と呼ばれるそれらの紹介をしていこうと思います

f:id:toshizabeth:20210924094254p:plain

※ 以下の公式リファレンスを参照します

Events unit | Visual Scripting | 1.7.3

イベントユニットは緑色で表示される

Lifecycle

Events > Lifecycle

f:id:toshizabeth:20210924095416p:plain

グラフにおいて最初に起動される。
ライフサイクルイベントはUnityのMonobehaviourにある Start, Update, OnEnable などを再現している

f:id:toshizabeth:20210925235851j:plain

※ Monobehaviourの挙動と同じなため簡潔に説明します

OnStart

グラフが起動した瞬間に一度だけ呼び出されるユニット

OnUpdate

GameObejctがアクティブな状態の時、毎フレーム呼び出される

OnFixedUpdate

0.02秒ごとに一度呼び出されるUpdate(一秒に50回)
つまり特定の期間に一度呼び出されるUpdate

OnLateUpdate

すべてのUpdate, OnFixedUpdate処理の後に走るUpdate

OnEnable

オブジェクトがアクティブ化されるたびに呼び出される

OnDisable

オブジェクトが非アクティブ化されるたびに呼び出される

OnDestroy

オブジェクトが削除されたときに呼び出される


挙動を確認するには以下のように単純なログ出力を組むとわかりやすいと思います

f:id:toshizabeth:20210926160557p:plain

f:id:toshizabeth:20210926160639g:plain

Inputs & Outputs

  • ボタンが押された時
  • マウスがクリックされた時
  • 通知された時

何らかのトリガーがあった時に開始するユニット群

キーボードで特定の入力があった時

Events > Input > OnKeybordInput

f:id:toshizabeth:20210926163025p:plain

※ マウスの入力も同じ

UGUIのボタンが押された時

画面上に配置したボタンが押された時に反応するようにします

f:id:toshizabeth:20210926164308p:plain

Events > GUI > Button Click

ボタンに Script Machine を取り付けている場合は Button Click ユニットそのままでいいですが
ボタンと違うオブジェクトのScriptMachineが押されたことを検知したい場合は、ボタンを参照するために SceneVariables に登録をします

f:id:toshizabeth:20210926164517p:plain

※ Type は GameObjectでもButtonでも大丈夫です

これでScene全体からこのUGUIボタンが参照できるようになったので
以下画像のように Get Variables で TestButtonを参照し OnButtonClickユニットと結びつけます

f:id:toshizabeth:20210926164700p:plain

UGUI内部にはその他UI変化があった時にトリガーされるユニットが用意されているので見ておくと良さげです

f:id:toshizabeth:20210926165024p:plain

Custom Events

個人的にイベントの中で最も重要だと思うのがCustomEvents
(よく使いそうなため)

あるグラフから別のグラフに対して

「プレイヤーがダメージを受けた」
「スキルを使用した」
などのユーザーがカスタムした通知を送りたい時に使用できる


ユニットの説明

Trigger Custom Event → イベントを送るユニット
Custom Event → イベントを受け取るユニット

f:id:toshizabeth:20210926173947p:plain

Trigger Custom Event ユニットで特定の名前のイベントを通知して、Custom Event ユニットで名前を指定したイベントを受け取る。


今回はボタンにScriptMachineを付けて、

f:id:toshizabeth:20210926190921p:plain

  1. ボタンが押された時に"TestButtonClick"というCustomEventを投げる。
  2. この時 "文字列" と "数値" をセットに送る
  3. 別のScriptMachineで "TestButtonClick" イベントを受け取って 文字列と数値 をログに出す

処理をしてみようと思います

まず、UGUIのボタンにScriptMachineを付けて中で OnButtonClickTrigger Custom Event を結びつけます
今回は Button コンポーネントに ScriptMachine を付けたので Get Variable する必要はありません。

f:id:toshizabeth:20210926180348p:plain

イベント名は "TestButtonClick" とします

文字列と数値を送るために、Trigger Custom Event ユニットの Arguments を 2 に変更し
string と integer ユニットを配置して結びつけます (Arg0, Arg1)

これでボタンが押された時にイベントが投げられるようになりました

Trigger Custom Event にはイベントを受け取る側のオブジェクトを付けることができます
今回は別のScriptMachineにイベントを送りたいため、対象のオブジェクトを Scene に登録して

f:id:toshizabeth:20210926190001p:plain

Get Variable ユニットでつなげます

f:id:toshizabeth:20210926190428p:plain


次にイベントを受け取る方を実装します

イベントを受け取るGameObjectにScriptMachineを付けて以下画像のように組み立てます

f:id:toshizabeth:20210926190215p:plain

Custom Event で TestButtonClick 名のイベントを受け取るようにして
Arg0, 1 を String.Concat ユニットで連結したものをログで出力します


結果

ボタン押した時にイベントの送受信が確認できます

f:id:toshizabeth:20210926190616g:plain

f:id:toshizabeth:20210926190635g:plain

【Unity】VisualScriptingとTextMeshProを使用してメッセージの文字送りを実現する

概要

toshizabeth.hatenablog.com

こちらの続き

www.youtube.com

こちらの内容から。
メッセージのテキスト送りについての手順について

f:id:toshizabeth:20210921095340g:plain

はじめに

TextMeshProには設定したテキストの「表示テキスト数」が指定できる

private TextMeshPro _text;

private void Update()
{
     // 最大可視化文字を指定。ここをアニメーションで増やしていくと表示文字が増えていく演出になる
     _text.maxVisibleCharacters = 1; 
}

この maxVisibleCharacters を VisualScripting上から操作します

Timerユニットを取り付ける

「○秒に○文字出す」という条件にするために、Timerユニットが使用できます

docs.unity3d.com

これのStartを SetText ユニットに取り付けます

f:id:toshizabeth:20210923132617p:plain

TimerUnit

機能名 説明
Started StartがOnになったら呼び出される
Tick タイマーがアクティブの間毎フレーム呼び出される
Completed 完了したら呼び出される
Elapsed タイマーが開始されてからの時間を返す
Elapsed(%) タイマーが開始されてからの時間を0〜1の範囲で返す
Remaining 残り時間を返す
Remaining(%) 残り時間を0〜1の範囲で返す
Duration カウント時間
UnScaled チェックを入れるとタイムスケールの無視

テキストが設定された瞬間の表示文字は0
「10秒で40文字出す」という条件を追加します

Timerから先は以下

f:id:toshizabeth:20210923141312p:plain

「10秒カウント」したいのでTimerのDurationには10を入れる

まず、Timerの開始時に表示テキスト数を0にしたいので、StartedからSetMaxVisibleCharactersユニットを付ける。
SetMaxVisibleCharactersユニットが

_text.maxVisibleCharacters = 1; 

maxVisibleCharactersへの設定の役目をしてくれるわけですね
表示数は0としたいので値は0。

更新のたびに SetMaxVisibleCharacters を呼び出してもらうようにもう一つ配置して Tick と結びつける。

表示数の指定は残り時間を返す Elapsed % を使用。
Elapsed %はTimerの残り秒数に応じて 0〜1 の値を返してくれるので、それをMultiplyユニットと結びつけることで Elapsed% x 40 = 表示文字数 を実現しています

40は「10秒で40文字出す」の40。


0秒目のときは0なので 0 x 40 = 0 文字表示
1秒目のときは0.1なので 0.1 x 40 = 4 文字表示
10秒目のときは 1 なので 1 x 40 = 40 文字表示


という扱いになります
これをSetMaxVisibleCharactersユニットの文字数に指定してあげることで「10秒で40文字出す」が達成できます