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