版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
Unity3D节点编辑器——树状节点布局算法
1.原始需求
在编辑器中显示出树状图,希望能满足以下几个要求:
(1)节点将会根据父节点的中点作为中间,分两边布局
(2)节点与节点之间不能覆盖,并留有一些空隙
2.算法详情
2.1节点布局算法
(1)递归计算每一个树状节点需要的宽度(NeedWidth),即子节点需要的宽度累加决定父节点需要的宽度。
(2)将当前节点的子节点所需要的宽度进行累加,求出子节点所在层级所需的宽度(curLayerNeedWidth)
(3)子节点所在层级所需的宽度(curLayerNeedWidth)除于2,即为以当前节点为中心开始的布局的初始位置(initOffset)
(4)从初始位置(initOffset)开始,累加对应节点所需宽度,即为子节点应在的位置
2.2 点与点间的画线算法
(1)当子节点数为1时,直接绘制当前节点到子节点位置的连线
(2)当子节点数大于1时,先绘制一条横线,起始点x为第一个子节点的中点值,y为两个父子节点之间高度差除于2;终点x为最后一个子节点的中点值,y为两个父子节点之间高度差除于2
3.核心代码
3.1 节点数据结构
using UnityEngine;
public class EditorNodeData
{
/// <summary>节点ID </summary>
public int NodeId;
/// <summary>节点位置 </summary>
public Rect WindowRect;
/// <summary>节点所需宽度 </summary>
public float NeedWidth;
}
3.2 节点所需宽度计算方法
public void ComputeNodeNeedWidth(NodeData node,float width)
{
width += offset.x;
if (!NodeEditorWindow.NodeId2NodeDataRects.ContainsKey(node.Id))
{
EditorNodeData nodeData = new EditorNodeData();
nodeData.NodeId = node.Id;
NodeEditorWindow.NodeId2NodeDataRects.Add(node.Id, nodeData);
}
int count = node.ChildNodes.Count;
for (int i = 0; i < count; i++)
{
ComputeNodeNeedWidth(node.ChildNodes[i], width);
}
if (count==0)
{
NodeEditorWindow.NodeId2NodeDataRects[node.Id].NeedWidth = width;
}
else
{
float needWith = 0;
for (int i = 0; i < count; i++)
{
needWith += NodeEditorWindow.NodeId2NodeDataRects[node.ChildNodes[i].Id].NeedWidth;
}
NodeEditorWindow.NodeId2NodeDataRects[node.Id].NeedWidth = needWith;
}
}
3.3 节点之间画线方法
public void DrawLineNode2ChildNode(NodeData node)
{
Handles.color = Color.black;
Rect startRect;
Rect endRect;
if (node.ChildNodes.Count == 1)
{
startRect = NodeEditorWindow.NodeId2NodeDataRects[node.Id].WindowRect;
endRect= NodeEditorWindow.NodeId2NodeDataRects[node.ChildNodes[0].Id].WindowRect;
Vector3 verticalStartPoint = new Vector3(startRect.x + startRect.width / 2, startRect.y + startRect.height);
Vector3 verticalEndPoint = new Vector3(endRect.x + endRect.width / 2, endRect.y );
Handles.DrawLine(verticalStartPoint, verticalEndPoint);
return;
}
//多个子节点时,绘制横线
startRect = NodeEditorWindow.NodeId2NodeDataRects[node.ChildNodes[0].Id].WindowRect;
endRect = NodeEditorWindow.NodeId2NodeDataRects[node.ChildNodes.Last().Id].WindowRect;
Vector3 horizontalStartPoint = new Vector3(startRect.x+startRect.width/2, startRect.y - (startRect.height + offset.y) / 2, 0);
Vector3 horizontalEndPoint = new Vector3(endRect.x + endRect.width / 2, endRect.y - (endRect.height + offset.y) / 2, 0);
Handles.DrawLine(horizontalStartPoint, horizontalEndPoint);
//父节点引出 竖线
startRect = NodeEditorWindow.NodeId2NodeDataRects[node.Id].WindowRect;
endRect = NodeEditorWindow.NodeId2NodeDataRects[node.Id].WindowRect;
Vector3 parentNodeverticalStartPoint = new Vector3(startRect.x + startRect.width / 2, startRect.y + startRect.height);
Vector3 parentNodeverticalEndPoint = new Vector3(endRect.x + endRect.width / 2, endRect.y+endRect.height +(endRect.height+ offset.y)/2);
Handles.DrawLine(parentNodeverticalStartPoint, parentNodeverticalEndPoint);
for (int i = 0; i < node.ChildNodes.Count; i++)
{
startRect = NodeEditorWindow.NodeId2NodeDataRects[node.ChildNodes[i].Id].WindowRect;
endRect = NodeEditorWindow.NodeId2NodeDataRects[node.ChildNodes[i].Id].WindowRect;
Vector3 childNodeverticalStartPoint = new Vector3(startRect.x + startRect.width / 2, startRect.y);
Vector3 childNodeverticalEndPoint = new Vector3(endRect.x + endRect.width / 2, endRect.y - (endRect.height + offset.y) / 2);
Handles.DrawLine(childNodeverticalStartPoint, childNodeverticalEndPoint);
}
}
3.4 绘制节点方法
public override void DrawNode(object data,Rect rect)
{
NodeData node = (NodeData)data;
rect=GUILayout.Window(node.Id, rect, DrawNodeWindow, "My Window");
if (NodeEditorWindow.NodeId2NodeDataRects.ContainsKey(node.Id))
{
NodeEditorWindow.NodeId2NodeDataRects[node.Id].WindowRect = rect;
}
else
{
Debug.LogError("NodeId2NodeDataRects Not Contains node,nodeId is " + node.Id);
}
if (node.ChildNodes.Count > 0)
{
DrawLineNode2ChildNode(node);
int count = node.ChildNodes.Count;
float initOffset=0;
float curLayerNeedWidth = 0;
for (int i = 0; i < count; i++)
{
curLayerNeedWidth+= NodeEditorWindow.NodeId2NodeDataRects[node.ChildNodes[i].Id].NeedWidth;
}
for (int i = 0; i < count; i++)
{
Rect childRect;
float curNodeNeedWidth = NodeEditorWindow.NodeId2NodeDataRects[node.ChildNodes[i].Id].NeedWidth;
childRect = new Rect(rect.x - (curLayerNeedWidth / 2) + curNodeNeedWidth / 2 + initOffset , rect.y + rect.height*2 + offset.y, rect.width, rect.height);
initOffset += curNodeNeedWidth;
DrawNode(node.ChildNodes[i], childRect);
}
}
}
protected override void DrawNodeWindow(int id)
{
GUILayout.Label("Dialogue Node - ID: "+id);
if (GUILayout.Button("xxx"))
{
}
}