您现在的位置是:首页 >技术杂谈 >Unity3d UI层级穿透之间的学习,实现点击空白区区域关闭UI界面网站首页技术杂谈

Unity3d UI层级穿透之间的学习,实现点击空白区区域关闭UI界面

1128STH 2025-07-16 00:01:03
简介Unity3d UI层级穿透之间的学习,实现点击空白区区域关闭UI界面

近期学习Unity3d的时候想制作出两个UI功能
1、界面上添加一个遮罩,让点击事件无法传递到下层
2、对于一个界面,点击空白区域关闭界面。

如果用cocos2dx来说,新建一个Layout,并且设置他的点击事件以及让他触摸吞噬,这样就可以完成了。
到了Unity3d这里,我发现有些组件里有一个RaycastTarget的勾选。经过百度以及各种AI的询问。得出了一个结论,这个RaycastTarget的勾选就是类似于触摸吞噬。随即我按照这个思路进行了界面的制作。
我先添加了一个Image(图片A),在这上面勾选了RaycastTarget属性,然后添加了EventTrigger的组件,并且添加了点击事件,这个事件是在控制台进行打印。然后又添加了一个Image(图片B),也在上面勾选了RaycastTarget属性。并且把图片B移动到图片A附近,并且保证两个图片中间有部分重叠。
按照设想应该会出现以下的情况:
1、点击图片A的部分控制台出现打印
2、点击图片B的部分没有任何反应
3、点击图片AB重合的部分也没有任何反应

确实也出现了上述的情况。当我以为成功的时候,我按照制作界面的方法去进行制作了。结果出现了和我所想不同的情况:
1、点击图片A的部分控制台出现打印
2、点击图片B的部分没有任何反应
3、点击图片AB重合的部分控制台出现打印

我随后查看了界面制作和我测试制作有什么不同。最后发现了一个问题:我在测试制作的时候,图片A和图片B是同级的节点,但当我制作界面的图片B是图片A的一个子节点

问题:同级关系和父子关系竟然会出现不同的结果?如果放在cocos里只要按照渲染的前后进行设置吞噬就可以了,但在3d中这个规则只适用于同级关系里。呢这里的父子的关系应该怎么处理呢?

经过AI和百度的查询,出现了一个新的名词:事件冒泡,以下是AI的解释:

在Unity的UI事件系统中,当子节点添加了EventTrigger组件(即使不绑定任何事件)时,会改变事件传递的逻辑。以下是详细解释:
1.事件传递机制
Raycast检测:当点击UI时,事件系统会通过射线检测(Raycast)收集所有被命中的UI元素(根据RaycastTarget属性),并按从后到前的渲染顺序排序(子节点通常渲染在父节点上方,因此会先被检测到)。
事件处理优先级:事件系统会遍历命中的元素列表,检查每个元素是否有处理该事件的组件(如Button、EventTrigger)。一旦找到第一个能处理事件的组件,就会触发它,并停止传递事件。

2.子节点无EventTrigger时
子节点只有Image(RaycastTarget=true),但没有事件处理组件(如EventTrigger或Button)。
事件系统检测到子节点被命中,但由于没有处理点击事件的组件,会继续遍历列表,检查下一个元素(父节点)。
父节点的EventTrigger被触发,因为它是下一个可处理事件的对象。

3.子节点添加EventTrigger时
EventTrigger组件本身实现了事件接口(如IPointerClickHandler),即使没有绑定任何回调。
事件系统检测到子节点有EventTrigger,认为它可以处理事件,于是直接触发子节点的事件逻辑(即使没有实际逻辑),并阻止事件继续传递到父节点。
因此,父节点的EventTrigger不再被调用。

4.关键区别
EventTrigger的存在会标记该对象为“事件处理器”,即使没有绑定事件,也会拦截事件流。
仅启用RaycastTarget但无事件处理器时,事件会继续传递到后续元素。

通过上面的解释我们可以这样理解,RaycastTarget类似于一个开关,EventTrigger是某一种条件。开关是控制是否出现在射线的列表里,条件是处理事件的东西,如果你没有东西,呢只能让下一位来处理事件,也就是父节点,一直会传递到根节点。用cocos2dx来类比那就是如果我想做触摸吞噬的那种效果,就要同时满足RaycastTarget以及EventTrigger。
上述理解后,我制作了以下的脚本,可以实现我最初的两个功能
1、界面上添加一个遮罩,让点击事件无法传递到下层
2、对于一个界面,点击空白区域关闭界面。

以下是代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class BlockLayer : MonoBehaviour
{
	public GameObject targetNode;
    public bool m_canUseBlockClose;
	// Start is called before the first frame update
	void Start()
    {
		// 检查当前节点是否已经有 Image 组件
		Image blockImage = GetComponent<Image>();
		if (blockImage == null)
		{
			// 如果没有 Image 组件,则添加 Image 组件
			blockImage = gameObject.AddComponent<Image>();
		}

		// 设置 Image 的透明度为 0
		blockImage.color = new Color(blockImage.color.r, blockImage.color.g, blockImage.color.b, 0);

		// 获取屏幕大小
		float screenWidth = Screen.width;
		float screenHeight = Screen.height;

		// 设置 Image 的大小为屏幕的 2 倍
		RectTransform rectTransform = GetComponent<RectTransform>();
		if (rectTransform == null)
		{
			rectTransform = gameObject.AddComponent<RectTransform>();
		}
		rectTransform.sizeDelta = new Vector2(screenWidth * 2, screenHeight * 2);

		// 设置 RaycastTarget 为开启
		blockImage.raycastTarget = true;

		// 添加 EventTrigger 组件
		EventTrigger trigger = GetComponent<EventTrigger>();
		if (trigger == null)
		{
			trigger = gameObject.AddComponent<EventTrigger>();
		}

		if (m_canUseBlockClose)
		{
			EventTrigger.Entry entry = new EventTrigger.Entry();
			entry.eventID = EventTriggerType.PointerClick;
			entry.callback.AddListener((data) => { OnPointerClick((PointerEventData)data); });
			trigger.triggers.Add(entry);

			// 检查 targetNode 是否有 Graphic 组件
			Graphic graphic = targetNode.GetComponent<Graphic>();
			if (graphic != null)
			{
				// 如果有 Graphic 组件,则设置 RaycastTarget 为 true
				graphic.raycastTarget = true;
			}
			else
			{
				Debug.LogWarning("Target node does not have a Graphic component.");
			}

			// 添加 EventTrigger 组件
			EventTrigger targetTrigger = targetNode.GetComponent<EventTrigger>();
			if (targetTrigger == null)
			{
				targetTrigger = targetNode.AddComponent<EventTrigger>();
			}
		}
	}

	// 点击事件的处理函数
	public void OnPointerClick(PointerEventData data)
	{
		Destroy(gameObject);
	}

	// Update is called once per frame
	void Update()
    {
        
    }
}

	其中
	public GameObject targetNode;
	public bool m_canUseBlockClose;
	第二个是一个功能开关,是指我这个界面是否需要点击空白区域关闭
	第一个是一个对象,主要是为了表明空白区域是哪块,空白区域其实就是脚本挂载的节点的区域减去targetNode表明的区域。
	使用方法就是,将脚本添加到需要有这功能的界面根节点上,然后拖入点击不关闭的一个节点即可
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。