您现在的位置是:首页 >技术杂谈 >Unity3d UI层级穿透之间的学习,实现点击空白区区域关闭UI界面网站首页技术杂谈
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表明的区域。
使用方法就是,将脚本添加到需要有这功能的界面根节点上,然后拖入点击不关闭的一个节点即可