UnityでNode Graph Processorを使って簡単にノードエディタを作る方法を紹介します。
Unity2019.4.0
Node Graph Processor 0.7.1
Node Graph Processor?
Node Graph ProcessorはUnityでノードエディタを簡単に作るためのOSSです。
UnityのGraph Viewを内部的に使っていて、グラフやノードをより簡単に作れるようになっています。
Graph Viewについては以下の記事にまとめていますので、必要に応じて参照してください。
またGraph Viewはあくまでノードエディタのビューを作るために提供されている機能ですが、
Node Graph Processorはグラフやノードの情報をシリアライズして永続化する機能なども備えています。
実際にノードエディタを作る際には必要だけどGraph Viewに無いような機能をうまく提供してくれるライブラリです。
基本的な使い方
まず基本的な使い方をまとめます。
Windowを作る
初めにグラフを表示するEditor Windowを作ります。
using System.IO; using UnityEngine; using UnityEditor; using GraphProcessor; using UnityEngine.Assertions; public class ExampleGraphWindow : BaseGraphWindow { protected override void InitializeWindow(BaseGraph graph) { Assert.IsNotNull(graph); // ウィンドウのタイトルを適当に設定 var fileName = Path.GetFileNameWithoutExtension(AssetDatabase.GetAssetPath(graph)); titleContent = new GUIContent(ObjectNames.NicifyVariableName(fileName)); // グラフを編集するためのビューであるGraphViewを設定 if (graphView == null) { graphView = new BaseGraphView(this); } rootView.Add(graphView); } }
BaseGraphView
かそれを継承するクラスのインスタンスをgraphView
に代入します。
この時点ではひとまずBaseGraphView
をそのまま使います。
グラフを作る
次にグラフを作成します。
これはBaseGraph
を継承したScriptableObjectとして作成します。
using GraphProcessor; using UnityEditor.Callbacks; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif // CreateメニューからScriptableObjectのアセットを作れるように [CreateAssetMenu(menuName = "Example Graph")] public class ExampleGraph : BaseGraph { #if UNITY_EDITOR // ダブルクリックでウィンドウが開かれるように [OnOpenAsset(0)] public static bool OnBaseGraphOpened(int instanceID, int line) { var asset = EditorUtility.InstanceIDToObject(instanceID) as ExampleGraph; if (asset == null) return false; var window = EditorWindow.GetWindow<ExampleGraphWindow>(); window.InitializeGraph(asset); return true; } #endif }
今回は処理は変えず、ただ継承してコメント部分の処理を追加しただけです。
ノードをいくつか作る
次にノードをいくつか作ります。
まずは二つの入力ポートの結果を足し合わせて出力するAdd
ノードを作ります。
using System; using GraphProcessor; [Serializable] [NodeMenuItem("Custom/Add")] // 作成時のメニュー名 public class AddNode : BaseNode { // 入力ポートを定義 [Input(name = "A")] public float input1; // 2つ目の入力ポートを定義 [Input(name = "B")] public float input2; // 出力ポートを定義 [Output(name = "Out", allowMultiple = false)] public float output; public override string name => "Add"; // 計算処理 protected override void Process() { output = input1 + input2; } }
ポートを作成するには上記のようにInput
やOutput
アトリビュートを付けたフィールドを定義します。
また実際の計算処理はProcess()
をオーバーライドして行います。
同じようにしてfloat値を定義するFloat
ノードを作ります。
using GraphProcessor; [System.Serializable, NodeMenuItem("Primitives/Float")] public class FloatNode : BaseNode { [Output("Out")] public float output; [Input("In")] public float input; public override string name => "Float"; protected override void Process() => output = input; }
同様に、計算結果を格納するResult
ノードを作ります。
using System; using GraphProcessor; [Serializable] [NodeMenuItem("Custom/Result")] public class ResultNode : BaseNode { [Input(name = "Result")] public float input; private float _result; public float Result => _result; public override string name => "Result"; protected override void Process() { _result = input; } }
動作確認
ここまでで一度動作確認してみます。
まずAssets > Create > Example Graph
からグラフのScritableObjectアセットを生成します。
そしてこのアセットをダブルクリックしてWindowを開きます。
Windowが開いたら右クリック > Create Nodeメニューから各ノードが生成できることを確認してください。
グラフを使って処理する
次にこのグラフを使って実際に計算を行っていきます。
カスタムGraphViewを作る
まず今のままではFloatノードに値を入力できません。
そこでノードのカスタムViewを実装することで値入力用のフィールドを表示させます。
using UnityEditor.UIElements; using UnityEngine.UIElements; using GraphProcessor; [NodeCustomEditor(typeof(FloatNode))] public class FloatNodeView : BaseNodeView { public override void Enable() { var floatNode = nodeTarget as FloatNode; DoubleField floatField = new DoubleField { value = floatNode.input }; floatNode.onProcessed += () => floatField.value = floatNode.input; floatField.RegisterValueChangedCallback((v) => { owner.RegisterCompleteObjectUndo("Updated floatNode input"); floatNode.input = (float)v.newValue; }); controlsContainer.Add(floatField); } }
これでFloat
ノードに入力フィールドが表示されました。
Resultノードを最初から表示&消せなくする
次に、Resultノードは必ず一つだけ存在するという仕様にするため、最初から表示して消せなくします。
まず最初から表示するためにExampleGraph
のOnEnable()
に処理を追加します。
using System.Linq; using GraphProcessor; using UnityEditor.Callbacks; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [CreateAssetMenu(menuName = "Example Graph")] public class ExampleGraph : BaseGraph { // 追加 protected override void OnEnable() { base.OnEnable(); // ResultNodeが無かったらつくる if (!nodes.Any(x => x is ResultNode)) { AddNode(BaseNode.CreateFromType<ResultNode>(Vector2.zero)); } } ...(略)... }
また消せなくするにはGraphViewを拡張する必要があります。
以下のようにExampleGraphView
を作成します。
using System; using System.Collections.Generic; using System.Linq; using GraphProcessor; using UnityEditor; public class ExampleGraphView : BaseGraphView { public ExampleGraphView(EditorWindow window) : base(window) { } public override IEnumerable<KeyValuePair<string, Type>> FilterCreateNodeMenuEntries() { foreach (var nodeMenuItem in NodeProvider.GetNodeMenuEntries()) { // ResultNodeを追加できないように if (nodeMenuItem.Value == typeof(ResultNode)) { continue; } yield return nodeMenuItem; } } protected override bool canDeleteSelection { // ResultNodeを消せないように get { return !selection.Any(e => e is ResultNodeView); } } }
またExampleGraphWindow
でこのExampleGraphView
を読み込むように変更します。
using System.IO; using UnityEngine; using UnityEditor; using GraphProcessor; using UnityEngine.Assertions; public class ExampleGraphWindow : BaseGraphWindow { protected override void InitializeWindow(BaseGraph graph) { Assert.IsNotNull(graph); var fileName = Path.GetFileNameWithoutExtension(AssetDatabase.GetAssetPath(graph)); titleContent = new GUIContent(ObjectNames.NicifyVariableName(fileName)); if (graphView == null) { // ExampleGraphViewに変更する graphView = new ExampleGraphView(this); } rootView.Add(graphView); } }
これでResult
ノードが常に表示されるようになりました。
追記:2021/1/26
この節の内容はv1.0.0だと仕様が変わりコンパイルエラーとなることをご報告いただきました。
v1.0.0以降をお使いの方は適したAPIに適宜書き換えをお願いします。
処理を実行する
さてそれではグラフの処理を実行します。
処理を実行するにはBaseGraphProcessor
を継承したクラスを作成します。
using System.Collections.Generic; using System.Linq; using Unity.Jobs; namespace GraphProcessor { public class ExampleGraphProcessor : BaseGraphProcessor { private List<BaseNode> _processList; public float Result { get; private set; } public ExampleGraphProcessor(BaseGraph graph) : base(graph) { } public override void UpdateComputeOrder() { _processList = graph.nodes.OrderBy(n => n.computeOrder).ToList(); } public override void Run() { var count = _processList.Count; // すべてのノードを順番に処理する for (var i = 0; i < count; i++) { _processList[i].OnProcess(); } JobHandle.ScheduleBatchedJobs(); // Resultノードを取得する var resultNode = _processList.OfType<ResultNode>().FirstOrDefault(); Result = resultNode.Result; } } }
Run()
の中ですべてのノードを処理し、その後に結果をResult
に格納しています。
後はこれを実行するだけです。
今回はExampleGraphのInspectorに実行ボタンを表示してみました。
using GraphProcessor; using UnityEngine; using UnityEditor; [CustomEditor(typeof(ExampleGraph))] public class ExampleGraphEditor : Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); if (GUILayout.Button("Process")) { var graph = target as ExampleGraph; var processor = new ExampleGraphProcessor(graph); processor.Run(); Debug.Log(processor.Result); } } }
適当なグラフを作り実行ボタンを押すと正常に結果がログ出力することが確認できます。
その他
最後にその他の機能を簡単に紹介して終わります。
色んなノード
まずノードについては今回紹介したほかにも様々なものを作ることができます。
一番右はわかりづいですが、チェックボックスにチェックを入れるとInputポートの型が変わります。
Conditional Processor
Conditional Processorを使うとノードをステップ実行することができます。
Exposed Property
Exposed Propertyを使うと変数を使えます。
定義した変数はInspectorから編集することも可能です。