Unityでノードエディタを簡単に作れるGraph Viewの基本的な使い方についてまとめました。
Unity2019.4.0
作るもの
UnityのShader GraphやVFX Graphを使うと、以下のようにノードベースのエディタによりアセットが編集できます。
Unityが用意しているGraphViewとその周辺クラスを使うとこのようなノードエディタのViewを簡単に作ることができます。
GraphViewは現時点でExperimentalという位置付けで公開されていますが、
既にShader Graphなどに使われていることもあり普通に使えるのではないかと思います。
本記事ではこの機能を使って以下のような簡単なグラフを作ってみます。
なおGraph ViewはUIElementsを使って作られています。
これに関しては以下の記事にまとめていますので必要に応じて参照してください。
ノードを作る
まず以下のようなノードを表すクラスを作ります。
ノードはNode
クラスを継承することにより作成します。
また、ノードには入出力ポートが存在するのでコンストラクタでそれらを作成します。
今回は入力ポートと出力ポートを一つずつ作成しました。
using UnityEditor.Experimental.GraphView; public class ExampleNode : Node { public ExampleNode() { title = "Example"; // 入力用のポートを作成 var inputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(float)); // 第三引数をPort.Capacity.Multipleにすると複数のポートへの接続が可能になる inputPort.portName = "Input"; inputContainer.Add(inputPort); // 入力用ポートはinputContainerに追加する // 出力用のポートを作る var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(float)); outputPort.portName = "Value"; outputContainer.Add(outputPort); // 出力用ポートはoutputContainerに追加する } }
グラフを作る
次にこのポートを表示するためのグラフを作ります。
グラフはGraphView
を継承したクラスを作ることで作成できます。
とりあえず確認用に前節で作ったノードを一つAddElement()
で子に追加しています。
またついでにサイズを親に合わせたり基本的なマウス操作ができるようにセットアップしています。
using UnityEditor; using UnityEngine.UIElements; using UnityEditor.Experimental.GraphView; public class ExampleGraphView : GraphView { public ExampleGraphView(EditorWindow editorWindow) { // ノードを追加 AddElement(new ExampleNode()); // 親のサイズに合わせてGraphViewのサイズを設定 this.StretchToParentSize(); // MMBスクロールでズームインアウトができるように SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale); // MMBドラッグで描画範囲を動かせるように this.AddManipulator(new ContentDragger()); // LMBドラッグで選択した要素を動かせるように this.AddManipulator(new SelectionDragger()); // LMBドラッグで範囲選択ができるように this.AddManipulator(new RectangleSelector()); } }
ウィンドウに表示する
さてそれではこれをウィンドウに表示してみます。
これは普通にEditorWindow
を作って前節のGraphViewをrootVisualElement
として追加するだけです。
using UnityEditor; public class ExampleGraphEditorWindow : EditorWindow { [MenuItem("Window/ExampleGraphEditorWindow")] public static void Open() { GetWindow<ExampleGraphEditorWindow>(ObjectNames.NicifyVariableName(nameof(ExampleGraphEditorWindow))); } void OnEnable() { var graphView = new ExampleGraphView(this); rootVisualElement.Add(graphView); } }
このウィンドウを開くと以下のようにノードが表示されます。
この時点でマウス操作も効くようになっています。
他のノードを作る
さてノードの表示が確認できたところで今回必要なノードをいくつか作っていきます。
まずは一番簡単そうなOutputノードです。
これはInputノードを一つ生成するだけです。
using UnityEditor.Experimental.GraphView; public class OutputNode : Node { public OutputNode() { title = "Output"; var port = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(float)); port.portName = "Value"; inputContainer.Add(port); } }
次にAddノードを作ります。
二つのInputノードと一つのOutputノードを定義します。
using UnityEditor.Experimental.GraphView; public class AddNode : Node { public AddNode() { title = "Add"; var inputPort1 = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(float)); inputPort1.portName = "A"; inputContainer.Add(inputPort1); var inputPort2 = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(float)); inputPort2.portName = "B"; inputContainer.Add(inputPort2); var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(float)); outputPort.portName = "Out"; outputContainer.Add(outputPort); } }
最後に値を設定できるフィールドを持つValueノードです。
ポートの設定方法は他のノードを変わりませんが、
値を設定するためのフィールドを追加するためにextensionContainer
にFloatField
を追加している点に注目してください。
またextensionContainer
にUI要素を追加した場合にはRefreshExpandedState()
を呼んで見た目に即時反映する必要があります。
using UnityEditor.Experimental.GraphView; using UnityEditor.UIElements; public class ValueNode : Node { public ValueNode() { title = "Value"; var port = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(float)); port.portName = "Value"; outputContainer.Add(port); extensionContainer.Add(new FloatField()); RefreshExpandedState(); } }
ノードを右クリックメニューから作れるようにする
さて次にこれらのノードをグラフの右クリックメニューから作れるようにします。
右クリックメニューを作るにはISearchWindowProvider
を実装したScriptableObjectを作成します。
using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEditor.Experimental.GraphView; using UnityEngine.UIElements; public class SearchMenuWindowProvider : ScriptableObject, ISearchWindowProvider { private ExampleGraphView _graphView; private EditorWindow _editorWindow; public void Initialize(ExampleGraphView graphView, EditorWindow editorWindow) { _graphView = graphView; _editorWindow = editorWindow; } List<SearchTreeEntry> ISearchWindowProvider.CreateSearchTree(SearchWindowContext context) { var entries = new List<SearchTreeEntry>(); entries.Add(new SearchTreeGroupEntry(new GUIContent("Create Node"))); // Exampleというグループを追加 entries.Add(new SearchTreeGroupEntry(new GUIContent("Example")) { level = 1 }); // Exampleグループの下に各ノードを作るためのメニューを追加 entries.Add(new SearchTreeEntry(new GUIContent(nameof(ValueNode))) { level = 2, userData = typeof(ValueNode) }); entries.Add(new SearchTreeEntry(new GUIContent(nameof(AddNode))) { level = 2, userData = typeof(AddNode) }); entries.Add(new SearchTreeEntry(new GUIContent(nameof(OutputNode))) { level = 2, userData = typeof(OutputNode) }); return entries; } bool ISearchWindowProvider.OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context) { var type = searchTreeEntry.userData as Type; var node = Activator.CreateInstance(type) as Node; // マウスの位置にノードを追加 var worldMousePosition = _editorWindow.rootVisualElement.ChangeCoordinatesTo(_editorWindow.rootVisualElement.parent, context.screenMousePosition - _editorWindow.position.position); var localMousePosition = _graphView.contentViewContainer.WorldToLocal(worldMousePosition); node.SetPosition(new Rect(localMousePosition, new Vector2(100, 100))); _graphView.AddElement(node); return true; } }
次に、右クリックをした時にこのメニューが開くようにGraphViewに処理を追加します。
using UnityEditor; using UnityEngine.UIElements; using UnityEditor.Experimental.GraphView; using UnityEngine; public class ExampleGraphView : GraphView { public ExampleGraphView(EditorWindow editorWindow) { AddElement(new ValueNode()); this.StretchToParentSize(); SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale); this.AddManipulator(new ContentDragger()); this.AddManipulator(new SelectionDragger()); this.AddManipulator(new RectangleSelector()); // 右クリックメニューを追加 var menuWindowProvider = ScriptableObject.CreateInstance<SearchMenuWindowProvider>(); menuWindowProvider.Initialize(this, editorWindow); nodeCreationRequest += context => { SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), menuWindowProvider); }; } }
これでウィンドウの右クリックメニューからノードを作れるようになりました。
ノード同士を繋げられるようにする
さてこのままではノードを繋げられないので繋げられるようにします。
これはGraphView.GetCompatiblePorts()
をオーバーライドすることで行います。
対象のポートに接続可能なポートのリストを返却します。
using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine.UIElements; using UnityEditor.Experimental.GraphView; using UnityEngine; public class ExampleGraphView : GraphView { public ExampleGraphView(EditorWindow editorWindow) { AddElement(new ValueNode()); this.StretchToParentSize(); SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale); this.AddManipulator(new ContentDragger()); this.AddManipulator(new SelectionDragger()); this.AddManipulator(new RectangleSelector()); var menuWindowProvider = ScriptableObject.CreateInstance<SearchMenuWindowProvider>(); menuWindowProvider.Initialize(this, editorWindow); nodeCreationRequest += context => { SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), menuWindowProvider); }; } // GetCompatiblePortsをオーバーライドする public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) { var compatiblePorts = new List<Port>(); compatiblePorts.AddRange(ports.ToList().Where(port => { // 同じノードには繋げない if (startPort.node == port.node) return false; // Input同士、Output同士は繋げない if (port.direction == startPort.direction) return false; // ポートの型が一致していない場合は繋げない if (port.portType != startPort.portType) return false; return true; })); return compatiblePorts; } }
これでポート同士を接続できるようになりました。