(client) feat:健康给予,路径优化,结算界面,商店界面 (#60)

Co-authored-by: m0_75251201 <m0_75251201@noreply.gitcode.com>
Reviewed-on: http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite/pulls/60
This commit is contained in:
2025-10-10 14:08:23 +08:00
parent 9a797479ff
commit 16b49f3d3a
1900 changed files with 114053 additions and 34157 deletions

View File

@@ -1,32 +1,36 @@
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using Data;
using Managers;
using UnityEngine;
using Utils;
namespace UI
{
/// <summary>
/// SkillTreePageUI 类负责在UI中生成和布局技能树。
/// 它通过处理技能定义、创建UI节点并根据其层级关系进行定位来展示技能树。
/// SkillTreePageUI 类负责在UI中生成和布局技能树。
/// 它通过处理技能定义、创建UI节点并根据其层级关系进行定位来展示技能树。
/// </summary>
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;
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) 遍历,同时探索正向边(子节点)和反向边(父节点)。
/// 辅助方法:获取一个节点所在的整个连通组件(通过父子关系可达的所有节点)。
/// 此方法使用广度优先搜索 (BFS) 遍历,同时探索正向边(子节点)和反向边(父节点)。
/// </summary>
/// <param name="startNode">开始遍历的节点。</param>
/// <param name="allGraphNodes">整个图中的所有节点集合,用于限定搜索范围。</param>
@@ -44,56 +48,42 @@ namespace UI
// 确保起始节点属于当前图的有效节点集合,并将其加入队列和组件集合
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>
/// <returns>
/// 一个包含多层SkillTreeDef列表的列表的列表
/// 外层List代表独立的技能树结构中间List代表该树中的层内层List代表该层中的技能节点。
/// 如果没有找到相关节点或发生错误,返回空列表。
/// </returns>
public List<List<List<SkillTreeDef>>> GenerateSkillTreeLayers(string tag)
{
// 用于快速查找一个节点是否属于当前tag集合仅包含本次处理的节点。
@@ -128,30 +118,20 @@ namespace UI
// 计算当前节点的入度 (只计算父节点也在当前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);
}
}
}
}
// 存储所有独立的技能树的层级结果
@@ -160,7 +140,6 @@ namespace UI
var visitedNodesForComponents = new HashSet<SkillTreeDef>();
// 4.1 识别独立的连通分量并对每个分量独立进行拓扑排序Kahn算法
foreach (var startNode in allTaggedNodes)
{
if (!visitedNodesForComponents.Contains(startNode))
{
// 发现一个尚未处理过的新连通分量
@@ -168,9 +147,7 @@ namespace UI
GetConnectedComponent(startNode, allNodesInCurrentGraph, adjList, revAdjList);
// 将此连通分量中的所有节点标记为已访问,避免重复处理
foreach (var nodeInComponent in currentComponentNodes)
{
visitedNodesForComponents.Add(nodeInComponent);
}
// 4.2 为当前连通分量准备各自的局部入度和局部邻接列表(构建此组件的局部图)
// 这一步是关键,它确保拓扑排序只考虑当前组件内部的依赖关系,不被外部组件影响。
@@ -189,27 +166,15 @@ namespace UI
{
// 计算局部入度: 只考虑父节点也在当前组件内的依赖关系
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算法进行分层
@@ -218,12 +183,8 @@ namespace UI
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算法主循环逐层处理节点
@@ -239,24 +200,16 @@ namespace UI
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 (componentInDegrees[child] == 0) nextQueue.Enqueue(child); // 入度为0成为下一层的节点
}
}
}
// 如果当前层有节点,则将其添加到此技能树的层级结构中
if (currentLayerNodes.Count > 0)
{
currentTreeLayers.Add(currentLayerNodes);
}
if (currentLayerNodes.Count > 0) currentTreeLayers.Add(currentLayerNodes);
queue = nextQueue; // 进入下一层处理
}
@@ -272,51 +225,39 @@ namespace UI
$"[技能树] 标签 '{tag}' 的一个连通组件中检测到循环依赖。无法完全分层。构成循环的节点:{string.Join(", ", remainingCycleNodes.Select(n => n.defName))}");
// 处理策略:
// ① 将已成功分层的部分(无环部分)作为独立的技能树添加。
if (currentTreeLayers.Any())
{
allSeparateTreesLayers.Add(currentTreeLayers);
}
if (currentTreeLayers.Any()) allSeparateTreesLayers.Add(currentTreeLayers);
// ② 对于构成循环的节点,由于它们无法满足“父节点在前层”和“同层无父子关系”的要求,
// 将它们作为独立的单层“树”添加到结果中,以最小化错误并保留信息。
foreach (var cycleNode in remainingCycleNodes)
{
// 确保此循环节点尚未作为层中的一部分被添加(理论上不会,因为 remainingCycleNodes 是未处理的)
bool alreadyAddedAsLayerNode = false;
var alreadyAddedAsLayerNode = false;
foreach (var layer in currentTreeLayers)
{
if (layer.Contains(cycleNode))
{
alreadyAddedAsLayerNode = true;
break;
}
}
if (!alreadyAddedAsLayerNode)
{
// 将每个循环节点视为一个独立的、单层的技能树
allSeparateTreesLayers.Add(new List<List<SkillTreeDef>>
{ new List<SkillTreeDef> { cycleNode } });
}
{ 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 或组件发现逻辑存在漏洞。
@@ -333,7 +274,7 @@ namespace UI
// 确保获取到原始的SkillTreeDef对象来添加
var nodeToAdd = allTaggedNodes.First(n => n.defName == nodeDefName);
allSeparateTreesLayers.Add(
new List<List<SkillTreeDef>>() { new List<SkillTreeDef>() { nodeToAdd } });
new List<List<SkillTreeDef>> { new() { nodeToAdd } });
}
}
@@ -341,8 +282,8 @@ namespace UI
}
/// <summary>
/// 根据指定的SkillTreeDef列表实例化SkillTreeNodeUI节点。
/// 只负责创建UI元素不进行数据绑定或位置设置。
/// 根据指定的SkillTreeDef列表实例化SkillTreeNodeUI节点。
/// 只负责创建UI元素不进行数据绑定或位置设置。
/// </summary>
/// <param name="skillDefs">一层中的技能定义列表。</param>
/// <returns>已实例化的SkillTreeNodeUI列表。</returns>
@@ -355,16 +296,18 @@ namespace UI
var nodeUI = Instantiate(skillTreeNodeUIPrefab, rectTransform);
nodeUI.name = $"SkillNode_{skillDef.defName}";
nodeUI.skillTreeUI = skillTreeUI;
nodeUI.Init(skillDef);
layerNodesUI.Add(nodeUI);
}
return layerNodesUI;
}
/// <summary>
/// 计算一个SkillTreeNodeUI层的总高度和总宽度用于布局。
/// 假定层内的节点是纵向排列的。
/// 计算一个SkillTreeNodeUI层的总高度和总宽度用于布局。
/// 假定层内的节点是纵向排列的。
/// </summary>
/// <param name="layerNodes">已实例化的一层SkillTreeNodeUI节点列表。</param>
/// <param name="allTaggedNodesInGraph">所有具有指定标签的节点集合,用于过滤父节点。</param>
@@ -372,10 +315,7 @@ namespace UI
private (float totalHeight, float totalWidth) CalculateLayerDimensions(List<SkillTreeNodeUI> layerNodes,
HashSet<SkillTreeDef> allTaggedNodesInGraph)
{
if (layerNodes == null || !layerNodes.Any())
{
return (0f, 0f); // 空层返回0高度和宽度
}
if (layerNodes == null || !layerNodes.Any()) return (0f, 0f); // 空层返回0高度和宽度
var totalHeight = 0f;
var maxWidth = 0f;
@@ -392,31 +332,22 @@ namespace UI
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;
}
if (layerNodes.Count > 1) totalHeight += (layerNodes.Count - 1) * NODE_VERTICAL_SPACING;
return (totalHeight, maxWidth);
}
@@ -424,18 +355,13 @@ namespace UI
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);
}
if (treeSize.Count > 1) totalHeight += NODE_VERTICAL_SPACING * (treeSize.Count - 1);
// 加上 PAGE_PADDING_Y * 2
totalHeight += PAGE_PADDING_Y * 2;
@@ -447,8 +373,8 @@ namespace UI
}
/// <summary>
/// 根据指定的标签生成并布局所有独立的技能树。
/// 该方法将根据内部逻辑将技能树分解为独立的组件和层级然后将它们实例化为UI节点并定位。
/// 根据指定的标签生成并布局所有独立的技能树。
/// 该方法将根据内部逻辑将技能树分解为独立的组件和层级然后将它们实例化为UI节点并定位。
/// </summary>
/// <param name="tag">要生成和布局的技能树标签。</param>
public void GenerateAndLayoutAllSkillTrees(string tag)
@@ -457,12 +383,8 @@ namespace UI
// 遍历rectTransform的所有子对象销毁所有SkillTreeNodeUI实例。
// 使用ToList()避免在循环中修改集合 (重要:否则会在循环中修改集合导致迭代器失效)
foreach (var child in rectTransform.transform.Cast<Transform>().ToList())
{
if (child.GetComponent<SkillTreeNodeUI>() != null)
{
Destroy(child.gameObject);
}
}
// 2. 获取所有独立的技能树的层级结构
// Outer List: 独立的技能树
@@ -504,7 +426,8 @@ namespace UI
}
var pageSize = CalculatePageSize(treeSize);
rectTransform.sizeDelta = new Vector2(pageSize.width, pageSize.height);
rectTransform.sizeDelta =
new Vector2(Mathf.Max(MinWeight, pageSize.width), Mathf.Max(MinHeight, pageSize.height));
var treeX = PAGE_PADDING_X;
var treeY = PAGE_PADDING_Y;
@@ -525,7 +448,7 @@ namespace UI
{
node.rectTransform.anchoredPosition = new Vector2(layerX, nodeY);
nodeY += NODE_VERTICAL_SPACING +
node.GetRequiredNodeHeight(Managers.SkillTreeManager.Instance
node.GetRequiredNodeHeight(SkillTreeManager.Instance
.GetAllDirectParents(node.SkillTreeDefine).Count);
}
@@ -536,19 +459,15 @@ namespace UI
}
foreach (var i in nodes)
foreach (var j in i)
foreach (var nodeUI in j)
{
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;
}
}
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
@@ -563,25 +482,25 @@ namespace UI
foreach (var node in sortedNodes)
{
var parentOutputPoint = new List<Vector2>();
var parentNodes=SkillTreeManager.Instance.GetAllDirectParents(node.SkillTreeDefine);
ReorderParentNodesBySortedNodesAlternative(sortedNodes,parentNodes);
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 index = GetChildOrderUnderParent(sortedNodes, node.SkillTreeDefine, parentNodeDef,
nodeDefToNodeUI);
var parentNode = nodeDefToNodeUI[parentNodeDef];
var outputCount = SkillTreeManager.Instance.GetAllDirectChildren(parentNodeDef).Count;
var point= parentNode.GetOutputPosition(outputCount,index);
var point = parentNode.GetOutputPosition(outputCount, index);
parentOutputPoint.Add(point);
}
node.LinkLine(parentOutputPoint.ToArray());
}
}
/// <summary>
/// 获取子节点在父节点下的顺序。
/// 顺序基于 sortedNodes (Y轴位置降序) 的排列。
/// 获取子节点在父节点下的顺序。
/// 顺序基于 sortedNodes (Y轴位置降序) 的排列。
/// </summary>
/// <param name="sortedNodes">所有UI节点已按Y轴位置降序排序。</param>
/// <param name="childDef">要查询顺序的子节点的 SkillTreeDef。</param>
@@ -604,18 +523,13 @@ namespace UI
// 2. 获取父节点的所有直接子节点 (SkillTreeDef)
var allDirectChildrenDefs =
Managers.SkillTreeManager.Instance.GetAllDirectChildren(parentDef);
SkillTreeManager.Instance.GetAllDirectChildren(parentDef);
if (allDirectChildrenDefs == null || allDirectChildrenDefs.Count == 0)
{
// 父节点没有直接子节点
return -1;
}
// 验证 childDef 是否确实是 parentDef 的直接子节点
if (!allDirectChildrenDefs.Contains(childDef))
{
return -1;
}
if (!allDirectChildrenDefs.Contains(childDef)) return -1;
// 3. 从 sortedNodes 中筛选出属于 allDirectChildrenDefs 且在 nodeDefToNodeUI 中有对应UI的节点
// 由于 sortedNodes 已经按 Y 轴排序,过滤后的列表也保持这个顺序
@@ -630,12 +544,8 @@ namespace UI
// 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 有问题
@@ -643,8 +553,9 @@ namespace UI
$"GetChildOrderUnderParent: Could not find UI node for childDef '{childDef.defName}' among its siblings, or mapping is inconsistent.");
return -1;
}
void ReorderParentNodesBySortedNodesAlternative(SkillTreeNodeUI[] sortedNodes, List<SkillTreeDef> parentNodes)
private void ReorderParentNodesBySortedNodesAlternative(SkillTreeNodeUI[] sortedNodes,
List<SkillTreeDef> parentNodes)
{
var set = new HashSet<SkillTreeDef>(parentNodes); // 需要重排的集合
var result = new List<SkillTreeDef>(parentNodes.Count);
@@ -653,22 +564,16 @@ namespace UI
{
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);
}
}
}
}