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

Unity3D中如何基于屏幕点击查找视觉上最近的物体?

解决Unity中点击屏幕选择视觉最近管状物体的问题

你的核心需求是基于屏幕视觉位置选择最近物体,而非3D空间距离,原SphereCastAll的问题在于:球形射线的碰撞点是3D空间中先接触的位置,不一定是物体上屏幕坐标离点击点最近的部分,导致判断错误。下面给出两种针对性的解决方案:

方案一:针对管状物体计算屏幕线段距离

因为目标是横置圆柱体(管道),可以把管道的轴线转换为屏幕上的线段,直接计算点击点到这条线段的最短距离,以此判断视觉上的远近:

步骤1:实现屏幕点到线段的距离计算方法

// 计算屏幕上一点到线段的最短距离
float GetScreenDistanceToLineSegment(Vector2 mousePos, Vector2 lineStart, Vector2 lineEnd)
{
    float lengthSquared = (lineEnd - lineStart).sqrMagnitude;
    if (lengthSquared == 0) return Vector2.Distance(mousePos, lineStart);

    // 计算点在线段上的投影比例,限制在0-1之间(避免超出线段范围)
    float t = Mathf.Clamp01(Vector2.Dot(mousePos - lineStart, lineEnd - lineStart) / lengthSquared);
    Vector2 closestPoint = lineStart + t * (lineEnd - lineStart);
    return Vector2.Distance(mousePos, closestPoint);
}

步骤2:点击逻辑中筛选视觉最近的管道

void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        // 用RaycastAll获取所有被射线命中的物体
        RaycastHit[] hitList = Physics.RaycastAll(ray, 100f);

        if (hitList.Length > 0)
        {
            GameObject nearestPipe = null;
            float minScreenDistance = float.MaxValue;
            Vector2 mousePos = Input.mousePosition;

            foreach (RaycastHit hit in hitList)
            {
                // 假设管道沿transform.right方向延伸,根据你的实际轴向调整(比如Z轴改为transform.forward)
                Collider pipeCollider = hit.collider;
                float pipeLength = pipeCollider.bounds.size.x;
                // 获取管道轴线的两个端点
                Vector3 axisStart = hit.transform.position - hit.transform.right * pipeLength / 2f;
                Vector3 axisEnd = hit.transform.position + hit.transform.right * pipeLength / 2f;

                // 转换为屏幕坐标
                Vector2 screenStart = Camera.main.WorldToScreenPoint(axisStart);
                Vector2 screenEnd = Camera.main.WorldToScreenPoint(axisEnd);

                // 计算点击点到屏幕线段的距离
                float distance = GetScreenDistanceToLineSegment(mousePos, screenStart, screenEnd);

                // 更新最近物体
                if (distance < minScreenDistance)
                {
                    minScreenDistance = distance;
                    nearestPipe = hit.transform.gameObject;
                }
            }

            // 输出选中结果
            if (nearestPipe != null)
            {
                Debug.Log("选中视觉最近的管道:" + nearestPipe.name);
            }
        }
    }
}

方案二:通用屏幕包围盒距离判断

如果需要兼容更多类型的物体,可通过计算物体屏幕包围盒与点击点的最短距离来判断:

void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit[] hitList = Physics.RaycastAll(ray, 100f);

        if (hitList.Length > 0)
        {
            GameObject nearestObj = null;
            float minScreenDistance = float.MaxValue;
            Vector2 mousePos = Input.mousePosition;

            foreach (RaycastHit hit in hitList)
            {
                Bounds worldBounds = hit.collider.bounds;
                Rect screenBounds = GetWorldBoundsToScreenRect(worldBounds);

                // 先判断点击点是否在包围盒内,此时距离为0
                if (screenBounds.Contains(mousePos))
                {
                    minScreenDistance = 0;
                    nearestObj = hit.transform.gameObject;
                    break; // 直接选中,无需继续比较
                }

                // 计算点击点到包围盒各边的最短距离
                float closestDist = float.MaxValue;
                // 包围盒四个边的线段
                Vector2[] edges = new Vector2[]
                {
                    screenBounds.min, new Vector2(screenBounds.xMax, screenBounds.yMin),
                    new Vector2(screenBounds.xMax, screenBounds.yMin), screenBounds.max,
                    screenBounds.max, new Vector2(screenBounds.xMin, screenBounds.yMax),
                    new Vector2(screenBounds.xMin, screenBounds.yMax), screenBounds.min
                };

                for (int i = 0; i < edges.Length; i += 2)
                {
                    float dist = GetScreenDistanceToLineSegment(mousePos, edges[i], edges[i+1]);
                    if (dist < closestDist) closestDist = dist;
                }

                if (closestDist < minScreenDistance)
                {
                    minScreenDistance = closestDist;
                    nearestObj = hit.transform.gameObject;
                }
            }

            if (nearestObj != null)
            {
                Debug.Log("选中视觉最近的物体:" + nearestObj.name);
            }
        }
    }
}

// 把世界空间包围盒转换为屏幕空间矩形
Rect GetWorldBoundsToScreenRect(Bounds worldBounds)
{
    Camera cam = Camera.main;
    Vector3 minScreen = cam.WorldToScreenPoint(worldBounds.min);
    Vector3 maxScreen = cam.WorldToScreenPoint(worldBounds.max);

    float xMin = Mathf.Min(minScreen.x, maxScreen.x);
    float xMax = Mathf.Max(minScreen.x, maxScreen.x);
    float yMin = Mathf.Min(minScreen.y, maxScreen.y);
    float yMax = Mathf.Max(minScreen.y, maxScreen.y);

    return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
}

// 复用之前的GetScreenDistanceToLineSegment方法
float GetScreenDistanceToLineSegment(Vector2 mousePos, Vector2 lineStart, Vector2 lineEnd)
{
    float lengthSquared = (lineEnd - lineStart).sqrMagnitude;
    if (lengthSquared == 0) return Vector2.Distance(mousePos, lineStart);

    float t = Mathf.Clamp01(Vector2.Dot(mousePos - lineStart, lineEnd - lineStart) / lengthSquared);
    Vector2 closestPoint = lineStart + t * (lineEnd - lineStart);
    return Vector2.Distance(mousePos, closestPoint);
}

原方案问题解析

SphereCastAll是3D空间的球形射线检测,它会优先命中离相机更近的物体,且碰撞点是球体与物体表面的交点——这个点的屏幕坐标不一定是物体上离点击点最近的位置,比如红色管道的斜向部分可能先被命中,但视觉上蓝色管道才是点击点正下方的目标,导致判断偏差。

内容的提问来源于stack exchange,提问作者Digger

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 02:43:16