(client) feat:技能树节点及其素材
This commit is contained in:
166
Client/Assets/Scripts/UI/ColorBar.cs
Normal file
166
Client/Assets/Scripts/UI/ColorBar.cs
Normal file
@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
// 自定义梯度效果类,继承自BaseMeshEffect
|
||||
public class ColorBar : BaseMeshEffect
|
||||
{
|
||||
[SerializeField, Range(0f, 360f)] // 使用Range特性,让角度在Inspector中更直观
|
||||
private float angle = 0f; // 渐变角度 (0-360度),0度为水平向右,90度为水平向上
|
||||
|
||||
[SerializeField]
|
||||
public Color32 color1 = Color.white; // 渐变起始颜色,默认白色
|
||||
|
||||
[SerializeField]
|
||||
public Color32 color2 = Color.white; // 渐变结束颜色,默认白色
|
||||
|
||||
[SerializeField, Range(0f, 1f)] // 限制范围在0到1之间,0表示全渐变,1表示无渐变(所有颜色为color2)
|
||||
private float range = 0f; // 渐变范围。此值用于缩短渐变覆盖的距离,0表示渐变覆盖所有范围,1表示所有像素都显示color2。
|
||||
|
||||
[SerializeField]
|
||||
private bool isFlip = false; // 是否翻转渐变方向,默认不翻转,影响color1和color2的分布
|
||||
|
||||
// 缓存Graphic组件引用,避免GetComponent频繁调用
|
||||
private Graphic m_Graphic;
|
||||
public Graphic graphic
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_Graphic == null)
|
||||
{
|
||||
m_Graphic = GetComponent<Graphic>();
|
||||
}
|
||||
return m_Graphic;
|
||||
}
|
||||
}
|
||||
|
||||
// 当在Inspector中修改参数时,刷新UI
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
Refresh(); // 刷新网格
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (graphic != null)
|
||||
{
|
||||
graphic.SetVerticesDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// 重写ModifyMesh方法,用于修改UI元素的网格
|
||||
public override void ModifyMesh(VertexHelper vh)
|
||||
{
|
||||
if (!IsActive() || vh.currentVertCount == 0) // 如果组件未激活 或 没有顶点,则不执行后续操作
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var vertices = new List<UIVertex>(); // 创建顶点列表
|
||||
// 遍历所有顶点并添加到列表中
|
||||
for (var i = 0; i < vh.currentVertCount; i++)
|
||||
{
|
||||
var uIVertex = new UIVertex();
|
||||
vh.PopulateUIVertex(ref uIVertex, i); // 填充顶点信息
|
||||
vertices.Add(uIVertex);
|
||||
}
|
||||
|
||||
// 计算渐变方向向量
|
||||
Vector2 gradientDir = AngleToVector2(angle);
|
||||
|
||||
// 计算所有顶点在此方向上的最小和最大投影值
|
||||
float minProjection = float.MaxValue;
|
||||
float maxProjection = float.MinValue;
|
||||
|
||||
for (var i = 0; i < vh.currentVertCount; i++)
|
||||
{
|
||||
// 注意:UIVertex.position 是 Vector3,但对于UI渐变通常只关心在Canvas 2D平面上的投影
|
||||
// 这里我们使用x,y分量进行点积,以获得在渐变方向上的相对“深度”或“进度”
|
||||
Vector2 vertexPos2D = vertices[i].position;
|
||||
float projection = Vector2.Dot(vertexPos2D, gradientDir);
|
||||
|
||||
if (projection < minProjection) minProjection = projection;
|
||||
if (projection > maxProjection) maxProjection = projection;
|
||||
}
|
||||
|
||||
// 如果所有顶点投影相同(例如,只有一个点,或所有点在这个渐变方向上是平齐的),则没有渐变范围
|
||||
if (Mathf.Approximately(maxProjection, minProjection))
|
||||
{
|
||||
// 在这种情况下,所有顶点颜色应相同。为了与range=1时的行为一致,设置为color2。
|
||||
for (var i = 0; i < vh.currentVertCount; i++)
|
||||
{
|
||||
var vertex = vertices[i];
|
||||
vertex.color = color2;
|
||||
vh.SetUIVertex(vertex, i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算渐变范围的实际有效长度。
|
||||
// 原始代码中的 (1f - range) 乘法意味着 range=0 时渐变覆盖100%,range=1 时覆盖0%。
|
||||
// 它的效果是“压缩”渐变区域,使得渐变在UI元素的一端更早地达到color1。
|
||||
// 例如,如果 range = 0.5,那么渐变只覆盖了总长度的一半。
|
||||
float totalProjectionRange = maxProjection - minProjection;
|
||||
float effectiveGradientLength = totalProjectionRange * (1f - range);
|
||||
|
||||
for (var i = 0; i < vh.currentVertCount; i++)
|
||||
{
|
||||
var vertex = vertices[i];
|
||||
Vector2 vertexPos2D = vertex.position;
|
||||
float projection = Vector2.Dot(vertexPos2D, gradientDir);
|
||||
|
||||
float t; // 归一化的插值因子 [0, 1]
|
||||
|
||||
// 计算归一化的插值因子
|
||||
// 如果 effectiveGradientLength 为0 (即 range=1),t 将是0,表示始终取 color2。
|
||||
if (Mathf.Approximately(effectiveGradientLength, 0f))
|
||||
{
|
||||
t = 0f; // 渐变长度为0,所有顶点颜色都将是 color2 (在Lerp前)
|
||||
}
|
||||
else
|
||||
{
|
||||
// 将顶点投影值映射到 [0, 1] 范围,考虑到有效渐变长度
|
||||
// 此时 t 可能超出 [0,1],例如,当 projection - minProjection > effectiveGradientLength 时,
|
||||
// 这通常发生在 range > 0 的情况下,超出的部分会被钳制,使其颜色保持在 color1。
|
||||
t = (projection - minProjection) / effectiveGradientLength;
|
||||
t = Mathf.Clamp01(t); // 钳制以确保插值因子在有效范围内
|
||||
}
|
||||
|
||||
// 根据 isFlip 调整插值因子
|
||||
// 如果 isFlip 为 true,则 color1 变为渐变起点,color2 变为渐变终点
|
||||
// 例如,t=0变为t=1,t=1变为t=0。
|
||||
if (isFlip)
|
||||
{
|
||||
t = 1f - t;
|
||||
}
|
||||
|
||||
// 使用Lerp进行颜色插值
|
||||
// Color32.Lerp(a, b, t) 表示当 t=0 时取 a,t=1 时取 b。
|
||||
// 此时,t=0 对应 minProjection 处,t=1 对应 maxProjection 处 (考虑effectiveGradientLength和isFlip)。
|
||||
vertex.color = Color32.Lerp(color2, color1, t);
|
||||
vh.SetUIVertex(vertex, i); // 更新网格中的顶点
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将角度转换为归一化的Vector2方向。
|
||||
/// 0度:(1, 0) - 右
|
||||
/// 90度:(0, 1) - 上
|
||||
/// 180度:(-1, 0) - 左
|
||||
/// 270度:(0, -1) - 下
|
||||
/// </summary>
|
||||
/// <param name="angleDegrees">角度,单位为度。</param>
|
||||
/// <returns>归一化的Vector2方向。</returns>
|
||||
private Vector2 AngleToVector2(float angleDegrees)
|
||||
{
|
||||
// 将角度转换为弧度
|
||||
float angleRadians = angleDegrees * Mathf.Deg2Rad;
|
||||
// 计算X和Y分量
|
||||
return new Vector2(Mathf.Cos(angleRadians), Mathf.Sin(angleRadians));
|
||||
}
|
||||
}
|
||||
}
|
2
Client/Assets/Scripts/UI/ColorBar.cs.meta
Normal file
2
Client/Assets/Scripts/UI/ColorBar.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8df3d16a358d74644b86e92ca5177fa1
|
111
Client/Assets/Scripts/UI/SkillNodeEnterLineUI.cs
Normal file
111
Client/Assets/Scripts/UI/SkillNodeEnterLineUI.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class SkillNodeEnterLineUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private SkillNodeLinkLineUI skillNodeLinkLineUIPrefab;
|
||||
|
||||
private const float MIN_VERTICAL_SPACING = 12.9447f; // 每条连接线的最小垂直间距
|
||||
private const float UPWARD_OFFSET_FROM_BASE = 0f; // 整体结束点相对于参照点的向上偏移量
|
||||
|
||||
/// <summary>
|
||||
/// 初始化连接线。根据传入的起始点数组创建多条连接线,
|
||||
/// 这些线的结束点将围绕 transform.position - new Vector3(6,0) 并在其基础上向上偏移5,
|
||||
/// 纵向均匀排列,至少间距12。
|
||||
/// 在每次调用前,会清空所有旧的连接线以避免累积。
|
||||
/// </summary>
|
||||
/// <param name="startPoints">要连接的线起始点数组。</param>
|
||||
/// <returns>所有连接线的结束点所占据的最小总高度。</returns>
|
||||
public float Init(Vector2[] startPoints)
|
||||
{
|
||||
// 清空所有旧的连接线,避免重复调用时线累积。
|
||||
// 从后往前遍历子对象,因为销毁子对象会改变子对象列表的索引。
|
||||
for (var i = transform.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
// 获取当前子对象。
|
||||
var child = transform.GetChild(i);
|
||||
// 仅销毁SkillNodeLinkLineUI类型的子对象实例,避免意外删除其他非连接线的子对象。
|
||||
if (child != null && child.GetComponent<SkillNodeLinkLineUI>() != null)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
// 预制体加载检查:确保在执行任何操作之前,预制体已经加载。
|
||||
// 优先使用Inspector赋值,如果 skillNodeLinkLineUIPrefab 为 null,则尝试从 Resources 文件夹加载作为备用方案。
|
||||
if (skillNodeLinkLineUIPrefab == null)
|
||||
{
|
||||
// 从Resources加载预制体。
|
||||
skillNodeLinkLineUIPrefab = Resources.Load<SkillNodeLinkLineUI>("Prefab/SkillTree/linkLine");
|
||||
if (skillNodeLinkLineUIPrefab == null)
|
||||
{
|
||||
// 打印错误日志(已本地化为中文)。
|
||||
Debug.LogError(
|
||||
"初始化失败:SkillNodeLinkLineUI 预制体未在 Prefab/SkillTree/linkLine 路径找到。请检查路径,确保预制体存在,或直接在 Inspector 中赋值。");
|
||||
return 0f; // 预制体缺失,返回0高度。
|
||||
}
|
||||
}
|
||||
|
||||
// 获取起始点的数量。
|
||||
var numLines = startPoints.Length;
|
||||
if (numLines == 0)
|
||||
{
|
||||
// 没有起始点,不需要绘制连接线,返回0高度。
|
||||
// 此时,清理旧连接线的逻辑已经执行,确保了没有残留的线。
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// 计算所有连接线共享的基准结束点X坐标。
|
||||
// 这是所有连接线结束点集合的垂直居中轴线。
|
||||
var commonEndPointX = transform.position.x - 6f;
|
||||
// 计算所有连接线结束点集合的实际垂直中心Y坐标。
|
||||
// 根据需求 "有多个点时向上偏移5",这个偏移量应用于整个结束点集合的中心。
|
||||
var effectiveCenterY = transform.position.y + UPWARD_OFFSET_FROM_BASE;
|
||||
// 初始化所有线结束点所占据的总垂直跨度。
|
||||
var requiredHeight = 0f;
|
||||
|
||||
if (numLines == 1)
|
||||
{
|
||||
// 只有一个起始点时,其结束点直接放在计算出的有效中心Y坐标上。
|
||||
var endPoint = new Vector2(commonEndPointX, effectiveCenterY);
|
||||
|
||||
// 实例化连接线预制体并设置为当前对象的子对象。
|
||||
var line = Instantiate(skillNodeLinkLineUIPrefab, transform);
|
||||
// 设置连接线的起始点和结束点。
|
||||
line.SetConnectionPoints(startPoints[0], endPoint);
|
||||
|
||||
// 只有一个点时,垂直高度可以认为是0,因为它不形成一个跨越多个点的“总高度”。
|
||||
// 这里返回0f符合“所有连接线的结束点所占据的最小总高度”的定义语义。
|
||||
requiredHeight = 0f;
|
||||
}
|
||||
else // numLines > 1
|
||||
{
|
||||
// 计算所有连接线结束点所占据的总垂直跨度(从最顶端到最底端)。
|
||||
// 这是从最上面一条线的结束点Y到最下面一条线的结束点Y的距离。
|
||||
requiredHeight = (numLines - 1) * MIN_VERTICAL_SPACING;
|
||||
// 计算最上面一条线的结束点Y坐标。
|
||||
// effectiveCenterY 是所有线的垂直中心,从这个中心向上偏移 requiredHeight / 2 即可得到最顶端的Y。
|
||||
var currentEndPointY = effectiveCenterY + (requiredHeight / 2f);
|
||||
|
||||
// 遍历起始点数组,创建并设置每条连接线。
|
||||
for (var i = 0; i < numLines; i++)
|
||||
{
|
||||
// 为当前连接线确定结束点坐标。
|
||||
var endPoint = new Vector2(commonEndPointX, currentEndPointY);
|
||||
|
||||
// 实例化连接线预制体。
|
||||
var line = Instantiate(skillNodeLinkLineUIPrefab, transform);
|
||||
// 设置连接线的起始点和结束点。
|
||||
line.SetConnectionPoints(startPoints[i], endPoint);
|
||||
|
||||
// 为下一条线计算其结束点Y坐标(向下移动一个最小间距)。
|
||||
currentEndPointY -= MIN_VERTICAL_SPACING;
|
||||
}
|
||||
}
|
||||
|
||||
// 返回所有连接线的结束点所占据的最小总高度。
|
||||
return requiredHeight;
|
||||
}
|
||||
}
|
||||
}
|
3
Client/Assets/Scripts/UI/SkillNodeEnterLineUI.cs.meta
Normal file
3
Client/Assets/Scripts/UI/SkillNodeEnterLineUI.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d15100cc2164e78bfaaef16efd7825a
|
||||
timeCreated: 1757469894
|
121
Client/Assets/Scripts/UI/SkillNodeLinkLineUI.cs
Normal file
121
Client/Assets/Scripts/UI/SkillNodeLinkLineUI.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 技能节点连接线UI组件。
|
||||
/// 该组件用于管理贝塞尔曲线的生成和显示,连接两个技能节点或其他UI元素。
|
||||
/// 它通过控制贝塞尔曲线生成器的参数,实现UI元素之间的视觉连接。
|
||||
/// </summary>
|
||||
// 确保当前GameObject拥有RectTransform组件,因为这是一个UI组件,其布局和定位依赖于它。
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class SkillNodeLinkLineUI : MonoBehaviour
|
||||
{
|
||||
// 引用UILineRenderer组件,用于实际绘制曲线。
|
||||
public UILineRenderer line;
|
||||
// 引用UIBezierCurveGenerator组件,负责根据控制点计算贝塞尔曲线的几何点。
|
||||
public UIBezierCurveGenerator curveGenerator;
|
||||
// 曲线起始点关联的UI元素,例如:箭头的头部或起始连接点标识。
|
||||
public GameObject lineHead;
|
||||
// 曲线结束点关联的UI元素,例如:箭头的尾部或结束连接点标识。
|
||||
public GameObject lineTail;
|
||||
|
||||
// 当前GameObject的RectTransform组件,方便在代码中直接访问和操作。
|
||||
// [HideInInspector] 确保该字段在Inspector面板中不可见,通常因为它会被自动赋值。
|
||||
[HideInInspector] public RectTransform rectTransform;
|
||||
|
||||
/// <summary>
|
||||
/// 当脚本实例被启用时,或者首次加载时调用。
|
||||
/// 用于初始化和检查必要的组件。
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
// 获取当前GameObject的RectTransform组件。
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
// 检查贝塞尔曲线生成器是否已赋值。
|
||||
if (curveGenerator == null)
|
||||
{
|
||||
Debug.LogError("UIBezierCurveGenerator未赋值给SkillNodeLinkLineUI!", this);
|
||||
}
|
||||
// 检查UILineRenderer是否已赋值。
|
||||
// 注意:UILineRenderer通常由UIBezierCurveGenerator管理,但此处也进行一个警告检查。
|
||||
if (line == null)
|
||||
{
|
||||
Debug.LogWarning("UILineRenderer未赋值给SkillNodeLinkLineUI。它可能由UIBezierCurveGenerator自动处理,或需要手动赋值。", this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置曲线的起始点和结束点,并更新贝塞尔曲线的生成以及线头线尾UI元素的位置。
|
||||
/// </summary>
|
||||
/// <param name="startWorldPos">曲线起始点的世界坐标。</param>
|
||||
/// <param name="endWorldPos">曲线结束点的世界坐标。</param>
|
||||
public void SetConnectionPoints(Vector2 startWorldPos, Vector2 endWorldPos)
|
||||
{
|
||||
// 如果贝塞尔曲线生成器未赋值,则无法设置连接点。
|
||||
if (curveGenerator == null)
|
||||
{
|
||||
Debug.LogError("无法设置连接点:UIBezierCurveGenerator未赋值。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 将世界坐标转换为 UIBezierCurveGenerator 的局部坐标。
|
||||
// 贝塞尔曲线生成器的控制点是相对于其自身RectTransform的局部坐标。
|
||||
// 因此,我们需要先获取curveGenerator所在GameObject的RectTransform,并进行坐标转换。
|
||||
var curveGeneratorRectTransform = curveGenerator.GetComponent<RectTransform>();
|
||||
if (curveGeneratorRectTransform == null)
|
||||
{
|
||||
Debug.LogError("UIBezierCurveGenerator所在的GameObject没有RectTransform组件。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 将世界坐标转换为curveGeneratorRectTransform的局部坐标。
|
||||
Vector2 localStartPos = curveGeneratorRectTransform.InverseTransformPoint(startWorldPos);
|
||||
Vector2 localEndPos = curveGeneratorRectTransform.InverseTransformPoint(endWorldPos);
|
||||
|
||||
// 2. 计算贝塞尔曲线的水平偏移量,用于控制曲线的弯曲程度。
|
||||
var horizontalOffset = 100f; // 默认水平偏移量。
|
||||
|
||||
// 根据起始点和结束点X坐标的绝对差值来调整偏移量,使曲线在近距离时更平滑。
|
||||
var xDiff = Mathf.Abs(startWorldPos.x - endWorldPos.x); // 使用世界坐标计算X轴差值更直观。
|
||||
if (xDiff < 200f)
|
||||
{
|
||||
horizontalOffset = xDiff / 2f + 10f; // 在X轴差值较小时,减小偏移量。
|
||||
}
|
||||
|
||||
// 3. 计算贝塞尔曲线的四个控制点 (P0, P1, P2, P3)。
|
||||
var p0 = localStartPos; // 贝塞尔曲线的起始点。
|
||||
var p3 = localEndPos; // 贝塞尔曲线的终止点。
|
||||
Vector2 p1, p2; // 贝塞尔曲线的两个控制点。
|
||||
|
||||
// 根据起始点和结束点X坐标的相对位置,确定控制点P1和P2的水平偏移方向،
|
||||
// 以保证曲线的自然走向。
|
||||
if (localStartPos.x <= localEndPos.x)
|
||||
{
|
||||
// 如果曲线从左向右(或垂直),P1在P0右侧,P2在P3左侧。
|
||||
p1 = new Vector2(localStartPos.x + horizontalOffset, localStartPos.y);
|
||||
p2 = new Vector2(localEndPos.x - horizontalOffset, localEndPos.y);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果曲线从右向左,P1在P0左侧,P2在P3右侧。
|
||||
p1 = new Vector2(localStartPos.x - horizontalOffset, localStartPos.y);
|
||||
p2 = new Vector2(localEndPos.x + horizontalOffset, localEndPos.y);
|
||||
}
|
||||
|
||||
// 4. 将计算出的控制点设置给 UIBezierCurveGenerator,使其生成新的曲线。
|
||||
curveGenerator.SetControlPoints(p0, p1, p2, p3);
|
||||
|
||||
// 5. 将线头和线尾UI元素移动到对应的世界坐标位置,使其与曲线的起始和结束对齐。
|
||||
if (lineHead != null)
|
||||
{
|
||||
lineHead.transform.position = startWorldPos; // 将线头UI元素移动到起始点的世界坐标。
|
||||
}
|
||||
if (lineTail != null)
|
||||
{
|
||||
lineTail.transform.position = endWorldPos; // 将线尾UI元素移动到结束点的世界坐标。
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
Client/Assets/Scripts/UI/SkillNodeLinkLineUI.cs.meta
Normal file
3
Client/Assets/Scripts/UI/SkillNodeLinkLineUI.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04b3f5cbad874b31893c0d7e1de082b9
|
||||
timeCreated: 1757470314
|
196
Client/Assets/Scripts/UI/SkillTreeNodeUI.cs
Normal file
196
Client/Assets/Scripts/UI/SkillTreeNodeUI.cs
Normal file
@ -0,0 +1,196 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditor; // 引入 UnityEditor 命名空间
|
||||
|
||||
namespace UI
|
||||
{
|
||||
[ExecuteInEditMode] // 允许脚本在编辑器模式下运行
|
||||
public class SkillTreeNodeUI : MonoBehaviour
|
||||
{
|
||||
public SkillNodeEnterLineUI skillNodeEnterLineUI;
|
||||
|
||||
[Header("Editor Test Settings")]
|
||||
public bool testModeActive = true; // 是否在编辑器模式下激活测试线的绘制
|
||||
[Range(0, 20)] // 限制线条数量在0到20之间,防止意外生成过多
|
||||
public int numberOfLinesToTest = 5; // 要生成多少条测试线
|
||||
public Vector2 initialStartPointOffset = new Vector2(-300, 100); // 初始点的偏移
|
||||
public Vector2 perLineOffset = new Vector2(0, -30); // 每条线之间的相对偏移
|
||||
|
||||
[Header("UI Node Settings")]
|
||||
public float minNodeHeight = 70f; // UI节点的最小高度,即使没有线条也应保持此高度
|
||||
|
||||
private RectTransform _rectTransform; // 缓存自身的 RectTransform 组件
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
if (_rectTransform == null)
|
||||
{
|
||||
Debug.LogError("SkillTreeNodeUI requires a RectTransform component on this GameObject.", this);
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 确保 _rectTransform 在 Play Mode 中也可用
|
||||
if (_rectTransform == null)
|
||||
{
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
// 在Play Mode启动时执行初始化。对于运行时,testModeActive(作为一个“编辑器测试设置”)通常不应阻止线条绘制。
|
||||
// 因此,isEditorCall 参数传递 false。
|
||||
InitializeLines(false);
|
||||
}
|
||||
|
||||
// 当Inspector中的值发生改变或脚本被加载时调用
|
||||
private void OnValidate()
|
||||
{
|
||||
// 确保只在编辑器模式且非运行时执行
|
||||
if (!Application.isEditor || Application.isPlaying)
|
||||
return;
|
||||
|
||||
// 调用初始化方法。对于编辑器调用,isEditorCall 参数传递 true,这将使 testModeActive 生效。
|
||||
InitializeLines(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前设置初始化线条绘制。
|
||||
/// 可以从外部调用以刷新线条。
|
||||
/// </summary>
|
||||
/// <param name="isEditorCall">如果为 true,表示是从编辑器上下文(如 OnValidate)调用,将尊重 testModeActive 设置。</param>
|
||||
public void InitializeLines(bool isEditorCall)
|
||||
{
|
||||
if (_rectTransform == null)
|
||||
{
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
if (_rectTransform == null)
|
||||
{
|
||||
Debug.LogError("SkillTreeNodeUI requires a RectTransform component for initialization.", this);
|
||||
ClearExistingLines(); // 即使缺少 RectTransform,也尝试清理避免残留
|
||||
SetRectTransformHeight(0f); // 尝试重置高度
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (skillNodeEnterLineUI == null)
|
||||
{
|
||||
Debug.LogWarning("SkillNodeEnterLineUI reference is not set for initialization on " + gameObject.name + ". Cannot draw lines.", this);
|
||||
ClearExistingLines();
|
||||
SetRectTransformHeight(0f);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearExistingLines(); // 始终在绘制新线前清理旧线
|
||||
|
||||
// 如果是编辑器调用且测试模式未激活,或者要绘制的线数为0,则不绘制新线并设置最小高度。
|
||||
// 当 isEditorCall 为 false (运行时调用) 时,会忽略 testModeActive 的状态。
|
||||
if ((isEditorCall && !testModeActive) || numberOfLinesToTest <= 0)
|
||||
{
|
||||
SetRectTransformHeight(0f);
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据 Inspector 中的参数生成 startPoints
|
||||
var startPoints = new List<Vector2>();
|
||||
// 注意:transform.position 是世界坐标。对于UI,通常建议使用 RectTransform.anchoredPosition
|
||||
// 或转换为 RectTransformUtility.ScreenPointToLocalPointInRectangle 的方式获取相对 Canvas 的本地坐标。
|
||||
// 但为保持与原有逻辑一致性,且假设 skillNodeEnterLineUI 内部能正确处理世界坐标,暂不修改。
|
||||
var currentPoint = (Vector2)transform.position + initialStartPointOffset;
|
||||
startPoints.Add(currentPoint);
|
||||
|
||||
for (var i = 1; i < numberOfLinesToTest; i++)
|
||||
{
|
||||
currentPoint += perLineOffset; // 使用相对偏移
|
||||
startPoints.Add(currentPoint);
|
||||
}
|
||||
|
||||
// 调用 Init 来创建线并获取所需的高度
|
||||
var requiredLineHeight = skillNodeEnterLineUI.Init(startPoints.ToArray());
|
||||
|
||||
// 立即设置 RectTransform 高度
|
||||
SetRectTransformHeight(requiredLineHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据所需行高设置自身的RectTransform高度。
|
||||
/// 高度为 Max(requiredLineHeight + 40, minNodeHeight)。
|
||||
/// </summary>
|
||||
/// <param name="requiredLineHeight">SkillNodeEnterLineUI.Init方法返回的作为线终点区域的高度。</param>
|
||||
private void SetRectTransformHeight(float requiredLineHeight)
|
||||
{
|
||||
if (_rectTransform == null)
|
||||
{
|
||||
Debug.LogWarning("RectTransform is null, cannot set height for " + transform.name + ".", this);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentWidth = _rectTransform.sizeDelta.x; // 保持宽度不变
|
||||
var currentHeight = _rectTransform.sizeDelta.y;
|
||||
|
||||
// 计算线条所需的最小高度,加上40的额外填充
|
||||
var minNeededHeightWithPadding = requiredLineHeight + 40f;
|
||||
|
||||
// 目标高度为:线条所需的最小高度与 RectTransform 当前高度中的较大值,同时不小于 minNodeHeight。
|
||||
// 这确保了RectTransform的高度只在需要时增加,且不会低于其当前的尺寸或设定的最小高度。
|
||||
var targetHeight = Mathf.Max(minNeededHeightWithPadding, minNodeHeight);
|
||||
|
||||
// 只有当计算出的目标高度与当前高度存在显著差异时才进行更新,以避免不必要的UI刷新和场景脏化。
|
||||
// 使用一个小小的epsilon值进行浮点数比较,以应对精度问题。
|
||||
if (Mathf.Abs(currentHeight - targetHeight) > 0.1f)
|
||||
{
|
||||
_rectTransform.sizeDelta = new Vector2(currentWidth, targetHeight);
|
||||
|
||||
// 在编辑器模式下,标记RectTransform为脏(dirty),确保修改被保存到场景中。
|
||||
// 仅当非Play Mode才需要SetDirty,且只对改变的组件。
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
EditorUtility.SetDirty(_rectTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除 skillNodeEnterLineUI 下所有已存在的子对象(连接线)。
|
||||
/// 在编辑器模式下使用 EditorApplication.delayCall 延迟销毁,避免 OnValidate 的限制。
|
||||
/// 在Play Mode下使用 Destroy。
|
||||
/// </summary>
|
||||
private void ClearExistingLines()
|
||||
{
|
||||
if (skillNodeEnterLineUI == null) return;
|
||||
|
||||
var childrenToDestroy = new List<GameObject>();
|
||||
for (var i = skillNodeEnterLineUI.transform.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
childrenToDestroy.Add(skillNodeEnterLineUI.transform.GetChild(i).gameObject);
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
foreach (var child in childrenToDestroy)
|
||||
{
|
||||
Destroy(child);
|
||||
}
|
||||
}
|
||||
else // 在编辑器模式下
|
||||
{
|
||||
EditorApplication.delayCall += () => {
|
||||
bool childrenWereDestroyed = false; // 增加一个标志,判断是否有子对象被实际销毁
|
||||
foreach (var child in childrenToDestroy)
|
||||
{
|
||||
if (child != null)
|
||||
{
|
||||
DestroyImmediate(child);
|
||||
childrenWereDestroyed = true;
|
||||
}
|
||||
}
|
||||
// 如果有子对象被销毁且 skillNodeEnterLineUI 仍然存在,则标记其为脏
|
||||
if (childrenWereDestroyed && skillNodeEnterLineUI != null)
|
||||
{
|
||||
EditorUtility.SetDirty(skillNodeEnterLineUI); // 标记 skillNodeEnterLineUI 组件为脏
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
Client/Assets/Scripts/UI/SkillTreeNodeUI.cs.meta
Normal file
3
Client/Assets/Scripts/UI/SkillTreeNodeUI.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0bd6744a131480d872dc07f318b561d
|
||||
timeCreated: 1757469859
|
@ -2,147 +2,174 @@ using UnityEngine;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
// 需要引用 UILineRenderer 组件
|
||||
/// <summary>
|
||||
/// UI贝塞尔曲线生成器。
|
||||
/// 此组件用于生成和显示三次贝塞尔曲线,并将其点数据传递给UILineRenderer进行绘制。
|
||||
/// </summary>
|
||||
// 确保当前GameObject上存在UILineRenderer组件,如果不存在则会自动添加。
|
||||
[RequireComponent(typeof(UILineRenderer))]
|
||||
// 允许在编辑器模式下实时更新,便于调试
|
||||
// 允许在编辑器模式下,当参数修改时实时更新曲线,便于调试和预览。
|
||||
[ExecuteAlways]
|
||||
public class UIBezierCurveGenerator : MonoBehaviour
|
||||
{
|
||||
// UILineRenderer 组件的引用
|
||||
// 对UILineRenderer组件的引用,用于绘制生成的贝塞尔曲线。
|
||||
[SerializeField] public UILineRenderer lineRenderer;
|
||||
|
||||
// 控制点
|
||||
[Header("Bezier Control Points")] public Vector2 P0; // 起点
|
||||
public Vector2 P1; // 第一个控制点
|
||||
public Vector2 P2; // 第二个控制点
|
||||
public Vector2 P3; // 终点
|
||||
// 贝塞尔曲线的四个控制点。
|
||||
[Header("贝塞尔控制点")]
|
||||
public Vector2 P0; // 曲线的起始点。
|
||||
public Vector2 P1; // 曲线的第一个控制点,影响曲线从P0开始的方向和曲率。
|
||||
public Vector2 P2; // 曲线的第二个控制点,影响曲线在接近P3时的方向和曲率。
|
||||
public Vector2 P3; // 曲线的终止点。
|
||||
|
||||
[Header("Curve Settings")] [Range(5, 200)] // 限制段数范围,防止过低导致不平滑,过高导致性能问题
|
||||
public int segmentCount = 50; // 曲线的平滑度,即采样的线段数量
|
||||
// 曲线的设置参数。
|
||||
[Header("曲线设置")]
|
||||
[Range(5, 200)] // 限制曲线段数的范围,确保曲线平滑度和性能之间的平衡。
|
||||
public int segmentCount = 50; // 用于近似曲线的线段数量,值越大曲线越平滑。
|
||||
|
||||
/// <summary>
|
||||
/// 当脚本实例被启用时,或者首次加载时调用。
|
||||
/// </summary>
|
||||
void Awake()
|
||||
{
|
||||
// 初始化组件,获取UILineRenderer的引用。
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当在编辑器中修改脚本的属性时调用。
|
||||
/// </summary>
|
||||
void OnValidate()
|
||||
{
|
||||
// 初始化组件,获取UILineRenderer的引用。
|
||||
Initialize();
|
||||
// 如果UILineRenderer组件有效,则在编辑器中实时重新生成曲线。
|
||||
if (lineRenderer != null)
|
||||
{
|
||||
GenerateCurvePoints(); // 在编辑器中修改参数时实时更新曲线
|
||||
GenerateCurvePoints();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化组件,获取 UILineRenderer 引用
|
||||
/// 初始化组件,获取 UILineRenderer 引用。
|
||||
/// </summary>
|
||||
private void Initialize()
|
||||
{
|
||||
// 如果UILineRenderer引用为空,则尝试获取组件。
|
||||
if (lineRenderer == null)
|
||||
{
|
||||
lineRenderer = GetComponent<UILineRenderer>();
|
||||
// 如果仍然无法获取UILineRenderer组件,则报错并禁用此组件。
|
||||
if (lineRenderer == null)
|
||||
{
|
||||
Debug.LogError("UILineRenderer component not found on this GameObject. Please add one.", this);
|
||||
enabled = false; // 禁用此组件,防止空引用错误
|
||||
Debug.LogError("UILineRenderer组件未在此GameObject上找到,请添加一个。", this);
|
||||
enabled = false; // 禁用此组件实例,以防止后续空引用错误。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算三次贝塞尔曲线上的点
|
||||
/// B(t) = (1-t)^3 * P0 + 3 * (1-t)^2 * t * P1 + 3 * (1-t) * t^2 * P2 + t^3 * P3
|
||||
/// 计算三次贝塞尔曲线上的特定点。
|
||||
/// 贝塞尔曲线公式: B(t) = (1-t)^3 * P0 + 3 * (1-t)^2 * t * P1 + 3 * (1-t) * t^2 * P2 + t^3 * P3
|
||||
/// </summary>
|
||||
/// <param name="t">参数,范围 [0, 1]</param>
|
||||
/// <param name="p0">起点</param>
|
||||
/// <param name="p1">第一个控制点</param>
|
||||
/// <param name="p2">第二个控制点</param>
|
||||
/// <param name="p3">终点</param>
|
||||
/// <returns>在参数 t 处的贝塞尔曲线上点</returns>
|
||||
/// <param name="t">参数,表示曲线上的位置,范围 [0, 1]。</param>
|
||||
/// <param name="p0">曲线的起始点。</param>
|
||||
/// <param name="p1">曲线的第一个控制点。</param>
|
||||
/// <param name="p2">曲线的第二个控制点。</param>
|
||||
/// <param name="p3">曲线的终止点。</param>
|
||||
/// <returns>在参数 t 处的贝塞尔曲线上点的二维坐标。</returns>
|
||||
private Vector2 CalculateBezierPoint(float t, Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3)
|
||||
{
|
||||
float u = 1 - t;
|
||||
float tt = t * t;
|
||||
float uu = u * u;
|
||||
float uuu = uu * u;
|
||||
float ttt = tt * t;
|
||||
var u = 1 - t; // 计算 (1-t)
|
||||
var tt = t * t; // 计算 t的平方
|
||||
var uu = u * u; // 计算 (1-t)的平方
|
||||
var uuu = uu * u; // 计算 (1-t)的立方
|
||||
var ttt = tt * t; // 计算 t的立方
|
||||
|
||||
Vector2 p = uuu * p0; // (1-t)^3 * P0
|
||||
p += 3 * uu * t * p1; // 3 * (1-t)^2 * t * P1
|
||||
p += 3 * u * tt * p2; // 3 * (1-t) * t^2 * P2
|
||||
p += ttt * p3; // t^3 * P3
|
||||
var p = uuu * p0; // 计算 (1-t)^3 * P0
|
||||
p += 3 * uu * t * p1; // 计算 3 * (1-t)^2 * t * P1
|
||||
p += 3 * u * tt * p2; // 计算 3 * (1-t) * t^2 * P2
|
||||
p += ttt * p3; // 计算 t^3 * P3
|
||||
|
||||
return p;
|
||||
return p; // 返回计算出的贝塞尔曲线上点。
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成贝塞尔曲线的点并更新 UILineRenderer
|
||||
/// 根据当前的控制点和段数生成贝塞尔曲线上的所有点,并更新 UILineRenderer 进行绘制。
|
||||
/// </summary>
|
||||
public void GenerateCurvePoints()
|
||||
{
|
||||
if (lineRenderer == null || segmentCount <= 0)
|
||||
// 如果UILineRenderer组件无效,则无法生成曲线。
|
||||
if (!lineRenderer)
|
||||
{
|
||||
Debug.LogWarning("UILineRenderer is null or segmentCount is invalid. Cannot generate curve.", this);
|
||||
Debug.LogWarning("UILineRenderer组件为空,无法生成曲线。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空 UILineRenderer 的点列表,以便重新填充
|
||||
// 清空 UILineRenderer 当前的点列表,为填充新的曲线点做准备。
|
||||
lineRenderer.points.Clear();
|
||||
|
||||
for (int i = 0; i <= segmentCount; i++)
|
||||
// 遍历并计算曲线上的所有采样点。
|
||||
for (var i = 0; i <= segmentCount; i++)
|
||||
{
|
||||
float t = i / (float)segmentCount;
|
||||
Vector2 point = CalculateBezierPoint(t, P0, P1, P2, P3);
|
||||
lineRenderer.points.Add(point);
|
||||
var t = i / (float)segmentCount; // 计算当前点的归一化参数 [0, 1]。
|
||||
var point = CalculateBezierPoint(t, P0, P1, P2, P3); // 根据参数 t 计算贝塞尔曲线上点坐标。
|
||||
lineRenderer.points.Add(point); // 将计算出的点添加到UILineRenderer的点列表中。
|
||||
}
|
||||
|
||||
// 通知 UILineRenderer 需要重新绘制其几何体
|
||||
// 通知 UILineRenderer 需要重新绘制其几何体,以显示更新后的曲线。
|
||||
lineRenderer.SetAllDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供了通过代码设置控制点并刷新曲线的方法
|
||||
/// 提供了通过代码设置贝塞尔曲线控制点的方法,并立即刷新曲线。
|
||||
/// </summary>
|
||||
/// <param name="p0">新的起始点。</param>
|
||||
/// <param name="p1">新的第一个控制点。</param>
|
||||
/// <param name="p2">新的第二个控制点。</param>
|
||||
/// <param name="p3">新的终止点。</param>
|
||||
public void SetControlPoints(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3)
|
||||
{
|
||||
this.P0 = p0;
|
||||
this.P1 = p1;
|
||||
this.P2 = p2;
|
||||
this.P3 = p3;
|
||||
GenerateCurvePoints();
|
||||
this.P0 = p0; // 设置起始点。
|
||||
this.P1 = p1; // 设置第一个控制点。
|
||||
this.P2 = p2; // 设置第二个控制点。
|
||||
this.P3 = p3; // 设置终止点。
|
||||
GenerateCurvePoints(); // 更新曲线。
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供了通过代码设置曲线段数并刷新曲线的方法
|
||||
/// 提供了通过代码设置贝塞尔曲线段数的方法,并立即刷新曲线。
|
||||
/// </summary>
|
||||
/// <param name="count">新的曲线段数。</param>
|
||||
public void SetSegmentCount(int count)
|
||||
{
|
||||
segmentCount = Mathf.Max(5, count); // 至少保留5段,保证一定平滑度
|
||||
GenerateCurvePoints();
|
||||
segmentCount = Mathf.Max(5, count); // 设置曲线段数,确保至少为5段以保持一定平滑度。
|
||||
GenerateCurvePoints(); // 更新曲线。
|
||||
}
|
||||
|
||||
// 当组件首次添加或重置时调用
|
||||
void Reset()
|
||||
/// <summary>
|
||||
/// 当组件首次添加到GameObject或在编辑器中选择“Reset”时调用。
|
||||
/// 用于设置组件的默认值。
|
||||
/// </summary>
|
||||
private void Reset()
|
||||
{
|
||||
Initialize(); // 确保 lineRenderer 已初始化
|
||||
Initialize(); // 确保UILineRenderer引用已初始化。
|
||||
|
||||
// 根据 RectTransform 的大小设置默认点,确保在可见范围内
|
||||
RectTransform rt = GetComponent<RectTransform>();
|
||||
float halfWidth = rt.rect.width / 2f;
|
||||
float halfHeight = rt.rect.height / 2f;
|
||||
// 获取当前GameObject的RectTransform,以便根据其尺寸设置默认控制点。
|
||||
var rt = GetComponent<RectTransform>();
|
||||
var halfWidth = rt.rect.width / 2f; // 获取RectTransform宽度的一半。
|
||||
var halfHeight = rt.rect.height / 2f; // 获取RectTransform高度的一半。
|
||||
|
||||
// 设置一些默认的控制点,让曲线在UI区域内可见
|
||||
// 这些是相对于 RectTransform 的局部坐标
|
||||
P0 = new Vector2(-halfWidth * 0.8f, -halfHeight * 0.5f); // 左下
|
||||
P1 = new Vector2(-halfWidth * 0.4f, halfHeight * 0.8f); // 左上控制点
|
||||
P2 = new Vector2(halfWidth * 0.4f, halfHeight * 0.8f); // 右上控制点
|
||||
P3 = new Vector2(halfWidth * 0.8f, -halfHeight * 0.5f); // 右下
|
||||
// 设置一组默认的控制点,使得曲线在UI区域内可见且具有S形或拱形。
|
||||
// 这些点是相对于RectTransform的局部坐标。
|
||||
P0 = new Vector2(-halfWidth * 0.8f, -halfHeight * 0.5f); // 默认起始点,位于左下。
|
||||
P1 = new Vector2(-halfWidth * 0.4f, halfHeight * 0.8f); // 默认第一个控制点,位于左上。
|
||||
P2 = new Vector2(halfWidth * 0.4f, halfHeight * 0.8f); // 默认第二个控制点,位于右上。
|
||||
P3 = new Vector2(halfWidth * 0.8f, -halfHeight * 0.5f); // 默认终止点,位于右下。
|
||||
|
||||
segmentCount = 50;
|
||||
segmentCount = 50; // 设置默认的曲线段数。
|
||||
|
||||
GenerateCurvePoints(); // 立即生成曲线
|
||||
GenerateCurvePoints(); // 立即根据默认设置生成并显示曲线。
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user