如何为Handles.DrawBezier绘制的线条实现事件检测?
检测节点连线的点击事件解决方案
问题背景
使用Handles.DrawBezier绘制节点间连线时,该方法不支持点击事件响应。尝试用Rect匹配线条位置,但无法旋转Rect对齐倾斜线条,导致点击检测失效。
原提问核心代码
public abstract class Line : ScriptableObject { [HideInInspector] public string id; [HideInInspector] public Node parent; [HideInInspector] public Node children; public Rect lineRect; // Line values private const float lineWidth = 3f; private const float lineArrowSize = 5f; private const float dashSize = 4f; // Rect values private const float rectWidth = 7f; public abstract LineTypes LineType { get; } public void Draw(StoryBuilderLocalGraphSO localGraph) { Node parentLocalNode = localGraph.GetLocalNode(parent.id); Node childLocalNode = localGraph.GetLocalNode(children.id); Vector2 startPosition = parentLocalNode.rect.center; Vector2 endPosition = childLocalNode.rect.center; // 条件分支线条:带箭头的虚线 if (LineType == LineTypes.ConditionalBranch) { DrawArrow(startPosition, endPosition); Handles.DrawDottedLine(startPosition, endPosition, dashSize); } // 普通分支线条:带箭头的实线 else if (LineType == LineTypes.Branch) { DrawArrow(startPosition, endPosition); Handles.DrawBezier(startPosition, endPosition, startPosition, endPosition, Color.white, null, lineWidth); } // 普通连接线:无箭头实线 else if(LineType == LineTypes.ConnectingLine) { Handles.DrawBezier(startPosition, endPosition, startPosition, endPosition, Color.white, null, lineWidth); } lineRect = GetLineRect(startPosition, endPosition); var localNodeStyle = new GUIStyle(); localNodeStyle.normal.background = EditorGUIUtility.Load("node1") as Texture2D; GUILayout.BeginArea(lineRect, localNodeStyle); EditorGUILayout.LabelField("Line"); GUILayout.EndArea(); GUI.changed = true; } private Rect GetLineRect(Vector2 startPosition, Vector2 endPosition) { float distance = Vector2.Distance(startPosition, endPosition); Vector2 scale = (startPosition+endPosition)/2; return new Rect(Mathf.Abs(scale.x), Mathf.Abs(scale.y), rectWidth, distance); } private void DrawArrow(Vector2 startPosition, Vector2 endPosition) { Vector2 midPosition = (endPosition + startPosition) / 2f; Vector2 direction = endPosition - startPosition; Vector2 arrowTailPoint1 = midPosition - new Vector2(-direction.y, direction.x).normalized * lineArrowSize; Vector2 arrowTailPoint2 = midPosition + new Vector2(-direction.y, direction.x).normalized * lineArrowSize; Vector2 arrowHeadPoint = midPosition + direction.normalized * lineArrowSize; Handles.DrawBezier(arrowHeadPoint, arrowTailPoint1, arrowHeadPoint, arrowTailPoint1, Color.white, null, lineArrowSize); Handles.DrawBezier(arrowHeadPoint, arrowTailPoint2, arrowHeadPoint, arrowTailPoint2, Color.white, null, lineArrowSize); } ... }
解决方案
方法1:距离判断检测点击(推荐)
放弃Rect,直接计算鼠标到线段的垂直距离,逻辑简单且准确。
- 在
Line类中添加点击检测方法:
public bool IsClicked(Vector2 mousePosition) { Vector2 start = parent.rect.center; Vector2 end = children.rect.center; // 计算鼠标到线段的最近点 Vector2 closestPoint = GetClosestPointOnLine(start, end, mousePosition); // 判断距离是否在可点击范围内(线条宽度+容错值) float clickThreshold = (lineWidth + 2f) / 2f; return Vector2.Distance(mousePosition, closestPoint) <= clickThreshold; } private Vector2 GetClosestPointOnLine(Vector2 lineStart, Vector2 lineEnd, Vector2 point) { Vector2 lineDir = lineEnd - lineStart; float dotProduct = Vector2.Dot(point - lineStart, lineDir); // 限制t在0-1之间,确保最近点在线段上(而非无限延长线) float t = Mathf.Clamp01(dotProduct / lineDir.sqrMagnitude); return lineStart + t * lineDir; }
- 在编辑器窗口的事件循环中处理点击:
private void OnGUI() { Event evt = Event.current; // 检测左键按下事件 if (evt.type == EventType.MouseDown && evt.button == 0) { foreach (Line line in yourLineCollection) { if (line.IsClicked(evt.mousePosition)) { // 处理点击逻辑:选中线条、弹出菜单等 Debug.Log($"点击了线条 {line.id}"); evt.Use(); // 标记事件已处理,避免触发其他UI元素 break; } } } }
方法2:使用自定义Handle交互
利用Unity编辑器的Handles系统,添加可交互的自定义区域:
在Line.Draw方法末尾添加:
public void Draw(StoryBuilderLocalGraphSO localGraph) { // ... 原有绘制代码 ... Event evt = Event.current; if (evt.type == EventType.MouseDown && evt.button == 0) { if (IsClicked(evt.mousePosition)) { // 处理点击逻辑 Debug.Log("线条被点击"); evt.Use(); } } }
方法3:修复旋转Rect的问题(不推荐)
若坚持用Rect,需通过矩阵变换实现旋转后的点击检测,但逻辑复杂且易出问题:
public bool IsRectClicked(Vector2 mousePosition) { Vector2 start = parent.rect.center; Vector2 end = children.rect.center; Vector2 center = (start + end) / 2; // 计算线条的旋转角度 float angle = Mathf.Atan2(end.y - start.y, end.x - start.x) * Mathf.Rad2Deg; // 保存原始GUI矩阵 Matrix4x4 originalMatrix = GUI.matrix; // 应用旋转矩阵(以线条中心为旋转点) GUI.matrix = Matrix4x4.TRS(center, Quaternion.Euler(0, 0, angle), Vector3.one); // 创建以原点为中心的Rect,通过矩阵变换映射到实际位置 float lineLength = Vector2.Distance(start, end); Rect rotatedRect = new Rect(-rectWidth/2, -lineLength/2, rectWidth, lineLength); // 转换鼠标坐标到变换后的GUI空间 Vector2 localMousePos = GUIUtility.ScreenToGUIPoint(mousePosition); bool isClicked = rotatedRect.Contains(localMousePos); // 恢复原始矩阵 GUI.matrix = originalMatrix; return isClicked; }
总结
推荐使用方法1,无需处理复杂的GUI矩阵变换,逻辑清晰且适配各种角度的线条,后续若改用曲线连线,也可扩展距离计算逻辑适配贝塞尔曲线。
内容的提问来源于stack exchange,提问作者Joel Fernando
相关产品推荐
相关产品推荐

