Files
Gen_Hack-and-Slash-Roguelite/Client/Assets/Scripts/UI/SkillTreePageUI.cs

579 lines
32 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using System.Linq;
using Data;
using Managers;
using UnityEngine;
using Utils;
namespace UI
{
/// <summary>
/// SkillTreePageUI 类负责在UI中生成和布局技能树。
/// 它通过处理技能定义、创建UI节点并根据其层级关系进行定位来展示技能树。
/// </summary>
public class SkillTreePageUI : MonoBehaviour
{
// 常量定义,用于技能树节点的布局间距。
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; // 页面上下内边距
private const float MinWeight = 1920;
private const float MinHeight = 1000;
[SerializeField] private RectTransform rectTransform; // 技能树UI页面的RectTransform
[SerializeField] private SkillTreeNodeUI skillTreeNodeUIPrefab; // 技能树节点UI预制体
public Vector2 Size => rectTransform.sizeDelta;
public SkillTreeUI skillTreeUI;
/// <summary>
/// 辅助方法:获取一个节点所在的整个连通组件(通过父子关系可达的所有节点)。
/// 此方法使用广度优先搜索 (BFS) 遍历,同时探索正向边(子节点)和反向边(父节点)。
/// </summary>
/// <param name="startNode">开始遍历的节点。</param>
/// <param name="allGraphNodes">整个图中的所有节点集合,用于限定搜索范围。</param>
/// <param name="adjList">正向邻接列表 (节点 -> 子节点)。</param>
/// <param name="revAdjList">反向邻接列表 (节点 -> 父节点)。</param>
/// <returns>一个包含当前连通组件所有节点的HashSet。</returns>
private HashSet<SkillTreeDef> GetConnectedComponent(
SkillTreeDef startNode,
HashSet<SkillTreeDef> allGraphNodes,
Dictionary<SkillTreeDef, List<SkillTreeDef>> adjList,
Dictionary<SkillTreeDef, List<SkillTreeDef>> revAdjList)
{
var componentNodes = new HashSet<SkillTreeDef>();
var queue = new Queue<SkillTreeDef>();
// 确保起始节点属于当前图的有效节点集合,并将其加入队列和组件集合
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;
}
/// <summary>
/// 根据指定的标签生成技能树的层级结构,并将独立的技能树结构分开。
/// 保证每层的父节点都在前层,同层不存在父子关系,在此基础上尽量减少层数并尽量靠前。
/// </summary>
/// <param name="tag">要生成层级的技能树标签。</param>
/// <returns>
/// 一个包含多层SkillTreeDef列表的列表的列表。
/// 外层List代表独立的技能树结构中间List代表该树中的层内层List代表该层中的技能节点。
/// 如果没有找到相关节点或发生错误,返回空列表。
/// </returns>
public List<List<List<SkillTreeDef>>> GenerateSkillTreeLayers(string tag)
{
// 用于快速查找一个节点是否属于当前tag集合仅包含本次处理的节点。
// 这是处理步骤中所有相关节点的缓存,确保我们只关注与指定标签相关联的节点。
HashSet<SkillTreeDef> allNodesInCurrentGraph;
// 1. 获取所有带有指定tag的技能节点
var allTaggedNodes = SkillTreeManager.Instance.GetNodesByTag(tag);
if (allTaggedNodes == null || allTaggedNodes.Count == 0)
{
// 如果没有找到任何带有指定标签的节点,则返回一个空列表,表示没有技能树可生成。
allNodesInCurrentGraph = new HashSet<SkillTreeDef>(); // 初始化为空集合,保持一致性
return new List<List<List<SkillTreeDef>>>();
}
// 缓存所有标记节点以便快速查找此Set是后续图构建的节点范围
allNodesInCurrentGraph = new HashSet<SkillTreeDef>(allTaggedNodes);
// 2. 初始化全局数据结构入度In-degree、正向邻接列表Adjacency List和反向邻接列表Reverse Adjacency List
// 这些结构用于完整描述所有相关节点之间的关系,是构建连通分量的基础。
var inDegrees = new Dictionary<SkillTreeDef, int>(); // 全局入度,用于拓扑排序的初始值(后会被局部化)
var adjList = new Dictionary<SkillTreeDef, List<SkillTreeDef>>(); // 全局正向邻接列表 (节点 -> 子节点)
var revAdjList = new Dictionary<SkillTreeDef, List<SkillTreeDef>>(); // 全局反向邻接列表 (节点 -> 父节点)
foreach (var node in allTaggedNodes)
{
inDegrees[node] = 0; // 初始化所有节点的入度为0
adjList[node] = new List<SkillTreeDef>(); // 初始化邻接列表
revAdjList[node] = new List<SkillTreeDef>(); // 初始化反向邻接列表
}
// 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<List<List<SkillTreeDef>>>();
// 追踪在连通分量发现过程中是否已访问的节点,确保每个节点只被分派到一个独立的连通分量中。
var visitedNodesForComponents = new HashSet<SkillTreeDef>();
// 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<SkillTreeDef, int>();
var componentAdjList = new Dictionary<SkillTreeDef, List<SkillTreeDef>>();
// 辅助集合,方便在当前组件内快速查找节点,优化性能
var componentNodesSet = new HashSet<SkillTreeDef>(currentComponentNodes);
foreach (var node in currentComponentNodes)
{
componentInDegrees[node] = 0; // 初始化为0
componentAdjList[node] = new List<SkillTreeDef>();
}
// 重新计算组件内部的入度和邻接列表,只考虑组件内部的关系
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<List<SkillTreeDef>>();
var queue = new Queue<SkillTreeDef>();
var processedNodesInComponent = new HashSet<SkillTreeDef>(); // 追踪当前组件已处理的节点
// 收集所有局部入度为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<SkillTreeDef>();
var nextQueue = new Queue<SkillTreeDef>(); // 用于存储下一层的节点
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 是未处理的)
var alreadyAddedAsLayerNode = false;
foreach (var layer in currentTreeLayers)
if (layer.Contains(cycleNode))
{
alreadyAddedAsLayerNode = true;
break;
}
if (!alreadyAddedAsLayerNode)
// 将每个循环节点视为一个独立的、单层的技能树
allSeparateTreesLayers.Add(new List<List<SkillTreeDef>>
{ new() { 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<List<SkillTreeDef>> { new() { nodeToAdd } });
}
}
return allSeparateTreesLayers;
}
/// <summary>
/// 根据指定的SkillTreeDef列表实例化SkillTreeNodeUI节点。
/// 只负责创建UI元素不进行数据绑定或位置设置。
/// </summary>
/// <param name="skillDefs">一层中的技能定义列表。</param>
/// <returns>已实例化的SkillTreeNodeUI列表。</returns>
private List<SkillTreeNodeUI> GenerateLayerNodesUI(List<SkillTreeDef> skillDefs)
{
var layerNodesUI = new List<SkillTreeNodeUI>();
foreach (var skillDef in skillDefs)
{
if (skillDef == null) continue; // 防止空引用
var nodeUI = Instantiate(skillTreeNodeUIPrefab, rectTransform);
nodeUI.name = $"SkillNode_{skillDef.defName}";
nodeUI.skillTreeUI = skillTreeUI;
nodeUI.Init(skillDef);
layerNodesUI.Add(nodeUI);
}
return layerNodesUI;
}
/// <summary>
/// 计算一个SkillTreeNodeUI层的总高度和总宽度用于布局。
/// 假定层内的节点是纵向排列的。
/// </summary>
/// <param name="layerNodes">已实例化的一层SkillTreeNodeUI节点列表。</param>
/// <param name="allTaggedNodesInGraph">所有具有指定标签的节点集合,用于过滤父节点。</param>
/// <returns>包含总高度和总宽度的元组。</returns>
private (float totalHeight, float totalWidth) CalculateLayerDimensions(List<SkillTreeNodeUI> layerNodes,
HashSet<SkillTreeDef> 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);
}
/// <summary>
/// 根据指定的标签生成并布局所有独立的技能树。
/// 该方法将根据内部逻辑将技能树分解为独立的组件和层级然后将它们实例化为UI节点并定位。
/// </summary>
/// <param name="tag">要生成和布局的技能树标签。</param>
public void GenerateAndLayoutAllSkillTrees(string tag)
{
// 1. 清理现有UI节点
// 遍历rectTransform的所有子对象销毁所有SkillTreeNodeUI实例。
// 使用ToList()避免在循环中修改集合 (重要:否则会在循环中修改集合导致迭代器失效)
foreach (var child in rectTransform.transform.Cast<Transform>().ToList())
if (child.GetComponent<SkillTreeNodeUI>() != 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<SkillTreeDef>(
allSeparateTreesLayers.Where(outerList => outerList != null) // 过滤掉 null 的外层列表
.SelectMany(outerList => outerList.Where(middleList => middleList != null)) // 展平外层列表
.SelectMany(middleList => middleList.Where(skillTree => skillTree != null)) // 展平中间列表
.ToList());
List<List<List<SkillTreeNodeUI>>> nodes = new();
var treeSize = new List<(float, float)>();
foreach (var trees in allSeparateTreesLayers)
{
var treeNodes = new List<List<SkillTreeNodeUI>>();
var size = (x: 0f, y: 0f);
foreach (var layer in trees)
{
var layerNodes = GenerateLayerNodesUI(layer);
var layerSize = CalculateLayerDimensions(layerNodes, allSeparateTreesLayersHashSet);
treeNodes.Add(new List<SkillTreeNodeUI>(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(Mathf.Max(MinWeight, pageSize.width), Mathf.Max(MinHeight, 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);
nodeY += NODE_VERTICAL_SPACING +
node.GetRequiredNodeHeight(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 = StringUtils.StringToVector2(nodeUI.SkillTreeDefine.position);
nodeUI.rectTransform.anchoredPosition = pos;
}
var sortedNodes = nodes
.SelectMany(middle => middle) // List<List<SkillTreeNodeUI>>
.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<Vector2>();
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());
}
}
/// <summary>
/// 获取子节点在父节点下的顺序。
/// 顺序基于 sortedNodes (Y轴位置降序) 的排列。
/// </summary>
/// <param name="sortedNodes">所有UI节点已按Y轴位置降序排序。</param>
/// <param name="childDef">要查询顺序的子节点的 SkillTreeDef。</param>
/// <param name="parentDef">子节点所属的父节点的 SkillTreeDef。</param>
/// <param name="nodeDefToNodeUI">SkillTreeDef 到 SkillTreeNodeUI 的映射字典。</param>
/// <returns>子节点在父节点下的0-based顺序如果找不到或关系不正确则返回 -1。</returns>
private static int GetChildOrderUnderParent(
SkillTreeNodeUI[] sortedNodes,
SkillTreeDef childDef,
SkillTreeDef parentDef,
Dictionary<SkillTreeDef, SkillTreeNodeUI> 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 =
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;
}
private void ReorderParentNodesBySortedNodesAlternative(SkillTreeNodeUI[] sortedNodes,
List<SkillTreeDef> parentNodes)
{
var set = new HashSet<SkillTreeDef>(parentNodes); // 需要重排的集合
var result = new List<SkillTreeDef>(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);
}
}
}