【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は使用しません。
目標
目標として下画面を作成を目指します
以下を達成させます
- 使用者が任意にパスを指定できるようにする。
- そのパスは手入力もできるし、「ドラッグ・アンド・ドロップ」エリアにフォルダorファイルを突っ込むと自動でそのパスが入力される
- ファイルを突っ込んだ場合はファイルが有るフォルダパスになる。
- 実行ボタンを押したら反応できるようにする
- 実行時の確認用のログを出せるようにする
- 実行ボタン下にログがリストで並ぶ
項目で分けると
- パスを入力するエリア
- デバッグログを表示するリストビュー
の2点の実装。
そしてA、Bは他EditorWindowでも使いまわしたいので一つの独立したクラスとして作成する。
です
前準備
UI画面構成のUXMLファイルを適用なところに作成します
Create > UI ToolKit > Editor Window
から UXML, CSS. .cs 全てまとめて作成できます
しかし自分の環境ではバージョンのせいか、MacOSだからかUnityがエラー吐くという.. (使えねえ
とにかく UXML を作成したらそのファイルをダブルクリックするとUIBuilder が自動的に立ち上がります
↓ 目指すべき完成
赤丸内の構成が中央のViewportに表示されてます
HierarchyViewとSceneView みたいな感じ。
A. パスを入力するエリア
テキスト入力UIである TextField
というものはUIBuilderの標準に用意されてあります。
しかし、毎回パスの手入力は面倒くさいので「ファイルorフォルダ」を ドラッグ・アンド・ドロップ してそのパスを自動設定する機能がほしい。
ですが、もちろん標準にその機能はありません
ここは自分で機能を作成します
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); } } }
上記のクラスが基本形になります。ここに情報を肉付けしていきます
まずは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が表示されます!
あとはこれを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)を利用してダメージ計算処理をノード設計できるようにし、誰でもさわれるようにする実験をしてみました
結論。
問題なく分離でき「ダメージ計算」「敵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で計算処理を組み立てることにしました
考えうる利点
- 計算式のコード変更がいらなくなるのでコンパイルが走らない
- VisualScriptingのUnitをつなぎを変えるだけで処理の変更ができる。Visual化による処理の可視化
- ログユニット付けてログ出したり、デバッグ処理を付けたりも簡単にできる
- プログラマー以外が触れる
プログラマーとしては1だけでもやる価値あった
実装
ダメージ計算処理は別で切り離したいので SkillCalculater
というGameObjectに ScriptMachine を付ける。
そしてスクリプト上から計算処理を呼び出すために SkillCalculater
MonoBehaviour クラスを作成。
計算処理呼び出し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に登録すること
CustomEvent Unit で Execute
という名前で受けて。4つの引数をそれぞれ SetVariableしてどこでもさわれるようにする
ここまで来たら後は if 文で 「固定ダメージ」かそうじゃないかで分岐したりしてダメージ結果をスクリプト側に返すと。
↑ 矢印先まで来たらこの処理は終わり。(今は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 バージョン管理
前回の記事の続きの前に
VisualScriptingを使って色々遊んでいた後、Gitを確認すると大量のファイルが生成されていた
(Assets/Unity.VisaulScripting.Generated
フォルダ配下)
ファイル数が多いのと、特に UnitOptions.db
が30MB近く存在しているのが問題。
このままバージョン管理するのはいかがなものかと思いリファレンスを確認しました
バージョン管理から除外する必要があるんですね(最初からそうしてほしい)
注:プロジェクトにパブリックリポジトリを使用している場合は、以下の手順でビジュアルスクリプトファイルを除外する必要があります。そうしないと、ビジュアルスクリプトをオンラインで違法に再配布し、Unity AssetStoreのEULAとToSに違反することになります。
なかなか熱い注意文!!!
いや、これは気づかない人いそうですが...
おとなしく除外する設定を入れようと思います。
.gitignoreに設定を追加します
VisualScriptingの設定からUnitを生成した時に吐き出されるファイルすべて除外対象
.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に追加して保存すると大量に出ていたファイルが全て消えます
と思ったらなんか残ってます。
フォルダの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 を継承しているスクリプトを見つけて登録してくれます)
(自分の環境 Unity2021.2.0b13 だと Regenerate Nodes と名前が違うんですよね.. おそらく次のバージョンで変わるのかな)
適当なグラフ内で検索ボックスを開くと一番下に自分が作成したユニットが配置されます
入力も出力も指定してないので配置してもただの箱になってます
次に
- 入力ポートと出力ポートを付ける
- 値の受け取りと送信をする
- ユニット内にロジックを入れる
- 検索の改装を変更する
を順を追ってやっていこうと思います
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"); } }
これでポートが付いたユニットが完成します
これだけではただつなぐことができる箱です
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); } }
このように、入力側と出力側の設定をしてみました
出力側はラムダで返す値を設定します。今回は固定値を返しています
ユニットの見た目は以下のようになりました
値の連結ができるようになっています
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 の戻り値には足し合わせた文字列を返します。
これで次のようにして実行します
結果、内部で変換した文字列がログに出るようになりました
関係性の追加(視覚的サポート)
Requirement
入力値に何も設定されていない時、または何もつながっていない時に警告を出してくれます
例えば、以下のような入力値を宣言した時
protected override void Definition() { ..... // これを追加 var test = ValueInput<string>("Test", null); }
グラフ上に変化はありません。
Testという入力値は string型で、デフォルト引数は null が指定されています。
nullの時、つまり何も値がない場合には警告を出すようにします
protected override void Definition() { ..... // これを追加 var test = ValueInput<string>("Test", null); Requirement(test, _inputTrigger); }
Requirement
で test を _inputTrigger と関係を付けることで、何も値がないときはユニットが黄色くなり Graph Inspector 上でも警告が出るようになりました
(個人的にはもう少し強めの警告色でも良かったかも。気づかずにスルーしそう)
Assignment
... これがよくわからず.. リファレンス見る限りアウトプット側に対して何らかの表示があるっぽいですが特に変化がわからない..
Succession
自分のユニットを作成して他のユニットと結びつけた時、結びつけ先のユニットがグレイアウトしていると思います
グレイアウトしているユニットの詳細を見てみると警告が出ています。
「前ユニットの出力(Output Trigger)からこのユニットに来ることがない」と判断されてグレイアウトされています。
そこで、『自分のユニットは入力があれば必ず出力します!』ということを教えてあげます。
Definition
メソッドに以下を追加
protected override void Definition() { ..... // これを追加 Succession(_inputTrigger, _outputTrigger); }
これで入力ポートになにか接続されていればoutput先がグレイアウトされなくなります
入力ポートが接続されている時 |
---|
入力ポートに何も接続されてないと自分含めグレイアウトします
入力ポートが接続されていない時 |
---|
ドキュメントの追加
グラフでユニットを選択した時に出る 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を見ると..
Unitだけ表示されても何を入力してどう出力するのかがわかりにくいため、コメントによる補佐は積極的に使っていきたいですね
長くなったので残りは次回
【Unity】VisualScripting Events unit について 2
はじめに
これの続き
Event Unit についてのまとめ
Animation Events
アニメーションに付けたイベントトリガーを検知します
試しに実装してみます
以下のようにCubeがスケールするアニメーションを作成して
半分の値のところにアニメーショントリガーを仕込みます
インスペクタから TriggerAnimationEvent
を選択します
今回は以下ようにパラメータを指定
これでグラフ上からアニメーションイベントを参照できるようになりました。
起動したら速攻アニメーションするように以下のようにEntryから直つなげしています
ユニット
アニメーションユニットは二種類あり Events > Animation
- AnimationEvent
- Named AnimationEvent
名前を入れる欄があるユニットは、同じ文字列を持つアニメーションイベントのみを受け入れます
まずは名前がいらない方でテストしてみます。
これで起動することでログが表示されます。
イベントが有るたびに起きるようにしているので、アニメーションイベントを2つ配置した場合
2回ログを出す処理が走ります
Named Animation Event
イベントの区別をするために名前指定の Named Animation Event を使用します。
2つ目のアニメーショントリガーのstringに指定した「イベント2」という文字列を入れて
実行すると
「イベント2」という文字列を入力したイベントトリガーのみにこのユニットが反応します
Named Animation Event は入力で文字列を受け取れるようになっているので条件によってフィルター名を変えることもできそうです
Event API
以下のような Custom Event (名前は TestEvent としてる)をグラフに配置して
スクリプト側からトリガーを叩いてみます
適当にスクリプトを作成して上記のグラフがついているGameObjectにアタッチします
using UnityEngine; using Unity.VisualScripting; public class CustomEventTriggerTest : MonoBehaviour { void Start() { // 第一引数はVisualScriptがあるGameObject CustomEvent.Trigger(gameObject, "TestEvent", "スクリプトから呼び出した"); } }
VisualScriptingのメソッド群は Unity.VisualScripting 内にあります。
実行すると呼び出されます
終わり
今回はアニメーションイベントとスクリプト側のトリガーの紹介をしました
VisualScriptingを見ていくにつれて、完全にVisualScriptingでゲームをつくるというよりスクリプトとのハイブリットで書く使い方が良いように思えます
【Unity】VisualScripting Events unit について 1
はじめに
- 画像をクリックしたときに反応する
- ある文字列の通知が投げられたら反応する
- 開始時に一度だけ呼び出される (Start)
- 毎フレーム呼び出される (Update)
などなど
VisualScriptingにはいくつものイベントが存在しており Events unit
と呼ばれるそれらの紹介をしていこうと思います
※ 以下の公式リファレンスを参照します
Events unit | Visual Scripting | 1.7.3
イベントユニットは緑色で表示される
Lifecycle
Events > Lifecycle
グラフにおいて最初に起動される。
ライフサイクルイベントはUnityのMonobehaviourにある Start, Update, OnEnable などを再現している
※ Monobehaviourの挙動と同じなため簡潔に説明します
OnStart
グラフが起動した瞬間に一度だけ呼び出されるユニット
OnUpdate
GameObejctがアクティブな状態の時、毎フレーム呼び出される
OnFixedUpdate
0.02秒ごとに一度呼び出されるUpdate(一秒に50回)
つまり特定の期間に一度呼び出されるUpdate
OnLateUpdate
すべてのUpdate, OnFixedUpdate処理の後に走るUpdate
OnEnable
オブジェクトがアクティブ化されるたびに呼び出される
OnDisable
オブジェクトが非アクティブ化されるたびに呼び出される
OnDestroy
オブジェクトが削除されたときに呼び出される
挙動を確認するには以下のように単純なログ出力を組むとわかりやすいと思います
Inputs & Outputs
- ボタンが押された時
- マウスがクリックされた時
- 通知された時
何らかのトリガーがあった時に開始するユニット群
キーボードで特定の入力があった時
Events > Input > OnKeybordInput
※ マウスの入力も同じ
UGUIのボタンが押された時
画面上に配置したボタンが押された時に反応するようにします
Events > GUI > Button Click
ボタンに Script Machine を取り付けている場合は Button Click ユニットそのままでいいですが
ボタンと違うオブジェクトのScriptMachineが押されたことを検知したい場合は、ボタンを参照するために SceneVariables に登録をします
※ Type は GameObjectでもButtonでも大丈夫です
これでScene全体からこのUGUIボタンが参照できるようになったので
以下画像のように Get Variables
で TestButtonを参照し OnButtonClick
ユニットと結びつけます
UGUI内部にはその他UI変化があった時にトリガーされるユニットが用意されているので見ておくと良さげです
Custom Events
個人的にイベントの中で最も重要だと思うのがCustomEvents
(よく使いそうなため)
あるグラフから別のグラフに対して
「プレイヤーがダメージを受けた」
「スキルを使用した」
などのユーザーがカスタムした通知を送りたい時に使用できる
ユニットの説明
Trigger Custom Event
→ イベントを送るユニット
Custom Event
→ イベントを受け取るユニット
Trigger Custom Event ユニットで特定の名前のイベントを通知して、Custom Event ユニットで名前を指定したイベントを受け取る。
今回はボタンにScriptMachineを付けて、
- ボタンが押された時に"TestButtonClick"というCustomEventを投げる。
- この時 "文字列" と "数値" をセットに送る
- 別のScriptMachineで "TestButtonClick" イベントを受け取って 文字列と数値 をログに出す
処理をしてみようと思います
まず、UGUIのボタンにScriptMachineを付けて中で OnButtonClick
と Trigger Custom Event
を結びつけます
今回は Button コンポーネントに ScriptMachine を付けたので Get Variable する必要はありません。
イベント名は "TestButtonClick" とします
文字列と数値を送るために、Trigger Custom Event
ユニットの Arguments を 2 に変更し
string と integer ユニットを配置して結びつけます (Arg0, Arg1)
これでボタンが押された時にイベントが投げられるようになりました
Trigger Custom Event
にはイベントを受け取る側のオブジェクトを付けることができます
今回は別のScriptMachineにイベントを送りたいため、対象のオブジェクトを Scene に登録して
Get Variable ユニットでつなげます
次にイベントを受け取る方を実装します
イベントを受け取るGameObjectにScriptMachineを付けて以下画像のように組み立てます
Custom Event で TestButtonClick 名のイベントを受け取るようにして
Arg0, 1 を String.Concat
ユニットで連結したものをログで出力します
結果
ボタン押した時にイベントの送受信が確認できます
【Unity】VisualScriptingとTextMeshProを使用してメッセージの文字送りを実現する
概要
こちらの続き
こちらの内容から。
メッセージのテキスト送りについての手順について
はじめに
TextMeshProには設定したテキストの「表示テキスト数」が指定できる
private TextMeshPro _text; private void Update() { // 最大可視化文字を指定。ここをアニメーションで増やしていくと表示文字が増えていく演出になる _text.maxVisibleCharacters = 1; }
この maxVisibleCharacters
を VisualScripting上から操作します
Timerユニットを取り付ける
「○秒に○文字出す」という条件にするために、Timerユニットが使用できます
これのStartを SetText ユニットに取り付けます
TimerUnit
機能名 | 説明 |
---|---|
Started | StartがOnになったら呼び出される |
Tick | タイマーがアクティブの間毎フレーム呼び出される |
Completed | 完了したら呼び出される |
Elapsed | タイマーが開始されてからの時間を返す |
Elapsed(%) | タイマーが開始されてからの時間を0〜1の範囲で返す |
Remaining | 残り時間を返す |
Remaining(%) | 残り時間を0〜1の範囲で返す |
Duration | カウント時間 |
UnScaled | チェックを入れるとタイムスケールの無視 |
テキストが設定された瞬間の表示文字は0
「10秒で40文字出す」という条件を追加します
Timerから先は以下
「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文字出す」が達成できます