You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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,直接计算鼠标到线段的垂直距离,逻辑简单且准确。

  1. 在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;
}
  1. 在编辑器窗口的事件循环中处理点击:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 04:06:25