Files
Gen_Hack-and-Slash-Roguelit…/Client/Assets/Scripts/UI/SkillTreeNodeUI.cs
2025-09-11 11:19:34 +08:00

197 lines
8.8 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 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 组件为脏
}
};
}
}
}
}