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(); 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(); } // 在Play Mode启动时执行初始化。对于运行时,testModeActive(作为一个“编辑器测试设置”)通常不应阻止线条绘制。 // 因此,isEditorCall 参数传递 false。 InitializeLines(false); } // 当Inspector中的值发生改变或脚本被加载时调用 private void OnValidate() { // 确保只在编辑器模式且非运行时执行 if (!Application.isEditor || Application.isPlaying) return; // 调用初始化方法。对于编辑器调用,isEditorCall 参数传递 true,这将使 testModeActive 生效。 InitializeLines(true); } /// /// 根据当前设置初始化线条绘制。 /// 可以从外部调用以刷新线条。 /// /// 如果为 true,表示是从编辑器上下文(如 OnValidate)调用,将尊重 testModeActive 设置。 public void InitializeLines(bool isEditorCall) { if (_rectTransform == null) { _rectTransform = GetComponent(); 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(); // 注意: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); } /// /// 根据所需行高设置自身的RectTransform高度。 /// 高度为 Max(requiredLineHeight + 40, minNodeHeight)。 /// /// SkillNodeEnterLineUI.Init方法返回的作为线终点区域的高度。 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); } } } /// /// 清除 skillNodeEnterLineUI 下所有已存在的子对象(连接线)。 /// 在编辑器模式下使用 EditorApplication.delayCall 延迟销毁,避免 OnValidate 的限制。 /// 在Play Mode下使用 Destroy。 /// private void ClearExistingLines() { if (skillNodeEnterLineUI == null) return; var childrenToDestroy = new List(); 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 组件为脏 } }; } } } }