using UnityEngine; using UnityEngine.UI.Extensions; namespace Ichni.Story.UI { /// /// 故事树中两个 block 之间的连接线视图,使用 UILineRenderer 绘制 S 形曲线。 /// public class BlockConnectorView : MonoBehaviour { // 判定两端口坐标是否塌缩为同一点的阈值(像素)。 private const float DegenerateDistanceThreshold = 1f; public UILineRenderer curve; public StoryBlockView startBlock; public StoryBlockView endBlock; /// /// 设置连接线的起止 block 并重算曲线点。传入 null 时保留已有引用。 /// 注意:必须在 block 的 RectTransform 完成布局(Canvas 已刷新)后调用, /// 否则端口世界坐标尚未生效,会导致所有顶点塌缩到同一点。 /// public void SetCurve(StoryBlockView start = null, StoryBlockView end = null) { if (start != null) startBlock = start; if (end != null) endBlock = end; if (startBlock == null || endBlock == null) { Debug.LogWarning("[BlockConnectorView] 起点或终点 block 未设置,无法绘制曲线。"); return; } RectTransform selfRect = (RectTransform)transform; // ScreenSpaceCamera / WorldSpace 画布下的世界→屏幕转换需要对应相机;Overlay 传 null。 Camera uiCamera = ResolveUICamera(); Vector2 startPosition = GetLocalPoint(startBlock.outPort, selfRect, uiCamera); Vector2 endPosition = GetLocalPoint(endBlock.inPort, selfRect, uiCamera); if (Vector2.Distance(startPosition, endPosition) < DegenerateDistanceThreshold) { Debug.LogWarning( $"[BlockConnectorView] '{startBlock.blockId}' -> '{endBlock.blockId}' 端口坐标塌缩为同一点 " + $"(start={startPosition}, end={endPosition}),通常是在 block 布局完成前调用了 SetCurve。"); } // 两个控制点构成 S 形:先水平离开起点,再水平进入终点。 float midX = (startPosition.x + endPosition.x) / 2f; Vector2 mid1 = new Vector2(midX, startPosition.y); Vector2 mid2 = new Vector2(midX, endPosition.y); curve.Points = new[] { startPosition, mid1, mid2, endPosition }; curve.SetVerticesDirty(); } /// /// 使用 的世界位置 /// 转换到 的局部坐标(相对 pivot),即 UILineRenderer 顶点所用的坐标系。 /// private static Vector2 GetLocalPoint(RectTransform port, RectTransform space, Camera uiCamera) { Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(uiCamera, port.position); RectTransformUtility.ScreenPointToLocalPointInRectangle(space, screenPoint, uiCamera, out Vector2 localPoint); return localPoint; } /// /// 解析用于坐标转换的相机:Overlay 画布返回 null,其余(ScreenSpaceCamera / WorldSpace)返回画布相机。 /// private Camera ResolveUICamera() { Canvas canvas = curve != null ? curve.canvas : GetComponentInParent(); if (canvas == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay) return null; return canvas.worldCamera; } } }