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
相关产品推荐
相关产品推荐

