Unity中如何阻止射线穿透UI按钮?场景问题及代码示例
问题描述
我编写了一个脚本用于检测省份的悬停与点击状态,点击省份后会选中该省份并显示对应UI界面。已修复“选中省份后点击UI建造按钮时,射线穿透按钮导致省份取消选中却未触发按钮点击”的问题,但仍存在新问题:当移动相机从不同角度查看地图时,按钮位置下方会出现省份,此时点击按钮会触发射线穿透,选中下方的省份。
原脚本代码:
using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections.Generic; public class ProvinceSelection : MonoBehaviour { public GameObject actionPanel; public BuildStructure provinceBuilder; public float hoverBrightness = 1.5f; public float originalBrightness = 1.0f; private Material provinceMaterial; private bool isSelected = false; private static GameObject selectedProvince; private Camera mainCamera; private int provinceLayerMask; void Start() { actionPanel.SetActive(false); mainCamera = GameObject.FindGameObjectWithTag("Player").GetComponent<Camera>(); provinceMaterial = GetComponent<Renderer>().material; // Set the province layer mask to ignore raycasts provinceLayerMask = 1 << LayerMask.NameToLayer("Provinces"); } void Update() { if (isSelected || IsHovering()) { provinceMaterial.SetFloat("_Brightness", hoverBrightness); } else { provinceMaterial.SetFloat("_Brightness", originalBrightness); } if (isSelected && Input.GetMouseButtonDown(0)) { // Check if the mouse is over UI if (!IsPointerOverButton()) { RaycastHit hit; Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); // Perform a raycast only against the provinces layer mask if (!Physics.Raycast(ray, out hit, Mathf.Infinity, provinceLayerMask)) { DeselectProvince(); } } } } void OnMouseDown() { if (selectedProvince != null && selectedProvince != gameObject) { selectedProvince.GetComponent<ProvinceSelection>().DeselectProvince(); } // Perform a raycast to check for UI elements between the camera and the clicked point Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hitInfo; bool hitUI = Physics.Raycast(ray, out hitInfo, Mathf.Infinity, LayerMask.GetMask("UI")); // If a UI element is hit, return without selecting the province if (hitUI) { return; } // Proceed with province selection isSelected = true; selectedProvince = gameObject; Debug.Log("Clicked on province: " + gameObject.name); actionPanel.SetActive(true); Text provinceNameText = actionPanel.GetComponentInChildren<Text>(); if (provinceNameText != null) { provinceNameText.text = gameObject.name; } else { Debug.LogWarning("No Text component found in the action panel."); } provinceBuilder.SetSelectedProvince(gameObject); } public void DeselectProvince() { isSelected = false; selectedProvince = null; actionPanel.SetActive(false); } bool IsHovering() { if (mainCamera != null) { Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { return hit.collider.gameObject == gameObject; } } return false; } bool IsPointerOverButton() { PointerEventData eventData = new PointerEventData(EventSystem.current); eventData.position = Input.mousePosition; // Create a list of raycast results List<RaycastResult> results = new List<RaycastResult>(); EventSystem.current.RaycastAll(eventData, results); // Check if any of the raycast results are UI buttons foreach (RaycastResult result in results) { if (result.gameObject.GetComponent<Button>() != null) { return true; } } return false; } }
问题分析
问题出在OnMouseDown方法的UI检测逻辑:使用Physics.Raycast检测UI层是错误的——Unity的UI系统有专门的射线检测机制(EventSystem),物理射线无法正确识别UI元素(除非UI对象额外添加3D Collider,这不符合UI设计规范)。当相机角度变化导致UI按钮与省份在屏幕空间重叠时,物理射线会优先命中省份的Collider,触发OnMouseDown从而选中下方省份。
解决方案
修改UI检测逻辑,使用Unity EventSystem提供的射线检测判断鼠标是否点击在UI元素上,只要点击在任意UI元素上,就不执行省份选中逻辑。同时优化原有的UI检测方法为通用型检测。
修改后的完整代码:
using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections.Generic; public class ProvinceSelection : MonoBehaviour { public GameObject actionPanel; public BuildStructure provinceBuilder; public float hoverBrightness = 1.5f; public float originalBrightness = 1.0f; private Material provinceMaterial; private bool isSelected = false; private static GameObject selectedProvince; private Camera mainCamera; private int provinceLayerMask; void Start() { actionPanel.SetActive(false); mainCamera = GameObject.FindGameObjectWithTag("Player").GetComponent<Camera>(); provinceMaterial = GetComponent<Renderer>().material; provinceLayerMask = 1 << LayerMask.NameToLayer("Provinces"); } void Update() { // 简化亮度设置逻辑 provinceMaterial.SetFloat("_Brightness", isSelected || IsHovering() ? hoverBrightness : originalBrightness); if (isSelected && Input.GetMouseButtonDown(0)) { if (!IsPointerOverUI()) { RaycastHit hit; Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); if (!Physics.Raycast(ray, out hit, Mathf.Infinity, provinceLayerMask)) { DeselectProvince(); } } } } void OnMouseDown() { // 优先判断是否点击UI,是则直接返回 if (IsPointerOverUI()) return; if (selectedProvince != null && selectedProvince != gameObject) { selectedProvince.GetComponent<ProvinceSelection>().DeselectProvince(); } isSelected = true; selectedProvince = gameObject; Debug.Log("Clicked on province: " + gameObject.name); actionPanel.SetActive(true); Text provinceNameText = actionPanel.GetComponentInChildren<Text>(); if (provinceNameText != null) { provinceNameText.text = gameObject.name; } else { Debug.LogWarning("No Text component found in the action panel."); } provinceBuilder.SetSelectedProvince(gameObject); } public void DeselectProvince() { isSelected = false; selectedProvince = null; actionPanel.SetActive(false); } bool IsHovering() { if (mainCamera == null) return false; Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit)) { return hit.collider.gameObject == gameObject; } return false; } // 通用方法:检测鼠标是否在任意UI元素上 bool IsPointerOverUI() { if (EventSystem.current == null) return false; PointerEventData eventData = new PointerEventData(EventSystem.current); eventData.position = Input.mousePosition; List<RaycastResult> results = new List<RaycastResult>(); EventSystem.current.RaycastAll(eventData, results); // 只要有UI元素被命中就返回true return results.Count > 0; } }
关键修改点
- 替换
OnMouseDown中的物理射线UI检测为IsPointerOverUI方法,使用EventSystem的射线检测准确判断点击是否落在UI上 - 重写原
IsPointerOverButton为通用的IsPointerOverUI方法,检测所有UI元素而非仅按钮,避免遗漏其他UI组件的干扰 - 简化Update中的亮度设置逻辑,用三元运算符让代码更简洁
内容的提问来源于stack exchange,提问作者Derek Leedberg
相关产品推荐
相关产品推荐

