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

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;
    }
}
关键修改点
  1. 替换OnMouseDown中的物理射线UI检测为IsPointerOverUI方法,使用EventSystem的射线检测准确判断点击是否落在UI上
  2. 重写原IsPointerOverButton为通用的IsPointerOverUI方法,检测所有UI元素而非仅按钮,避免遗漏其他UI组件的干扰
  3. 简化Update中的亮度设置逻辑,用三元运算符让代码更简洁

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 07:37:32