0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Experimentalでも使いたい! Unity GraphViewを実践で導入するための解説記事

Posted at

この記事は?

Unity エディタ拡張完全に理解した勉強会でのLTのフォローアップ記事です。

はじめに

〜UnityのGraphViewを取り巻く現状〜

Unity内部ではShaderGraphやVisualEffectGraphなど、有名なパッケージがこのAPIを使って実装されています。

UnityのGraphViewですがリリースされてから当初までExperimentalなAPIであり、ユーザーに正式に公開されたものではありません😭

またUnityは、GraphToolkitという、GraphViewとは完全に異なるGraph描画の機能を検討しており、将来的にはこちらのAPIを正式なものとする想定だそうです😭😭😭

公式からは数年具体的なリリース告知がなかったのですが、この記事を執筆している途中の3/20についに公式からリリース声明が出ました😇😇😇
もう直ぐリリース予定だそうです。Unity6系以降で使えるそうなので、利用できる環境の方はこちらを使ってください!💪
Unity Graph Toolkit Update (Q1 2025)

GraphToolkitについては数年間動きがなかったので、2025年3月の時点では、Unityのエディタ拡張としてのGraph表示機能はGraphViewが優良な選択肢として挙がるのが現状でした。

ここ数年間、上記のような状況だったため、筆者もGraphViewのキャッチアップは見送っていた次第でした。

しかしゲーム開発の現場においては、グラフ構造を取るものが非常に多く、筆者の現場でもGraph表示ツールを制作するニーズが出てきたので、重い腰を上げてGraphViewを学んでみた次第でした。

この記事で扱う範囲と対象読者

業務や趣味の個人開発などで、Graph描画ツールをガッツリ作り込みたい人を対象読者としています。

ExperimentalなAPIなのでUnity公式の情報も少ないです。この記事や補足のサンプルリポジトリを読んでもらった読者の方が、現場でGraphViewを導入できるレベルに持っていけるといいなという思いで執筆しました💪

この記事は手を動かして学ぶ基本編と、実際に使ってみる上で知っておきたいTipsをまとめた実践編に分かれています。理解度や知りたいポイントに応じ、かいつまんで読んでいただければなと思います。

GraphViewの基本を知りたい場合は基本編を読んでください。実践で活用する上のポイントや細かい機能は、実践編を読んでください。

サンプルリポジトリ

この記事と連動するサンプルリポジトリを以下に用意しました。必要に応じて参考にしてみてください!

サンプルの使い方や内容は、リポジトリのREADMEをご参照ください。

GraphViewとUIToolkitについて

UnityのGraphViewは、UIToolkitで作られています。

GraphViewの実装時にUIToolkitのAPIを使う箇所箇所もあるため、UIToolkitについてある程度の理解があるとキャッチアップがスムーズです。

特にUIToolkitの以下のようなトピックについて抑えておけると良いかなと思います。

  • VisualElement、BaseFieldなどのAPI仕様
  • Unityスタイルシート(USS)
  • イベントハンドリングの仕様
  • UIToolkitDebuggerの使い方

筆者はUIToolkitも未把握だったのですが、併せて勉強することでGraphViewの理解も捗りました。
最近のUnityの機能や外部パッケージでは、UIToolkitを使うものも増えてきた印象です。そのため、UIToolkitについても学んで損はないと思います!

〜基本編〜 GraphViewのAPIを触って、基本的なGraphViewを実装してみる

画面で見るGraphViewの構成要素

導入として、よく使うGraphViewの構成要素を図で示します。
GraphViewの実装時には色々なAPIが登場するので、まず主要な構成要素のイメージを掴んでおけると理解がスムーズです💪

graph_view_overview.png

構成要素 役割 対応APIなど
Window全体 ウィンドウ全体。 UnityEditor.Experimental.GraphView.GraphViewEditorWindow
GraphView グラフビューそのもの。 UnityEditor.Experimental.GraphView
Node 個々のノード。 UnityEditor.Experimental.GraphView.Node
Edge 個々のエッジ。ノード間を接続する。 UnityEditor.Experimental.GraphView.Edge
Port ノードの入力および出力ポート。 UnityEditor.Experimental.GraphView.Port
ContextMenu 右クリックで表示されるメニュー UnityEditor.Experimental.GraphView.BuildContextualMenu

基本編でやってみる実装

mini_sample_basic_graph_view_01.gif

基本編では、GraphViewの基本的なAPIを触ってみて、上記のような辺を接続できるノードの作成までを目指します。

GraphViewの基本的な機能については、沢山の有志の方がわかりやすい記事を書いてくれています。参考欄に記事を上げておきますので、そちらでキャッチアップ頂いても良いと思います。

GraphViewの表示〜StyleSheetの読み込みまで

mini_sample_basic_graph_view_02.png

まずは上図のような、何もないグラフビューの表示するところまでから始めましょう。
以下のようなWindowとGraphViewクラスを実装してください。

using UnityEngine;
using UnityEditor;

namespace MiniSample.BasicGraphView.Editor
{
    public class BasicGraphWindow : EditorWindow
    {
        private BasicGraphView _sampleGraphView;

        // Menuのパスはお好みで設定してください。
        [MenuItem("Window/Mini GraphView Samples/Basic", false, 0)]
        public static void OpenWindow()
        {
            var window = GetWindow<BasicGraphWindow>();
            window.titleContent = new GUIContent("Basic Sample");
            window.Show();
        }

        private void CreateGUI()
        {
            // 子にGraphViewを追加
            _sampleGraphView = new BasicGraphView();
            rootVisualElement.Add(_sampleGraphView);
        }
    }
}
using UnityEditor.Experimental.GraphView;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEngine.Assertions;

namespace MiniSample.BasicGraphView.Editor
{
    public class BasicGraphView : GraphView
    {
        // StyleSheetのパスは、お好みで設定してください。
        private const string StyleSheetPath = "Assets/GraphViewSamples/StyleSheets/MiniSample/background_style_sheet.uss";
        
        public BasicGraphView()
        {
            // ズーム倍率を設定する(デフォルト値の場合、0.25~1.0倍)
            SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
            this.StretchToParentSize();

            // 各種マニピュレーターを設定
            // 要素を選択、移動可能にする
            this.AddManipulator(new SelectionDragger());
            // 要素を矩形選択可能にする
            this.AddManipulator(new RectangleSelector());
            // 描画範囲自体を移動可能にする
            this.AddManipulator(new ContentDragger());

            // 背景を設定
            var backGround = new GridBackground();
            Insert(index:0, backGround);

            // StyleSheetを適用する
            var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(StyleSheetPath);
            Assert.IsNotNull(styleSheet);
            styleSheets.Add(styleSheet);
        }
    }
}

GraphViewの実装にあたっては、見慣れないAPIが多く登場します。
決まり文句的なものも多いので、はじめのうちは定型文としておさえましょう(笑)

上記のサンプルでは、SetupZoomでズーム倍率を設定しています。

また、UIToolkitにManipulatorという機能があり、マウス・ジェスチャー操作を簡単に記述することができます。
GraphViewの場合は、SelectionDragger・RectangleSelecter・ContentDraggerは指定することになるかなと思います。
それぞれのManipulatorの役割はサンプルコードの通りです。

上記のコードが用意できたら、コード上で指定したパスに、以下のようなbackground_style_sheet.ussというファイルを用意してください。

/* 次の記事を参考にさせていただきました。 https://hirukotime.hatenablog.jp/entry/2022/11/11/201335 */
/* GridBackgroundというclassセレクタに対し、以下のようなスタイルを適用 */
GridBackground {
    --grid-background-color:rgb(90,90,90);
    --line-color:rgba(80,80,80,255);
    --thick-line-color:rgba(40,40,40,153);
    --spacing:10;
}

上記はUIToolkitのUnityStyleSheet(USS)というもので、CSSとほぼ同じような仕様でスタイルの指定ができます。
グラフビューの子にGridBackgroundを指定すると、上記のように背景グリッドの色やサイズの設定ができるようになります。

また、このサンプルではGridBackgroundに対してスタイルを当てていますが、グラフビューの他要素(例えばノードやエッジ)についても、マッチするセレクタとスタイル指定を当てれば、表示をカスタマイズすることが可能です!

ここまで実装してもらえると、BasicGraphWindowクラスで指定したメニューパスで、グラフビューが開けるかと思います。

コンテキストメニューのカスタマイズとNodeの生成

次に、コンテキストメニューをカスタマイズし、右クリックでNode追加できる機能を追加します。
BasicGraphViewのコードを、以下のようにカスタマイズしてください。

//...

namespace MiniSample.BasicGraphView.Editor
{
    public class BasicGraphView : GraphView
    {
        //...
        
        public BasicGraphView()
        {
            // ...

            // コンテキストメニュー呼び出し時の挙動をカスタマイズする
            this.AddManipulator(new ContextualMenuManipulator(BuildContextualMenu));

            // ...
        }

        public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
        {
            // ノード作成メニューを作り、押された時にNodeを生成する
            var pos = viewTransform.matrix.inverse.MultiplyPoint(evt.localMousePosition);
            evt.menu.AppendAction("Create Node", action => CreateNode("Node", pos));

            // デフォルトのメニューを表示する
            base.BuildContextualMenu(evt);
        }

        private Node CreateNode(string title, Vector2 position)
        {
            var node = new Node { title = title };
            node.SetPosition(new Rect(position, new Vector2(150, 100)));
            AddElement(node);
            return node;
        }
    }
}

右クリックをした時のコンテキストメニューも、ContextMenuManipulatorという専用Manipulatorクラスでカスタマイズすることができます。

デフォルトのコンテキストメニューの表示処理は、BuildContextualMenuクラスで実装されているので、これをオーバーライドして、ノード生成メニューを追加します。

GraphView.BuildContextualMenuの実装を見てもらえれば、ノードのコピー&ペースト、削除、複製メニューが押された際に、どのようなコールバックが実行されるのかが分かります💡

Nodeの作成と辺を繋げる

次に、Nodeに入出力ポートを追加し、Node間を繋げられるようにします。

まずは、入出力ポートを持った専用のカスタムノードクラスを用意します。以下のようなクラスを追加してください。

using UnityEditor.Experimental.GraphView;

namespace MiniSample.BasicGraphView.Editor
{
    public class CustomNode : Node
    {
        private Port _inputPort;
        private Port _outputPort;

        public CustomNode(string title)
        {
            base.title = title;

            _inputPort = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(string));
            _inputPort.portName = "input";
            inputContainer.Add(_inputPort);

            _outputPort = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(string));
            _outputPort.portName = "output";
            outputContainer.Add(_outputPort);
        }
    }
}

次に、メニュー実行時にカスタムしたNodeクラスを生成するようにします。
BasicGraphViewのファクトリメソッド部分を1行書き換えてください。

//...

namespace MiniSample.BasicGraphView.Editor
{
    public class BasicGraphView : GraphView
    {
        //...

        private Node CreateNode(string title, Vector2 position)
        {
            // 以下一行を変更し、自前のカスタムノードを生成するようにしてください
            var node = new CustomNode(title);
            node.SetPosition(new Rect(position, new Vector2(150, 100)));
            AddElement(node);
            return node;
        }
    }
}

また、NodeのPort間の接続方法は、GraphView上にその振る舞いを定義する仕様となっています。
BasicGraphViewクラスを編集し、GetCompatibleメソッドをオーバーライドするようにしてください。

//...

namespace MiniSample.BasicGraphView.Editor
{
    public class BasicGraphView : GraphView
    {
        //...

        // Port間が接続可能かどうかは、GraphViewのGetCompatiblePortsをオーバーライドして判定する
        public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
        {
            // 接続開始Portが引数で渡ってくる。
            // GraphView上のPort群のうち、接続可能なPort群を返すように実装すればOK。
            var compatiblePorts = new List<Port>(ports.Count());
            foreach (var port in ports)
            {
                // 自Nodeは接続不可、同じDirectionは接続不可、Portの型が一致している必要がある
                if (startPort.node == port.node || startPort.direction == port.direction || startPort.portType != port.portType)
                {
                    continue;
                }
                compatiblePorts.Add(port);
            }
            return compatiblePorts;
        }

        //...
    }
}

ここまでで、基本編のサンプルは終了となります!
お疲れ様でした。

以降は、実践で使うTips集を解説します。
皆様の現場で使いそうなケースに応じて、かいつまんで読んでみてください🙏

~実践編~ 自前で実装しなければいけない機能と実践上のポイント

自前で実装する必要がある機能

GraphViewにおいては、以下のような機能は提供されておらず、自前での実装が必要です。

Save機能

dag_sample_02.gif

  • GraphViewではグラフのセーブ機能は提供されていません。
  • あくまでグラフ構造を表示するための機能であり、どういうデータ構造からSerialize/Deserializeされるかは自由だからです。
  • そのため、基本的にはGraphViewを使いたいデータ構造側の都合に合わせて、GraphViewとの同期をとる処理を実装する必要があります。
  • また、GraphView上のNodeは、位置やサイズ(Rect)の表示情報も持っています。これらの表示情報をデータ構造側でうまく表現してやる必要があります。
    • ex) データ構造側で表示用のRectフィールドを保持しつつ、インスペクタ拡張などでRectフィールドは非表示にして存在を隠蔽する。

Undo/Redo機能

dag_sample_03.gif

  • Undo/Redo機能も特に用意されていません。
  • UnityEditor.Undoを使ったUndo/Redo対応を行う必要があります。
  • GraphViewの構成要素はUnityEngine.Objectではありません。そのため、Undo/RedoはGraphViewに対して直接行えず、GraphViewと対になるデータ構造側でUndo/Redo対応し、データ構造がUndo/Redoされたタイミングに紐付きGraphView側の表示も更新してやる必要があります。

実装上のポイント: GraphView(View)とデータ構造(Model)のデータ同期

上述したSaveやUndo/Redoを踏まえると、GraphViewでツールを作る際は、まずGraphView(View)側とデータ構造(Model)側でどのように同期を取るか決める必要があるかと思います。

以下のような選択肢があり得るかと思います。

(1)同期タイミングを限定し、簡易的な実装で済ますパターン

  • 例えば以下のように、同期タイミングを限定するパターンです。
  • Model -> View への同期
    • Model(例えばScriptableObject)のインスペクタなどからWindowを開いた際に同期
    • Modelのインスペクタに反映用のボタンを設ける もしくは インスペクタ拡張で編集を制限する
  • View -> Model への同期
    • Windowが閉じた際(OnDisable)、Windowの表示物を切り替える(別ModelをGraph表示する)タイミングで同期
    • GraphView上にSaveのようなボタンを設け、明示的に反映させる

(2)同期タイミングを限定し、簡易的な実装で済ますパターン

  • こちらのパターンは考慮する事項が増えます。
  • 漏れなく実装しきる必要があります。上述のWindow開閉タイミングなどの他、例えば以下タイミングなどで同期を取る必要があります。
  • Undo/Redo対応したい場合は、こちらの実装パターンを採ることになると思います。
  • GraphView上での編集時
    • Node, Edgeの生成時
    • Node, Edgeの削除時
    • Nodeのパラメーター更新時(座標移動も含む)
  • Model側の編集時
    • インスペクタなどで値を書き換えたら際
    • Undo/Redoの実行時

やりたい要件や工数に応じて、どこまでサポートするのかを決めるのが良いのかなと思います。
サンプルリポジトリでは、(2)のケースで実装してみていますので、参考にしてみてください。

実装上のポイント: GraphViewの責務増えすぎないように分割する

  • 基本編のコード例などで示した通り、GraphViewクラスで各種コールバックや表示物の登録、Portの接続方法の登録などのあらゆる設定をすることから、GraphViewの実装が肥大化しがちです。
  • そのため、ロジックは何らかの粒度で分割できると良いでしょう。
    • ex) MVPパターンを使う
    • ex) Nodeのファクトリや更新処理を専用のクラスに逃す
    • ex) Serialize / Deserializeを専用の処理に逃す

~実践編~ GraphViewの各機能について

次に、GraphViewやUIToolkitが持つ、さまざまな機能について説明していきます🔥

Node/Edgeの生成・更新・削除処理について

いずれもGraphViewに該当タイミングのコールバックがあるので、これに処理を登録します。
以下にサンプルコードを記載します。

using System.Collections.Generic;
using System.Linq;
using UnityEditor.Experimental.GraphView;
using UnityEngine.UIElements;
using UnityEngine;

public class MyGraphView : GraphView
{
    public MyGraphView()
    {
        // (GraphViewの各種初期化処理)

        nodeCreationRequest += OnNodeCreationRequest;
        
        graphViewChanged += OnGraphViewChanged;
    }
    
    // Nodeの生成時に、このコールバックが呼ばれる
    private void OnNodeCreationRequest(NodeCreationContext context)
    {
        UnityEngine.Debug.Log($"index:{context.index} mousePos:({context.screenMousePosition.x},{context.screenMousePosition.y})");
        // ノードの生成処理をここに書く
    }
    
    // グラフ上のNodeやEdgeの色々な変更は、このコールバックが呼ばれる
    private GraphViewChange OnGraphViewChanged(GraphViewChange change)
    {
        if (change.edgesToCreate != null)
        {
            foreach (var edge in change.edgesToCreate)
            {
                // エッジの生成処理をここに書く
            }
        }
        
        if (change.movedElements != null)
        {
            foreach (var element in change.movedElements)
            {
                if (element is Node node)
                {
                    // ノードの座標移動処理をここに書く
                }
            }
        }

        if (change.elementsToRemove != null)
        {
            foreach (var element in change.elementsToRemove)
            {
                if (element is Node node)
                {
                    // ノードの削除処理をここに書く
                }
                else if (element is Edge edge)
                {
                    // エッジの削除処理をここに書く
                }
            }
        }

        return change;
    }
}

ノードの生成については、nodeCreationRequestに処理を登録します。
引数にNodeCreationContextという構造体があり、ここから生成時のindexやマウスポインタ座標、Nodeの親となるVisualElementなどが取得できます。

例えばShaderGraphなど、種類が異なるNodeを作りたいケースもあるかと思います。
そのような場合は、基本編で扱ったようにContextualMenuManipulatorをカスタマイズしても良いでしょうし、後述するISearchProviderを実装する方法なども考えられるかなと思います。

public struct NodeCreationContext
{
    /// <summary>
    ///   <para>Position of the click that initiated the request to create a node, in the coordinate space of the screen.</para>
    /// </summary>
    public Vector2 screenMousePosition;
    /// <summary>
    ///   <para>The VisualElement where the created node will be added.</para>
    /// </summary>
    public VisualElement target;
    /// <summary>
    ///   <para>The index where the created node will be inserted.</para>
    /// </summary>
    public int index;
}

Edgeの生成・Nodeの移動・NodeやEdgeなどの移動は、まとめてgraphViewChangedというコールバックで処理します。

以下のようなGraphViewChangeという構造体が引数で渡るので、それで各ケースを判定する形になります。

public struct GraphViewChange
{
    /// <summary>
    ///   <para>Elements about to be removed.</para>
    /// </summary>
    public List<GraphElement> elementsToRemove;
    /// <summary>
    ///   <para>Edges about to be created.</para>
    /// </summary>
    public List<Edge> edgesToCreate;
    /// <summary>
    ///   <para>Elements already moved.</para>
    /// </summary>
    public List<GraphElement> movedElements;
    /// <summary>
    ///   <para>The delta of the last move.</para>
    /// </summary>
    public Vector2 moveDelta;
}

Nodeで使える様々な型のFieldについて

mini_sample_node_fields_01.png

Nodeには、さまざまな型のフィールドを追加できます。
また、各フィールドの変更は、RegisterValueChangedCallbackで受け取ることができます。
コールバックの発火時に受け取るChangeEventから、変更前後の値を取得することができます。

// UnityEngine.Objectを参照するフィールドを追加
var objectField = new ObjectField();
// ObjectFieldの場合は、受け付けるアセットの具象型を指定する
objectField.objectType = typeof(GameObject);
// RegisterValueChangedCallbackで、変更前後の値を取得できる
objectField.RegisterValueChangedCallback(changeEvent =>
{
    var prevObjectReference = changeEvent.previousValue;
    var newObjectReference = changeEvent.newValue;
});
return objectField;

各Fieldは、GraphViewではなくUIToolkit側のAPIになります。
各Fieldの仕様については、スクリプトリファレンスの、UnityEditor.UIElements > Classes以下を参照すると良いと思います。

Toolbar

dag_sample_04.png

GraphView上部のToolbarは、Toolbarクラスにて実現できます。
こちらも、UIToolkit上のAPIになります。

以下の要領で、とても簡単に追加できます(笑)
GraphViewではなくWindow側のrootVisualElementに追加します。

using UnityEngine;
using UnityEditor;
using UnityEditor.UIElements;

public class MyGraphWindow : EditorWindow
{
    //...

    private void CreateGUI()
    {
        var graphView = new MyGraphView();
        rootVisualElement.Add(graphView);

        // 以下ボタン2つのツールバーの表示処理
        var toolbar = new Toolbar();
        var buttonA = new ToolbarButton(() =>
        {
            // ボタンがクリックされた時の処理をここに書く
        })
        {
            text = "ButtonA"
        };
        // Toolbar上の他要素と余白を持たせたい場合はstyle指定する
        buttonA.style.marginLeft = 8;
        toolbar.Add(buttonA);
        var buttonB = new ToolbarButton()
        {
            text = "ButtonB"
        };
        toolbar.Add(buttonB);
        rootVisualElement.Add(toolbar);
    }
}

コピー&ペースト機能について

コピー&ペーストについては、GraphView側に専用のコールバックがあるのでそちらを利用します。
以下三種類のコールバックが呼ばれた時の処理を、それぞれ実装すればOKです!

コールバック名 役割 実装内容
serializeGraphElements コピー時に呼ばれる。
引数にコピー対象となるGraphElementが渡る。
コピー対象となる情報を、stringにSerializeして渡す。
canPasteSerializedData ペースト時に最初に呼ばれる。
ペースト可否をここで判定する。
引数にSerializeされたstringが渡るのでそれを元に判断し、ペースト可能ならtrueを返す。
unserializeAndPaste ペースト時に上記コールバックがtrueを返すときに呼ばれる。 実際のペースト処理を実装する。
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;

public class MyGraphView : GraphView
{
    public MyGraphView()
    {
        //...

        // NodeのCopy&Pasteについては以下のコールバック群を使う
        // 要素のCopy時に呼ばれ、Paste処理に必要な情報をstringで返す
        serializeGraphElements += OnSerializeGraphElements;
        // 要素のPaste時に最初に呼ばれる。Paste可能かどうかを返す
        canPasteSerializedData += OnCanPasteSerializedData;
        // 上記コールバックがtrueを返す場合に呼ばれる。実際のPaste処理を行う
        unserializeAndPaste += OnUnserializeAndPaste;
    }

    private string OnSerializeGraphElements(IEnumerable<GraphElement> elements)
    {
        UnityEngine.Debug.Log($"{nameof(OnSerializeGraphElements)} time:{UnityEngine.Time.time}");
        // GraphElement群を元に、ペースト用の文字列を生成する処理を実装する
    }
    
    private bool OnCanPasteSerializedData(string data)
    {
        UnityEngine.Debug.Log($"{nameof(OnCanPasteSerializedData)} time:{UnityEngine.Time.time}");
        // 引数でOnSerializeGraphElementsで加工した文字列が渡る。
        // それを元に、ペースト可能かを判断。
    }
    
    private void OnUnserializeAndPaste(string operationName, string data)
    {
        UnityEngine.Debug.Log($"{nameof(OnUnserializeAndPaste)} time:{UnityEngine.Time.time}");
        // 実際のペースト処理をここで実装。
    }
}

Node生成メニューなど、カスタムSearchWindowの実装

mini_sample_search_provider_01.gif

ShaderGraphのように、異なる型のNodeを生成したい場合などは、階層型のメニューがあると便利です。

この場合は、GraphViewのSearchWindowという機能を使います。

SearchWindow自体はGraphView側の機能として提供されており、これに表示するメニューをISearchWindowProviderというインタフェースを実装して提供します。

using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

// ScriptableObjectの継承を忘れずに
public class SearchProvider : ScriptableObject, ISearchWindowProvider
{
    private readonly List<SearchTreeEntry> _entries = new List<SearchTreeEntry>(16);
    
    public void Initialize()
    {
        _entries.Add(new SearchTreeGroupEntry(new GUIContent("Create Custom Node")));
        // 包含するメニューの場合は、SearchTreeGroupEntryを追加する
        _entries.Add(new SearchTreeGroupEntry(new GUIContent("Primitive Nodes"))
        {
            // levelで階層を指定
            level = 1,
        });
        // 実行されるメニューの場合は、SearchTreeEntryを追加する
        _entries.Add(new SearchTreeEntry(new GUIContent("Int Node"))
        {
            level = 2,
            // userDataを設定することができ、OnSelectEntry時に取得できる
            userData = typeof(IntNode),
        });
        _entries.Add(new SearchTreeEntry(new GUIContent("Float Node"))
        {
            level = 2,
            userData = typeof(FloatNode),
        });
        _entries.Add(new SearchTreeEntry(new GUIContent("String Node"))
        {
            level = 2,
            userData = typeof(StringNode),
        });
        _entries.Add(new SearchTreeGroupEntry(new GUIContent("Unity Nodes"))
        {
            level = 1,
        });
        _entries.Add(new SearchTreeEntry(new GUIContent("Texture Node"))
        {
            level = 2,
            userData = typeof(TextureNode),
        });
        _entries.Add(new SearchTreeEntry(new GUIContent("Material Node"))
        {
            level = 2,
            userData = typeof(MaterialNode),
        });
    }

    // SearchWindow上で表示するメニューのエントリ一覧を返す。
    // SearchWindowが開かれる度に呼ばれる。
    public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
    {
        UnityEngine.Debug.Log($"CreateSearchTree time:{UnityEngine.Time.time}");
        return _entries;
    }

    // ここのメニューが選択された際に、このメソッドが呼ばれる。
    public bool OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
    {
        if (searchTreeEntry.userData == null)
        {
            return true;
        }

        var userDataName = searchTreeEntry.userData.ToString();
        UnityEngine.Debug.Log($"OnSelectEntry userData:{userDataName} time:{UnityEngine.Time.time}");
        return true;
    }
}

ISearchProviderには、CreateSearchTreeメソッドとOnSelectEntryメソッドを実装します。
各メソッドの意味は上記サンプルを見てください。

メニュー上のEntryはサンプルコードのように、levelで階層指定してネストさせます。登録順に表示される仕様です。
また、各Entryには任意型のuserDataを持たせることができます。
ここで各メニュー実行時に必要な情報を渡します。ShaderGraphのような階層型メニューを作る場合は型などで判断することになるでしょう。

上記ISearchProviderの実装ができたら、あとはSearchWindowを開く際にこのプロバイダクラスのインスタンスを渡してやればOKです。

以下の例は、右クリックからのCreateメニュー実行時に、SearchWindowを表示する例となっています。

using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine.UIElements;
using UnityEngine;
using UnityEngine.Assertions;

public class MyGraphView : GraphView
{
    //...
    
    private SearchProvider _searchProvider;

    public MyGraphView()
    {
        //...

        _searchProvider =  ScriptableObject.CreateInstance<SearchProvider>();
        _searchProvider.Initialize();
        
        nodeCreationRequest += OnNodeCreationRequest;
    }
    
    private void OnNodeCreationRequest(NodeCreationContext context)
    {
        SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), _searchProvider);
    }
}

BlackBoard機能

GraphViewにはBlackBoard機能も提供されています。
BlackBoardとは、Graph間で共有する変数群などを管理するための機能になります。
ブラックボードアーキテクチャパターン

BlackBoardインスタンスを生成し、GraphViewの要素として追加すれば簡単に表示ができます。
また、BlackBoard上の変数は、BlackBoardFieldというクラスで表現されます。

以下のサンプルコードなどを参考にしてみてください。

using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine.UIElements;
using UnityEngine;
using UnityEngine.Assertions;

public class MyGraphView : GraphView
{
    //...

    public MyGraphView()
    {
        //...

        blackboard = new Blackboard(this)
        {
            title = "Node Create",
            scrollable = true
        };
        blackboard.SetPosition(new Rect(20, 20, 200, 150));
        Add(blackboard);
        // + ボタンが押された時のメニューを登録する
        blackboard.addItemRequested = (_) => ShowAddMenu();
    }

    private void ShowAddMenu()
    {
        var menu = new GenericMenu();
        menu.AddItem(new GUIContent("Add Int"), false, () => AddBlackboardVariable("Int Node", nameof(Int32)));
        menu.AddItem(new GUIContent("Add Float"), false, () => AddBlackboardVariable("Float Node", nameof(Single)));
        menu.AddItem(new GUIContent("Add String"), false, () => AddBlackboardVariable("String Node", nameof(String)));
        menu.ShowAsContext();
    }

    private void AddBlackboardVariable(string name, string typeName)
    {
        var field = new BlackboardField { text = name, typeText = typeName };
        blackboard.Add(field);
    }
}

Group機能

mini_sample_group_and_minimap_01.gif

複数のNodeをグルーピングする、Groupという機能も提供されています。
Groupは、NodeやEdgeと同じくGraphElementを継承するクラスになります。
Nodeと同じような要領で、GraphViewにAddすれば表示されます。

var group = new Group
{
    title = title,
    autoUpdateGeometry = true // 自動でサイズ調整
};
group.SetPosition(new Rect(mousePosition, new Vector2(100, 100)));
AddElement(group);

実装を確認したところ、再帰的なGroupの追加はサポートされていないようです。
また、GroupからNodeを取り除くには、Shift + クリックでNodeを選択します。
スクリプト上で除外するには以下のコード例のようにします。


//...

private void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
    //...

    var selectedNodes = selection.OfType<Node>().ToList();
    if (selectedNodes.Count > 0)
    {
        evt.menu.AppendAction("Remove selected Nodes from Group", action =>
        {
            foreach (var node in selectedNodes)
            {
                // ScopeはGroupの基底クラス。Node側に所属するScopeが登録されているので、それを利用して登録解除ができる。
                if (node.GetContainingScope() is Scope scope)
                {
                    scope.RemoveElement(node);
                }
            }
        }, DropdownMenuAction.AlwaysEnabled);
    }
}

//...

まとめ

以上になります。
この記事が、皆様の現場のヒントになれば幸いです💪
あと、GraphToolkitのリリースが発表されたので、触れるようになったらGraphToolkitについても記事出せればと思います🔥

参考

【Unity】GraphViewで遊んでみた。【第一回】
【Unity】GraphViewで遊んでみた。【第二回】
【Unity】GraphViewで遊んでみた。【第三回】

  • 初回のキャッチアップはこちらの連載記事を参照させていただきました。

今PCが手元にないなら絶対に見ないで下さい。記事が優良すぎて、ほぼ100%その場でGraphViewしてしまいます

  • こちらも入門記事として良さそうです。以下のようなケースの参考にさせて頂きました。
    • Nodeのフィールドで日本語入力したいケース
    • Nodeに応じたファクトリクラスを毎度生成せず、リフレクションで生成する例

【Unity】GraphViewの構造解説

  • NodeのContainerの構造についてはこちらの記事が分かりやすかったです。

Unity Discussion Undo Redo in GraphView

  • Undo/RedoをGraphView上でどう実現するかについて。

Unity Discussion Best way of Implementing copy and pasting o nodes in graph view?

  • Copy&Paste実装の参考に。

Unityの最新UIシステム「UI Toolkit」でランタイムUIを作成する方法まとめ

  • UIToolkitの初回キャッチアップはこちらの記事にお世話になりました。

UI Toolkit を学ぶ

  • UIToolkitを学ぶ上での参考資料集がまとめられており、ガッツリ勉強したい方に良さそうです。
  • この記事内で紹介されているいくつかの動画を参考にさせていただきました。

UnityDocumentation USSセレクター

  • 公式。UnityStyleSheetの仕様についてはこちらを(CSSと大体似ているとは思いますが)。

【Unity】UI Toolkitにおけるイベント伝播の仕組みとハンドリング方法

  • UIToolkitのイベント伝播の仕様について。
  • 今回困ることはありませんでしたが、注意すべき仕様なので抑えておきたいです。
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?