【Unity编辑器拓展】GraphView自定义可视化节点

1、创建节点区域脚本

其中的new class UxmlFactory,可以让该元素显示在UI Builder中,我们就可以在Library-Project中看到我们新建的这两个UI元素,就可以拖入我们的UI窗口编辑了

cs 复制代码
public class NodeTreeViewer : GraphView
{
    public new class UxmlFactory : UxmlFactory<NodeTreeViewer, UxmlTraits> { }
}

默认的GraphView是一片黑屏。在这里,我们给我们的GraphView窗口添加上网格和拖拽缩放功能。

cs 复制代码
public class NodeTreeViewer : GraphView
{
    public new class UxmlFactory : UxmlFactory<NodeTreeViewer, UxmlTraits> { }

    public NodeTree tree;
    public Action<NodeView> OnNodeSelected;

    public NodeTreeViewer()
    {
        Insert(0, new GridBackground());
        this.AddManipulator(new ContentZoomer());
        this.AddManipulator(new ContentDragger());
        this.AddManipulator(new SelectionDragger());
        this.AddManipulator(new RectangleSelector());
        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/NodeEditor/Editor/UI/NodeTreeViewer.uss");
        styleSheets.Add(styleSheet);
    }
}

uss代码参考,上面代码的uss路径要根据项目实际路径进行设置。

cs 复制代码
GridBackground{
    --grid-background-color: rgb(40,40,40);
    --line-color: rgba(193,196,192,0.1);
    --thick-line-color: rgba(193,196,192,0.1);
    --spacing: 15;
}

2、创建节点和删除选中元素

2.1 创建节点类

cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

public class NodeView : UnityEditor.Experimental.GraphView.Node
{
    public Node node;
    public Port input;
    public Port output;
    public Action<NodeView> OnNodeSelected;
    public NodeView(Node node)
    {
        this.node = node;
        this.title = node.name;
        this.viewDataKey = node.guid;
        style.left = node.position.x;
        style.top = node.position.y;
        CreateInputPorts();
        CreateOutputPorts();
    }

    //创建输入端口
    private void CreateInputPorts()
    {
        input = InstantiatePort(Orientation.Vertical, Direction.Input, Port.Capacity.Multi, typeof(bool));
        if(input != null )
        {
            input.portName = "";
            inputContainer.Add(input);
        }
    }

    //创建输出端口
    private void CreateOutputPorts()
    {
        output = InstantiatePort(Orientation.Vertical, Direction.Output, Port.Capacity.Multi, typeof(bool));
        if (output != null)
        {
            output.portName = "";
            outputContainer.Add(output);
        }
    }

    //选中该节点时传递事件
    public override void OnSelected()
    {
        base.OnSelected();
        if( OnNodeSelected != null )
        {
            OnNodeSelected?.Invoke(this);
        }
    }

    //设置生成时位置
    public override void SetPosition(Rect newPos)
    {
        base.SetPosition(newPos);
        node.position.x = newPos.xMin;
        node.position.y = newPos.yMin;
    }

}

2.2 节点区域创建节点和删除选中元素功能

cs 复制代码
    //重写该方法,可以添加右键菜单按钮
    public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
    {
        var types = TypeCache.GetTypesDerivedFrom<Node>();
        foreach (var type in types)
        {
            evt.menu.AppendAction($"创建节点/{type.Name}", a => CreateNode(type));
        }
        evt.menu.AppendAction("删除选中元素", DeleteSelecteNode);
    }

    //删除选中元素,节点或者连线
    private void DeleteSelecteNode(DropdownMenuAction action)
    {
        DeleteSelection();
    }

    //创建节点
    private void CreateNode(Type type)
    {
        Node node = tree.CreateNode(type);
        CreateNodeView(node);
    }

    private void CreateNodeView(Node node)
    {
        NodeView nodeView = new NodeView(node);
        nodeView.OnNodeSelected = OnNodeSelected;
        AddElement(nodeView);
    }

3、设置节点元素输出端可连接端口

cs 复制代码
    public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
    {
        return ports.ToList().Where(
            endpost => endpost.direction != startPort.direction 
            && endpost.node != startPort.node).ToList();
    }

4、打开或者重新展示已有内容

cs 复制代码
    internal void PopulateView(NodeTree tree)
    {
        this.tree = tree;
        graphViewChanged -= OnGraphViewChange;
        DeleteElements(graphElements.ToList());
        graphViewChanged += OnGraphViewChange;
        tree.nodes.ForEach(n => CreateNodeView(n));
        tree.nodes.ForEach(n =>
        {
            var children = tree.GetChildren(n);
            children.ForEach(c =>
            {
                NodeView parentView = FindNodeView(n);
                NodeView childView = FindNodeView(c);
                Edge edge = parentView.output.ConnectTo(childView.input);
                AddElement(edge);
            });
        });
    }

5、当节点区域元素改变时,实现对应逻辑数据的修改

该方法在打开或展现时注册事件

cs 复制代码
    private GraphViewChange OnGraphViewChange(GraphViewChange graphViewChange)
    {
        if(graphViewChange.elementsToRemove != null)
        {
            graphViewChange.elementsToRemove.ForEach(elem => { 
                NodeView nodeview = elem as NodeView;
                if(nodeview != null)
                {
                    tree.DeleteNode(nodeview.node);
                }
                Edge edge = elem as Edge;
                if(edge != null)
                {
                    NodeView parentView = edge.output.node as NodeView;
                    NodeView childView = edge.input.node as NodeView;
                    tree.RemoveChild(parentView.node, childView.node);
                }
            });
        }

        if(graphViewChange.edgesToCreate != null)
        {
            graphViewChange.edgesToCreate.ForEach(edge =>
            {
                NodeView parentView = edge.output.node as NodeView;
                NodeView childView = edge.input.node as NodeView;
                tree.AddChild(parentView.node, childView.node);
            });
        }
        return graphViewChange;
    }

6、完整代码

运行时代码Runtime Code

6.1 Node

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class Node : ScriptableObject
{
    public enum State { Running, Waiting}

    public State state = State.Waiting;
    public bool started = false;
    public List<Node> children = new List<Node>();

    [HideInInspector] public string guid;
    [HideInInspector] public Vector2 position;

    public Node OnUpdate()
    {
        if(!started)
        {
            OnStart();
            started = true;
        }
        Node currentNode = LogicUpdate();
        if(state != State.Running)
        {
            OnStop();
            started = false;
        }
        return currentNode;
    }

    public abstract Node LogicUpdate();

    public abstract void OnStart();

    public abstract void OnStop();

}

6.2 NormalNode

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu]
public class NormalNode : Node
{
    [TextArea]
    public string dialogueContent;

    public override Node LogicUpdate()
    {
        // 判断进入下一节点条件成功时 需将节点状态改为非运行中 且 返回对应子节点
        if (Input.GetKeyDown(KeyCode.Space))
        {
            state = State.Waiting;
            if (children.Count > 0)
            {
                children[0].state = State.Running;
                return children[0];
            }
        }
        return this;
    }
    public override void OnStart()
    {
        Debug.Log(dialogueContent);
    }

    public override void OnStop()
    {
        Debug.Log("OnStop");
    }
}

6.3 NodeTree

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[CreateAssetMenu]
public class NodeTree : ScriptableObject
{
    public Node rootNode;
    public Node runningNode;
    public Node.State treeState = Node.State.Waiting;
    public List<Node> nodes = new List<Node>();

    public virtual void Update()
    {
        if(treeState == Node.State.Running && runningNode.state == Node.State.Running)
        {
            runningNode = runningNode.OnUpdate();
        }
    }

    /// <summary>
    /// 对话树开始的触发方法
    /// </summary>
    public virtual void OnTreeStart()
    {
        treeState = Node.State.Running;
        runningNode.state = Node.State.Running;
    }

    /// <summary>
    /// 对话树结束的触发方法
    /// </summary>
    public void OnTreeEnd()
    {
        treeState = Node.State.Waiting;
    }

#if UNITY_EDITOR
    public Node CreateNode(System.Type type)
    {
        Node node = ScriptableObject.CreateInstance(type) as Node;
        node.name = type.Name;
        node.guid = GUID.Generate().ToString();
        nodes.Add(node);
        if (!Application.isPlaying)
        {
            AssetDatabase.AddObjectToAsset(node, this);
        }
        AssetDatabase.SaveAssets();

        return node;
    }

    public Node DeleteNode(Node node)
    {
        nodes.Remove(node);
        AssetDatabase.RemoveObjectFromAsset(node);
        AssetDatabase.SaveAssets();
        return node;
    }

    public void RemoveChild(Node parent, Node child)
    {
        parent.children.Remove(child);
    }

    public void AddChild(Node parent, Node child)
    {
        parent.children.Add(child);
    }

    public List<Node> GetChildren(Node parent)
    {
        return parent.children;
    }
#endif
}

6.4 NodeTreeRunner

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NodeTreeRunner : MonoBehaviour
{
    public NodeTree tree;
     
    void Start()
    {
        
    }
     
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.P))
        {
            tree.OnTreeStart();
        }
        if(tree != null && tree.treeState == Node.State.Running)
        {
            tree.Update();
        }
        if(Input.GetKeyDown(KeyCode.D))
        {
            tree.OnTreeEnd();
        }
    }
}

可视化编辑器代码 Editor

6.5 Uxml和Uss

NodeEditor Uxml

XML 复制代码
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
    <Style src="NodeEditor.uss" />
    <ui:VisualElement style="flex-grow: 1; flex-direction: row;">
        <ui:VisualElement name="LeftDiv" style="flex-grow: 0.3;">
            <ui:Label text="Inspector" display-tooltip-when-elided="true" name="Inspector" style="font-size: 15px; -unity-font-style: bold;" />
            <uie:ObjectField label="NodeTree" name="NodeTree" style="flex-grow: 0; flex-shrink: 0; min-width: auto; align-items: stretch; flex-wrap: nowrap; flex-direction: row; width: auto; max-width: none;" />
            <InspectorViewer style="flex-grow: 1;" />
        </ui:VisualElement>
        <ui:VisualElement name="RightDiv" style="flex-grow: 0.7;">
            <ui:Label text="NodeTreeVirwer" display-tooltip-when-elided="true" name="NodeTreeVirwer" style="-unity-font-style: bold; font-size: 15px;" />
            <NodeTreeViewer focusable="true" style="flex-grow: 1;" />
        </ui:VisualElement>
    </ui:VisualElement>
</ui:UXML>

NodeTreeViewer Uss

css 复制代码
GridBackground{
    --grid-background-color: rgb(40,40,40);
    --line-color: rgba(193,196,192,0.1);
    --thick-line-color: rgba(193,196,192,0.1);
    --spacing: 15;
}

编辑器面板代码

6.6 NodeEdtor

cs 复制代码
using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;


public class NodeEditor : EditorWindow
{
    public NodeTreeViewer nodeTreeViewer;
    public InspectorViewer inspectorViewer;
    public ObjectField nodeTreeObj;

    [MenuItem("MyWindows/NodeEditor")]
    public static void ShowExample()
    {
        NodeEditor wnd = GetWindow<NodeEditor>();
        wnd.titleContent = new GUIContent("NodeEditor");
    }

    public void CreateGUI()
    {
        // Each editor window contains a root VisualElement object
        VisualElement root = rootVisualElement;


        // Import UXML
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/NodeEditor/Editor/UI/NodeEditor.uxml");
        visualTree.CloneTree(root);

        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/NodeEditor/Editor/UI/NodeEditor.uss");
        root.styleSheets.Add(styleSheet);
        nodeTreeViewer = root.Q<NodeTreeViewer>();
        inspectorViewer = root.Q<InspectorViewer>();
        nodeTreeObj = root.Q("NodeTree") as ObjectField;
        nodeTreeObj.objectType = typeof(NodeTree);
        nodeTreeViewer.OnNodeSelected = OnNodeSelectionChanged;
    }

    private void OnNodeSelectionChanged(NodeView view)
    {
        inspectorViewer.UpdateSelection(view.node);
    }

    private void OnSelectionChange()
    {
        NodeTree tree = Selection.activeObject as NodeTree;
        if (tree)
        {
            nodeTreeViewer.PopulateView(tree);
            nodeTreeObj.value = tree;
        }
        else
        {
            nodeTreeViewer.CloseNodeTreeViewer();
            nodeTreeObj.value = null;
        }
    }

}

6.7 NodeTreeViewer

cs 复制代码
using BehaviorDesigner.Runtime.Tasks.Unity.UnityInput;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

public class NodeTreeViewer : GraphView
{
    public new class UxmlFactory : UxmlFactory<NodeTreeViewer, UxmlTraits> { }

    public NodeTree tree;
    public Action<NodeView> OnNodeSelected;

    private Vector2 curMousePos;
    ContentZoomer contentZoomer;
    ContentDragger contentDragger;

    public NodeTreeViewer()
    {
        Insert(0, new GridBackground());
        contentZoomer = new ContentZoomer();
        this.AddManipulator(contentZoomer);
        contentDragger = new ContentDragger();
        this.AddManipulator(contentDragger);
        this.AddManipulator(new SelectionDragger());
        this.AddManipulator(new RectangleSelector());
        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/NodeEditor/Editor/UI/NodeTreeViewer.uss");
        styleSheets.Add(styleSheet);

        this.RegisterCallback<MouseDownEvent>(OnMouseDown);

    }

    private void OnMouseDown(MouseDownEvent evt)
    {
        Debug.Log(evt.localMousePosition);
        curMousePos = evt.localMousePosition;
        Debug.Log(contentZoomer.scaleStep);
        Debug.Log(contentZoomer.referenceScale);
        //Debug.Log(contentDragger.p)
    }

    public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
    {
        var types = TypeCache.GetTypesDerivedFrom<Node>();
        foreach (var type in types)
        {
            evt.menu.AppendAction($"创建节点/{type.Name}", a => CreateNode(type));
        }
        evt.menu.AppendAction("删除选中元素", DeleteSelecteNode);
    }

    private void DeleteSelecteNode(DropdownMenuAction action)
    {
        DeleteSelection();
    }

    private void CreateNode(Type type)
    {
        Node node = tree.CreateNode(type);
        node.position = curMousePos;
        CreateNodeView(node);
    }

    private void CreateNodeView(Node node)
    {
        NodeView nodeView = new NodeView(node);
        nodeView.OnNodeSelected = OnNodeSelected;
        AddElement(nodeView);
    }

    internal void PopulateView(NodeTree tree)
    {
        this.tree = tree;
        graphViewChanged -= OnGraphViewChange;
        DeleteElements(graphElements.ToList());
        graphViewChanged += OnGraphViewChange;
        tree.nodes.ForEach(n => CreateNodeView(n));
        tree.nodes.ForEach(n =>
        {
            var children = tree.GetChildren(n);
            children.ForEach(c =>
            {
                NodeView parentView = FindNodeView(n);
                NodeView childView = FindNodeView(c);
                Edge edge = parentView.output.ConnectTo(childView.input);
                AddElement(edge);
            });
        });
    }

    public void CloseNodeTreeViewer()
    {
        this.tree = null;
        graphViewChanged -= OnGraphViewChange;
        DeleteElements(graphElements.ToList());
    }

    private GraphViewChange OnGraphViewChange(GraphViewChange graphViewChange)
    {
        if(graphViewChange.elementsToRemove != null)
        {
            graphViewChange.elementsToRemove.ForEach(elem => { 
                NodeView nodeview = elem as NodeView;
                if(nodeview != null)
                {
                    tree.DeleteNode(nodeview.node);
                }
                Edge edge = elem as Edge;
                if(edge != null)
                {
                    NodeView parentView = edge.output.node as NodeView;
                    NodeView childView = edge.input.node as NodeView;
                    tree.RemoveChild(parentView.node, childView.node);
                }
            });
        }

        if(graphViewChange.edgesToCreate != null)
        {
            graphViewChange.edgesToCreate.ForEach(edge =>
            {
                NodeView parentView = edge.output.node as NodeView;
                NodeView childView = edge.input.node as NodeView;
                tree.AddChild(parentView.node, childView.node);
            });
        }
        return graphViewChange;
    }

    NodeView FindNodeView(Node node)
    {
        return GetNodeByGuid(node.guid) as NodeView;
    }

    public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
    {
        return ports.ToList().Where(
            endpost => endpost.direction != startPort.direction 
            && endpost.node != startPort.node).ToList();
    }

}

6.8 NodeView

cs 复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;

public class NodeView : UnityEditor.Experimental.GraphView.Node
{
    public Node node;
    public Port input;
    public Port output;
    public Action<NodeView> OnNodeSelected;
    public NodeView(Node node)
    {
        this.node = node;
        this.title = node.name;
        this.viewDataKey = node.guid;
        style.left = node.position.x;
        style.top = node.position.y;
        CreateInputPorts();
        CreateOutputPorts();
    }

    private void CreateInputPorts()
    {
        input = InstantiatePort(Orientation.Vertical, Direction.Input, Port.Capacity.Multi, typeof(bool));
        if(input != null )
        {
            input.portName = "input";
            inputContainer.Add(input);
        }
    }

    private void CreateOutputPorts()
    {
        output = InstantiatePort(Orientation.Vertical, Direction.Output, Port.Capacity.Multi, typeof(bool));
        if (output != null)
        {
            output.portName = "output";
            outputContainer.Add(output);
        }
    }

    public override void OnSelected()
    {
        base.OnSelected();
        if( OnNodeSelected != null )
        {
            OnNodeSelected?.Invoke(this);
        }
    }

    public override void SetPosition(Rect newPos)
    {
        Debug.Log(newPos);
        base.SetPosition(newPos);
        node.position.x = newPos.xMin;
        node.position.y = newPos.yMin;
    }

}

6.9 InspectorViewer

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

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

    Editor editor;

    public InspectorViewer()
    {
        //this.AddManipulator(new drag)
    }

    internal void UpdateSelection(Node node)
    {
        Clear();
        UnityEngine.Object.DestroyImmediate(editor);
        editor = Editor.CreateEditor(node);
        IMGUIContainer container = new IMGUIContainer(() =>
        {
            if (editor.target)
            {
                editor.OnInspectorGUI();
            }
        });
        Add(container);
    }

}

【Unity UIToolkit】UIBuilder基础教程-制作简易的对话系统编辑器 3步教你玩转Unity编辑器扩展工具_unity uibuilder-CSDN博客

[Unity] GraphView 可视化节点的事件行为树(二) UI Toolkit介绍,制作事件行为树的UI_unity graphview-CSDN博客

相关推荐
神码编程1 小时前
【Unity功能集】TextureShop纹理工坊(五)选区
unity·游戏引擎·shader·ps选区
一棵开花的树,枝芽无限靠近你3 小时前
【PPTist】组件结构设计、主题切换
前端·笔记·学习·编辑器
0xdadream3 小时前
typora数学符号
编辑器
m0_748251726 小时前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity
benben0446 小时前
Unity3D仿星露谷物语开发7之事件创建动画
unity·游戏引擎
带电的小王6 小时前
VSCode:VSCode安装 -- 最简洁的VSCode安装教程
ide·vscode·编辑器
林枫依依9 小时前
Unity2021.3.16f1可以正常打开,但是Unity2017.3.0f3却常常打开闪退或者Unity2017编辑器运行起来就闪退掉
unity
m0_748241239 小时前
ElasticPDF-新国产 PDF 编辑器开发框架(基于 pdf.js Web PDF批注开发,实现高亮多边形橡皮擦历史记录保存注释文字)
前端·pdf·编辑器
sg_knight11 小时前
VSCode如何修改默认扩展路径和用户文件夹目录到D盘
前端·ide·vscode·编辑器·web