最近做了一个科技树的功能,如上图树形结构,
- 一个节点有多个父节点/多个叶子节点,
- 要求叶子节点,整体在多个父节点的中间
算法思路是简单的,不过实现起来我是碰到了一堆问题,特别是递归导致卡死又得重启项目。
- 设置 root 的位置
- 布局叶子节点位置(由于每棵子树的宽度都不一致,必定导致叶子节点重合)
- 从最后一层开始,判断同一层相邻的叶子节点(n1,n2)是否重合,重合 移动 n1与n2共同的父节点下的n2的父节点 parentNode,向下一个yInterval间距.
- 由于移动了parentNode,parentNode的父节点中心就发生了偏移,所以中心对齐parentNode的父节点下的叶子节点
我项目用的 lua 写的,我做了个unity C# 的 damo https://download.csdn.net/download/weixin_41316824/86461090有需要的可以下载,下是树布局的核心两个类,
至于树的连线挺简单的我就不再用c#实现了
定义节点类
public class Node : MonoBehaviour
{
public float x;
public float y;
public int inParentIndex = 0;
// 所在树的层级
public int layer;
public List<Node> parents = new List<Node>();
public List<Node> childs = new List<Node>();
public NodeData data;
private void Start()
{
GetComponentInChildren<Text>().text = transform.name;
}
public void AddParent(Node parent)
{
if (!parents.Contains(parent))
{
parents.Add(parent);
}
}
}
定义tree
using System.Collections;
using System.Collections.Generic;
using UnityEditor.PackageManager;
using UnityEngine;
public class Tree : MonoBehaviour
{
private float spacingX;
private float spacingY;
private float nodeWidth;
private float nodeHeight;
private float xInterval;
private float yInterval;
public Node root;
// 每一层的节点信息
// 0
// 101 102
// 111 112 113 114
// 121
private List<List<Node>> hashNodes = new List<List<Node>>();
void Start()
{
spacingX = 20;
spacingY = 20;
nodeHeight = 100;
nodeWidth = 100;
xInterval = nodeWidth + spacingX;
yInterval = nodeHeight + spacingY;
InitTree();
Layout();
}
private void InitTree()
{
if (root == null)
{
Debug.LogError("请初始化 root");
return;
}
InitNode(root);
}
private void InitNode(Node node)
{
if (node == root)
{
node.layer = 0;
}
AddHashNode(node);
for (int i = 0; i < node.childs.Count; i++)
{
Node tmpNode = node.childs[i];
tmpNode.AddParent(node);
tmpNode.layer = node.layer + 1;
tmpNode.inParentIndex = i;
InitNode(tmpNode);
}
}
private void AddHashNode(Node node)
{
List<Node> layerNodes;
if (hashNodes.Count < node.layer + 1)
{
layerNodes = new List<Node>();
hashNodes.Add(layerNodes);
}
else
{
layerNodes = hashNodes[node.layer];
}
if(!layerNodes.Contains(node))
layerNodes.Add(node);
}
private void Layout()
{
LayoutChild(root);
LayoutOverlaps();
RefreshNodePosition(root);
}
// 布局子节点位置
private void LayoutChild(Node node)
{
if (node == root)
{
Vector3 p = node.transform.localPosition;
node.x = p.x;
node.y = p.y;
}
for (int i = 0; i < node.childs.Count; i++)
{
Node tmpNode = node.childs[i];
float centerY = GetCenterYByParents(tmpNode.parents);
// 便宜距离
float dy = 0;
tmpNode.x = node.x + xInterval;
// 最高点的位置 node.childs.Count - 1 :最上 + 最下 占用了一个间距
float start = centerY + (node.childs.Count - 1) * yInterval / 2;
float targetY = start - tmpNode.inParentIndex * yInterval;
dy = targetY - tmpNode.y;
if (dy != 0)
{
TranslateTree(tmpNode, tmpNode.y + dy);
}
LayoutChild(tmpNode);
}
}
// 移动树到目标位置
private void TranslateTree(Node node,float y)
{
float dy = y - node.y;
node.y = y;
for (int i = 0; i < node.childs.Count; i++)
{
Node tmpNode = node.childs[i];
if (tmpNode.parents.Count > 1)
{
float centerY = GetCenterYByParents(tmpNode.parents);
// 最高点的位置 node.childs.Count - 1 :最上 + 最下 占用了一个间距
float start = centerY + (node.childs.Count - 1) * yInterval / 2;
float targetY = start - tmpNode.inParentIndex * yInterval;
dy = targetY - tmpNode.y;
}
TranslateTree(tmpNode,tmpNode.y + dy);
}
}
// 获取中心位置
private float GetCenterYByParents(List<Node> parents)
{
if (parents.Count == 0) return 0;
if (parents.Count == 1)
{
return parents[0].y;
}
return (parents[0].y + parents[parents.Count - 1].y) / 2;
}
private void RefreshNodePosition(Node node)
{
node.transform.localPosition = new Vector3(node.x, node.y, 0);
for (int i = 0; i < node.childs.Count; i++)
{
RefreshNodePosition(node.childs[i]);
}
}
// 回推布局,从最底层开始,往上检索,查找重叠节点,调整优化树的布局
private void LayoutOverlaps()
{
for (int i = hashNodes.Count - 1; i >= 0; i--)
{
List<Node> layerNodes = hashNodes[i];
for (int j = 0; j < layerNodes.Count - 1; j++)
{
Node n1 = layerNodes[j];
Node n2 = layerNodes[j + 1];
// 重合了
if (IsOverlaps(n1, n2))
{
Debug.Log("重合了");
// 移动 n1与n2的同一个祖先节点下的 n2的祖先
Node moveNode = GetCommonParentN2Parent(n1,n2);
float y = moveNode.y - yInterval;
TranslateTree(moveNode,y);
CenterChild(moveNode.parents);
// 移动之后重新判断是否有重合
i = hashNodes.Count;
break;
}
}
}
}
// 中心对齐子物体
private void CenterChild(List<Node> parents)
{
float centerY = GetCenterYByParents(parents);
float dy = centerY - GetCenterYByParents(parents[0].childs);
for (int i = 0; i < parents[0].childs.Count; i++)
{
Node tmp = parents[0].childs[i];
TranslateTree(tmp,tmp.y + dy);
}
}
// 找到 你n1与n2共同的祖先节点,祖先节点下n2的祖先
private Node GetCommonParentN2Parent(Node n1,Node n2)
{
if (n1.parents[0] == n2.parents[0])
{
return n2;
}
return GetCommonParentN2Parent(n1.parents[0], n2.parents[0]);
}
// 是否重叠 n1为高处 n2为低处位置
private bool IsOverlaps(Node n1,Node n2)
{
return (n1.y - n2.y) < yInterval;
}
}