【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博客

相关推荐
在路上看风景12 小时前
4.5 顶点和片元
unity
LYOBOYI12320 小时前
vscode界面美化
ide·vscode·编辑器
浔川python社21 小时前
关于浔川代码编辑器 v5.0 网页版上线时间的通知
编辑器
在路上看风景1 天前
31. Unity 异步加载的底层细节
unity
天人合一peng1 天前
Unity中做表头时像work中整个调整宽窄
unity
浔川python社1 天前
浔川代码编辑器 v5.0 上线时间公布
编辑器
山峰哥1 天前
数据库工程与SQL调优——从索引策略到查询优化的深度实践
数据库·sql·性能优化·编辑器
Doro再努力1 天前
Vim 快速上手实操手册:从入门到生产环境实战
linux·编辑器·vim
Doro再努力1 天前
【Linux操作系统10】Makefile深度解析:从依赖推导到有效编译
android·linux·运维·服务器·编辑器·vim
小李也疯狂2 天前
Unity 中的立方体贴图(Cubemaps)
unity·游戏引擎·贴图·cubemap