(client) feat:技能树节点及其素材

This commit is contained in:
m0_75251201
2025-09-11 11:19:34 +08:00
parent d1c0387df0
commit bb691f9622
79 changed files with 7904 additions and 364 deletions

View 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=1t=1变为t=0。
if (isFlip)
{
t = 1f - t;
}
// 使用Lerp进行颜色插值
// Color32.Lerp(a, b, t) 表示当 t=0 时取 at=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));
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8df3d16a358d74644b86e92ca5177fa1

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4d15100cc2164e78bfaaef16efd7825a
timeCreated: 1757469894

View 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元素移动到结束点的世界坐标。
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 04b3f5cbad874b31893c0d7e1de082b9
timeCreated: 1757470314

View 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 组件为脏
}
};
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a0bd6744a131480d872dc07f318b561d
timeCreated: 1757469859

View File

@ -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(); // 立即根据默认设置生成并显示曲线
}
}
}
}