using System.Collections.Generic; using UnityEngine; using System.Linq; using Data; using Managers; namespace UI { /// /// SkillTreePageUI 类负责在UI中生成和布局技能树。 /// 它通过处理技能定义、创建UI节点并根据其层级关系进行定位来展示技能树。 /// public class SkillTreePageUI : MonoBehaviour { [SerializeField] private RectTransform rectTransform; // 技能树UI页面的RectTransform [SerializeField] private SkillTreeNodeUI skillTreeNodeUIPrefab; // 技能树节点UI预制体 // 常量定义,用于技能树节点的布局间距。 private const float NODE_HORIZONTAL_SPACING = 500f; // 节点水平间距 (父子层之间) private const float NODE_VERTICAL_SPACING = 150f; // 节点垂直间距 (同层节点之间或不同树之间) private const float PAGE_PADDING_X = 100f; // 页面左右内边距 private const float PAGE_PADDING_Y = 100f; // 页面上下内边距 public Vector2 Size=>rectTransform.sizeDelta; /// /// 辅助方法:获取一个节点所在的整个连通组件(通过父子关系可达的所有节点)。 /// 此方法使用广度优先搜索 (BFS) 遍历,同时探索正向边(子节点)和反向边(父节点)。 /// /// 开始遍历的节点。 /// 整个图中的所有节点集合,用于限定搜索范围。 /// 正向邻接列表 (节点 -> 子节点)。 /// 反向邻接列表 (节点 -> 父节点)。 /// 一个包含当前连通组件所有节点的HashSet。 private HashSet GetConnectedComponent( SkillTreeDef startNode, HashSet allGraphNodes, Dictionary> adjList, Dictionary> revAdjList) { var componentNodes = new HashSet(); var queue = new Queue(); // 确保起始节点属于当前图的有效节点集合,并将其加入队列和组件集合 if (allGraphNodes.Contains(startNode) && componentNodes.Add(startNode)) { queue.Enqueue(startNode); } else { // 如果起始节点本身就不在allGraphNodes中,直接返回空集合 return componentNodes; } while (queue.Count > 0) { var current = queue.Dequeue(); // 探索子节点:遍历当前节点的直接子节点,如果它们属于当前图且尚未被发现,则加入队列 if (adjList.TryGetValue(current, out var children) && children != null) { foreach (var child in children) { // 确保子节点也在当前图中,并且是新发现的节点 if (allGraphNodes.Contains(child) && componentNodes.Add(child)) { queue.Enqueue(child); } } } // 探索父节点:遍历当前节点的直接父节点,如果它们属于当前图且尚未被发现,则加入队列 if (revAdjList.TryGetValue(current, out var parents) && parents != null) { foreach (var parent in parents) { // 确保父节点也在当前图中,并且是新发现的节点 if (allGraphNodes.Contains(parent) && componentNodes.Add(parent)) { queue.Enqueue(parent); } } } } return componentNodes; } /// /// 根据指定的标签生成技能树的层级结构,并将独立的技能树结构分开。 /// 保证每层的父节点都在前层,同层不存在父子关系,在此基础上尽量减少层数并尽量靠前。 /// /// 要生成层级的技能树标签。 /// 一个包含多层SkillTreeDef列表的列表的列表。 /// 外层List代表独立的技能树结构,中间List代表该树中的层,内层List代表该层中的技能节点。 /// 如果没有找到相关节点或发生错误,返回空列表。 public List>> GenerateSkillTreeLayers(string tag) { // 用于快速查找一个节点是否属于当前tag集合,仅包含本次处理的节点。 // 这是处理步骤中所有相关节点的缓存,确保我们只关注与指定标签相关联的节点。 HashSet allNodesInCurrentGraph; // 1. 获取所有带有指定tag的技能节点 var allTaggedNodes = SkillTreeManager.Instance.GetNodesByTag(tag); if (allTaggedNodes == null || allTaggedNodes.Count == 0) { // 如果没有找到任何带有指定标签的节点,则返回一个空列表,表示没有技能树可生成。 allNodesInCurrentGraph = new HashSet(); // 初始化为空集合,保持一致性 return new List>>(); } // 缓存所有标记节点以便快速查找,此Set是后续图构建的节点范围 allNodesInCurrentGraph = new HashSet(allTaggedNodes); // 2. 初始化全局数据结构:入度(In-degree)、正向邻接列表(Adjacency List)和反向邻接列表(Reverse Adjacency List) // 这些结构用于完整描述所有相关节点之间的关系,是构建连通分量的基础。 var inDegrees = new Dictionary(); // 全局入度,用于拓扑排序的初始值(后会被局部化) var adjList = new Dictionary>(); // 全局正向邻接列表 (节点 -> 子节点) var revAdjList = new Dictionary>(); // 全局反向邻接列表 (节点 -> 父节点) foreach (var node in allTaggedNodes) { inDegrees[node] = 0; // 初始化所有节点的入度为0 adjList[node] = new List(); // 初始化邻接列表 revAdjList[node] = new List(); // 初始化反向邻接列表 } // 3. 填充inDegrees、adjList和revAdjList:遍历所有节点,建立完整的图结构 foreach (var node in allTaggedNodes) { // 计算当前节点的入度 (只计算父节点也在当前allNodesInCurrentGraph集合中的依赖) var directParents = SkillTreeManager.Instance.GetAllDirectParents(node); if (directParents != null) { foreach (var parent in directParents) { if (parent != null && allNodesInCurrentGraph.Contains(parent)) { inDegrees[node]++; // 填充反向邻接列表:记录当前节点的父节点 revAdjList[node].Add(parent); // 理论上这里的ContainsKey检查可以省略,因为所有节点已初始化 } } } // 填充邻接列表 (只添加子节点也在当前allNodesInCurrentGraph集合中的连接) var directChildren = SkillTreeManager.Instance.GetAllDirectChildren(node); if (directChildren != null) { foreach (var child in directChildren) { if (child != null && allNodesInCurrentGraph.Contains(child)) { adjList[node].Add(child); } } } } // 存储所有独立的技能树的层级结果 var allSeparateTreesLayers = new List>>(); // 追踪在连通分量发现过程中是否已访问的节点,确保每个节点只被分派到一个独立的连通分量中。 var visitedNodesForComponents = new HashSet(); // 4.1 识别独立的连通分量,并对每个分量独立进行拓扑排序(Kahn算法) foreach (var startNode in allTaggedNodes) { if (!visitedNodesForComponents.Contains(startNode)) { // 发现一个尚未处理过的新连通分量 var currentComponentNodes = GetConnectedComponent(startNode, allNodesInCurrentGraph, adjList, revAdjList); // 将此连通分量中的所有节点标记为已访问,避免重复处理 foreach (var nodeInComponent in currentComponentNodes) { visitedNodesForComponents.Add(nodeInComponent); } // 4.2 为当前连通分量准备各自的局部入度和局部邻接列表(构建此组件的局部图) // 这一步是关键,它确保拓扑排序只考虑当前组件内部的依赖关系,不被外部组件影响。 var componentInDegrees = new Dictionary(); var componentAdjList = new Dictionary>(); // 辅助集合,方便在当前组件内快速查找节点,优化性能 var componentNodesSet = new HashSet(currentComponentNodes); foreach (var node in currentComponentNodes) { componentInDegrees[node] = 0; // 初始化为0 componentAdjList[node] = new List(); } // 重新计算组件内部的入度和邻接列表,只考虑组件内部的关系 foreach (var node in currentComponentNodes) { // 计算局部入度: 只考虑父节点也在当前组件内的依赖关系 if (revAdjList.TryGetValue(node, out var parents) && parents != null) { foreach (var parent in parents) { if (componentNodesSet.Contains(parent)) // 只有父节点也在当前组件内,才计入局部入度 { componentInDegrees[node]++; } } } // 填充局部邻接列表: 只添加子节点也在当前组件内的连接 if (adjList.TryGetValue(node, out var children) && children != null) { foreach (var child in children) { if (componentNodesSet.Contains(child)) // 只有子节点也在当前组件内,才计入局部邻接 { componentAdjList[node].Add(child); } } } } // 4.3 对当前连通分量执行Kahn算法进行分层 var currentTreeLayers = new List>(); var queue = new Queue(); var processedNodesInComponent = new HashSet(); // 追踪当前组件已处理的节点 // 收集所有局部入度为0的起始节点,作为Kahn算法的第一层节点 foreach (var node in currentComponentNodes) { if (componentInDegrees[node] == 0) { queue.Enqueue(node); // 入度为0的节点是当前DAG的起点 } } var totalNodesInComponent = currentComponentNodes.Count; // 记录组件总节点数,用于循环检测 // Kahn算法主循环:逐层处理节点 while (queue.Count > 0) { var currentLayerSize = queue.Count; var currentLayerNodes = new List(); var nextQueue = new Queue(); // 用于存储下一层的节点 for (var i = 0; i < currentLayerSize; i++) { var node = queue.Dequeue(); // 取出当前层的一个节点 currentLayerNodes.Add(node); // 加入当前层的列表 processedNodesInComponent.Add(node); // 标记为已处理 // 遍历其子节点,减少子节点的入度。如果子节点入度变为0,则加入下一层队列。 if (componentAdjList.TryGetValue(node, out var children) && children != null) { foreach (var child in children) { // 理论上 child 肯定在 componentInDegrees 中,因为 componentAdjList 是根据 componentNodesSet 填充的 componentInDegrees[child]--; if (componentInDegrees[child] == 0) { nextQueue.Enqueue(child); // 入度为0,成为下一层的节点 } } } } // 如果当前层有节点,则将其添加到此技能树的层级结构中 if (currentLayerNodes.Count > 0) { currentTreeLayers.Add(currentLayerNodes); } queue = nextQueue; // 进入下一层处理 } // 4.4 检查当前组件处理结果:是否存在循环依赖 if (processedNodesInComponent.Count != totalNodesInComponent) { // 如果已处理节点数不等于组件总节点数,则表示存在循环,Kahn算法未能完全排序。 var remainingCycleNodes = currentComponentNodes .Where(n => !processedNodesInComponent.Contains(n)) .ToList(); Debug.LogWarning( $"[技能树] 标签 '{tag}' 的一个连通组件中检测到循环依赖。无法完全分层。构成循环的节点:{string.Join(", ", remainingCycleNodes.Select(n => n.defName))}"); // 处理策略: // ① 将已成功分层的部分(无环部分)作为独立的技能树添加。 if (currentTreeLayers.Any()) { allSeparateTreesLayers.Add(currentTreeLayers); } // ② 对于构成循环的节点,由于它们无法满足“父节点在前层”和“同层无父子关系”的要求, // 将它们作为独立的单层“树”添加到结果中,以最小化错误并保留信息。 foreach (var cycleNode in remainingCycleNodes) { // 确保此循环节点尚未作为层中的一部分被添加(理论上不会,因为 remainingCycleNodes 是未处理的) bool alreadyAddedAsLayerNode = false; foreach (var layer in currentTreeLayers) { if (layer.Contains(cycleNode)) { alreadyAddedAsLayerNode = true; break; } } if (!alreadyAddedAsLayerNode) { // 将每个循环节点视为一个独立的、单层的技能树 allSeparateTreesLayers.Add(new List> { new List { cycleNode } }); } } } else { // 如果所有节点都已处理,则该连通组件是无环的,成功分层。 if (currentTreeLayers.Any()) { allSeparateTreesLayers.Add(currentTreeLayers); } // 理论上,如果 totalNodesInComponent > 0 且 processedNodesInComponent == totalNodesInComponent, // 那么 currentTreeLayers 应该至少包含一层。此警告用于防御性编程。 else if (totalNodesInComponent > 0) { Debug.LogWarning( $"[技能树] 标签 '{tag}' 的一个无环连通组件被完全处理,但未生成任何层。这可能表示组件只包含一个独立节点,但未被正确处理。节点:{string.Join(", ", currentComponentNodes.Select(n => n.defName))}"); } } } } // 5. 最终检查:理论上,所有 allTaggedNodes 都应该被 visitedNodesForComponents 标记。 // 如果此检查触发,则表示 GetConnectedComponent 或组件发现逻辑存在漏洞。 if (visitedNodesForComponents.Count != allTaggedNodes.Count) { var unvisitedNodes = allTaggedNodes .Where(n => !visitedNodesForComponents.Contains(n)) .Select(n => n.defName) .ToList(); Debug.LogError($"[技能树] 标签 '{tag}' 有未被连通分量识别逻辑处理的节点。可能存在逻辑错误。未处理节点:{string.Join(", ", unvisitedNodes)}"); // 这些节点如果存在,也应该被添加到结果中,作为孤立的单层树处理,以防意外丢失数据。 foreach (var nodeDefName in unvisitedNodes) { // 确保获取到原始的SkillTreeDef对象来添加 var nodeToAdd = allTaggedNodes.First(n => n.defName == nodeDefName); allSeparateTreesLayers.Add( new List>() { new List() { nodeToAdd } }); } } return allSeparateTreesLayers; } /// /// 根据指定的SkillTreeDef列表实例化SkillTreeNodeUI节点。 /// 只负责创建UI元素,不进行数据绑定或位置设置。 /// /// 一层中的技能定义列表。 /// 已实例化的SkillTreeNodeUI列表。 private List GenerateLayerNodesUI(List skillDefs) { var layerNodesUI = new List(); foreach (var skillDef in skillDefs) { if (skillDef == null) continue; // 防止空引用 var nodeUI = Instantiate(skillTreeNodeUIPrefab, rectTransform); nodeUI.name = $"SkillNode_{skillDef.defName}"; nodeUI.Init(skillDef); layerNodesUI.Add(nodeUI); } return layerNodesUI; } /// /// 计算一个SkillTreeNodeUI层的总高度和总宽度,用于布局。 /// 假定层内的节点是纵向排列的。 /// /// 已实例化的一层SkillTreeNodeUI节点列表。 /// 所有具有指定标签的节点集合,用于过滤父节点。 /// 包含总高度和总宽度的元组。 private (float totalHeight, float totalWidth) CalculateLayerDimensions(List layerNodes, HashSet allTaggedNodesInGraph) { if (layerNodes == null || !layerNodes.Any()) { return (0f, 0f); // 空层,返回0高度和宽度 } var totalHeight = 0f; var maxWidth = 0f; foreach (var nodeUI in layerNodes) { if (nodeUI.SkillTreeDefine == null) { Debug.LogWarning($"[技能树] 技能树节点UI '{nodeUI.name}' 没有关联的技能定义。跳过此节点的尺寸计算。"); continue; // 无法计算,跳过此节点 } // 精确计算连接线数量:获取所有父节点,并筛选出属于当前tag的父节点 var actualLinkLineCount = 0; var directParents = SkillTreeManager.Instance.GetAllDirectParents(nodeUI.SkillTreeDefine); if (directParents != null) { // 仅计算那些来自当前技能图谱内部的、具有指定tag的父节点作为连接线 actualLinkLineCount = directParents.Count(parent => parent != null && allTaggedNodesInGraph.Contains(parent)); } // 使用实际的连接线数量来计算节点高度 totalHeight += nodeUI.GetRequiredNodeHeight(actualLinkLineCount); // 更新层的最大宽度 if (nodeUI.rectTransform != null) { maxWidth = Mathf.Max(maxWidth, nodeUI.rectTransform.rect.width); } else { Debug.LogWarning($"[技能树] 技能树节点UI '{nodeUI.name}' 的 RectTransform 为空。无法计算宽度。"); } } // 添加节点之间的垂直间距 if (layerNodes.Count > 1) { totalHeight += (layerNodes.Count - 1) * NODE_VERTICAL_SPACING; } return (totalHeight, maxWidth); } public (float width, float height) CalculatePageSize(List<(float width, float height)> treeSize) { if (treeSize == null || !treeSize.Any()) { // 如果列表为空,可以返回默认值或者抛出异常 return (PAGE_PADDING_X * 2, PAGE_PADDING_Y * 2); } // 高度相加 var totalHeight = treeSize.Sum(node => node.height); // 加上 NODE_VERTICAL_SPACING * (数量 - 1) if (treeSize.Count > 1) { totalHeight += NODE_VERTICAL_SPACING * (treeSize.Count - 1); } // 加上 PAGE_PADDING_Y * 2 totalHeight += PAGE_PADDING_Y * 2; // 宽度取大值 var maxWidth = treeSize.Max(node => node.width); // 加上 PAGE_PADDING_X * 2 var totalWidth = maxWidth + PAGE_PADDING_X * 2; return (totalWidth, totalHeight); } /// /// 根据指定的标签生成并布局所有独立的技能树。 /// 该方法将根据内部逻辑将技能树分解为独立的组件和层级,然后将它们实例化为UI节点并定位。 /// /// 要生成和布局的技能树标签。 public void GenerateAndLayoutAllSkillTrees(string tag) { // 1. 清理现有UI节点 // 遍历rectTransform的所有子对象,销毁所有SkillTreeNodeUI实例。 // 使用ToList()避免在循环中修改集合 (重要:否则会在循环中修改集合导致迭代器失效) foreach (var child in rectTransform.transform.Cast().ToList()) { if (child.GetComponent() != null) { Destroy(child.gameObject); } } // 2. 获取所有独立的技能树的层级结构 // Outer List: 独立的技能树 // Middle List: 某棵树的层 // Inner List: 某层中的技能定义 var allSeparateTreesLayers = GenerateSkillTreeLayers(tag); if (allSeparateTreesLayers == null || !allSeparateTreesLayers.Any()) { Debug.LogWarning($"[SkillTreePageUI] 没有找到标签 '{tag}' 的技能树节点或未能生成层级。"); // 如果没有内容,将页面尺寸重置为0,或者保持默认大小 rectTransform.sizeDelta = Vector2.zero; // 或者根据需要设置一个默认值 return; } var allSeparateTreesLayersHashSet = new HashSet( allSeparateTreesLayers.Where(outerList => outerList != null) // 过滤掉 null 的外层列表 .SelectMany(outerList => outerList.Where(middleList => middleList != null)) // 展平外层列表 .SelectMany(middleList => middleList.Where(skillTree => skillTree != null)) // 展平中间列表 .ToList()); List>> nodes = new(); var treeSize = new List<(float, float)>(); foreach (var trees in allSeparateTreesLayers) { var treeNodes = new List>(); var size = (x: 0f, y: 0f); foreach (var layer in trees) { var layerNodes = GenerateLayerNodesUI(layer); var layerSize = CalculateLayerDimensions(layerNodes, allSeparateTreesLayersHashSet); treeNodes.Add(new List(layerNodes)); size.x += layerSize.totalWidth; size.y = Mathf.Max(size.y, layerSize.totalHeight); } size.x += NODE_HORIZONTAL_SPACING * (trees.Count - 1); treeSize.Add(size); nodes.Add(treeNodes); } var pageSize = CalculatePageSize(treeSize); rectTransform.sizeDelta = new Vector2(pageSize.width, pageSize.height); var treeX = PAGE_PADDING_X; var treeY = PAGE_PADDING_Y; for (var i = 0; i < nodes.Count; i++) { var treeNodes = nodes[i]; var currentTreeSize = treeSize[i]; var layerX = treeX; var layerY = treeY + currentTreeSize.Item2 / 2f; for (var j = 0; j < treeNodes.Count; j++) { var layerNodes = treeNodes[j]; var layerSize = CalculateLayerDimensions(layerNodes, allSeparateTreesLayersHashSet); var nodeY = layerY - layerSize.totalHeight / 2; foreach (var node in layerNodes) { node.rectTransform.anchoredPosition = new Vector2(layerX, nodeY); Debug.Log($"{layerX},{nodeY}"); nodeY += NODE_VERTICAL_SPACING + node.GetRequiredNodeHeight(Managers.SkillTreeManager.Instance .GetAllDirectParents(node.SkillTreeDefine).Count); } layerX += NODE_HORIZONTAL_SPACING + layerSize.totalWidth; } treeY += currentTreeSize.Item2 + NODE_VERTICAL_SPACING; } foreach (var i in nodes) { foreach (var j in i) { foreach (var nodeUI in j) { if(string.IsNullOrEmpty(nodeUI.SkillTreeDefine.position)) continue; var pos=Utils.StringUtils.StringToVector2(nodeUI.SkillTreeDefine.position); nodeUI.rectTransform.anchoredPosition = pos; } } } var sortedNodes = nodes .SelectMany(middle => middle) // List> .SelectMany(inner => inner) // SkillTreeNodeUI .OrderByDescending(n => n.rectTransform.localPosition.y) // 或 OrderBy .ToArray(); var nodeDefToNodeUI = nodes .SelectMany(middle => middle) .SelectMany(inner => inner) .Where(node => node != null && node.SkillTreeDefine != null) .GroupBy(node => node.SkillTreeDefine) .ToDictionary(g => g.Key, g => g.First()); foreach (var node in sortedNodes) { var parentOutputPoint = new List(); var parentNodes=SkillTreeManager.Instance.GetAllDirectParents(node.SkillTreeDefine); ReorderParentNodesBySortedNodesAlternative(sortedNodes,parentNodes); foreach (var parentNodeDef in parentNodes) { var index = GetChildOrderUnderParent(sortedNodes, node.SkillTreeDefine, parentNodeDef, nodeDefToNodeUI); var parentNode=nodeDefToNodeUI[parentNodeDef]; var outputCount = SkillTreeManager.Instance.GetAllDirectChildren(parentNodeDef).Count; var point= parentNode.GetOutputPosition(outputCount,index); parentOutputPoint.Add(point); } node.LinkLine(parentOutputPoint.ToArray()); } } /// /// 获取子节点在父节点下的顺序。 /// 顺序基于 sortedNodes (Y轴位置降序) 的排列。 /// /// 所有UI节点,已按Y轴位置降序排序。 /// 要查询顺序的子节点的 SkillTreeDef。 /// 子节点所属的父节点的 SkillTreeDef。 /// SkillTreeDef 到 SkillTreeNodeUI 的映射字典。 /// 子节点在父节点下的0-based顺序,如果找不到或关系不正确则返回 -1。 private static int GetChildOrderUnderParent( SkillTreeNodeUI[] sortedNodes, SkillTreeDef childDef, SkillTreeDef parentDef, Dictionary nodeDefToNodeUI) { // 1. 参数验证 if (sortedNodes == null || sortedNodes.Length == 0 || childDef == null || parentDef == null || nodeDefToNodeUI == null) { Debug.LogWarning("GetChildOrderUnderParent: Input parameters cannot be null or empty."); return -1; } // 2. 获取父节点的所有直接子节点 (SkillTreeDef) var allDirectChildrenDefs = Managers.SkillTreeManager.Instance.GetAllDirectChildren(parentDef); if (allDirectChildrenDefs == null || allDirectChildrenDefs.Count == 0) { // 父节点没有直接子节点 return -1; } // 验证 childDef 是否确实是 parentDef 的直接子节点 if (!allDirectChildrenDefs.Contains(childDef)) { return -1; } // 3. 从 sortedNodes 中筛选出属于 allDirectChildrenDefs 且在 nodeDefToNodeUI 中有对应UI的节点 // 由于 sortedNodes 已经按 Y 轴排序,过滤后的列表也保持这个顺序 var orderedSiblingsUI = sortedNodes .Where(nodeUI => nodeUI != null && nodeUI.SkillTreeDefine != null && allDirectChildrenDefs.Contains(nodeUI.SkillTreeDefine) && nodeDefToNodeUI.ContainsKey(nodeUI.SkillTreeDefine) // 确保有UI对象映射 ) .ToList(); // 4. 查找 childDef 在 orderedSiblingsUI 中的索引 // 遍历找到 childDef 对应的 UI 节点 for (var i = 0; i < orderedSiblingsUI.Count; i++) { if (orderedSiblingsUI[i].SkillTreeDefine == childDef) { return i; // 返回 0-based 索引 } } // 如果 childDef 找不到对应的 UI 节点,或者不在 orderedSiblingsUI 列表中(尽管前面检查过关系) // 这种情况理论上不应该发生,除非 nodeDefToNodeUI 或 sortedNodes 有问题 Debug.LogWarning( $"GetChildOrderUnderParent: Could not find UI node for childDef '{childDef.defName}' among its siblings, or mapping is inconsistent."); return -1; } void ReorderParentNodesBySortedNodesAlternative(SkillTreeNodeUI[] sortedNodes, List parentNodes) { var set = new HashSet(parentNodes); // 需要重排的集合 var result = new List(parentNodes.Count); foreach (var node in sortedNodes) { var def = node.SkillTreeDefine; if (def != null && set.Remove(def)) // 存在则添加并从集合中移除,避免重复 { result.Add(def); } } // 把剩余的(未在 sortedNodes 中出现的)按原顺序追加 foreach (var def in parentNodes) { if (set.Contains(def)) result.Add(def); } parentNodes.Clear(); parentNodes.AddRange(result); } } }