树形布局算法的实现

 最近做了一个科技树的功能,如上图树形结构,

  • 一个节点有多个父节点/多个叶子节点,
  • 要求叶子节点,整体在多个父节点的中间

算法思路是简单的,不过实现起来我是碰到了一堆问题,特别是递归导致卡死又得重启项目。

  1. 设置 root 的位置
  2. 布局叶子节点位置(由于每棵子树的宽度都不一致,必定导致叶子节点重合)
  3. 从最后一层开始,判断同一层相邻的叶子节点(n1,n2)是否重合,重合 移动 n1与n2共同的父节点下的n2的父节点 parentNode,向下一个yInterval间距.
  4. 由于移动了parentNode,parentNode的父节点中心就发生了偏移,所以中心对齐parentNode的父节点下的叶子节点

我项目用的 lua 写的,我做了个unity C# 的 damo

icon-default.png?t=M7J4

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;
    }
}

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...