主要是求职复习中记的笔记,估计不太好看懂,各位可以参考一下。本篇是翻UGUI源码总结的,可以对照源码自己看看。
资料
(99+ 封私信 / 84 条消息) Unity入门精要 - 知乎
UGUI事件原理简述
本篇文章详细研究从玩家点击到按钮响应的流程,即事件处理流程。首先根据鼠标位置找到最上方的图形(继承自graphic,如Image等,以下称图形)。然后分发一些事件,分别向父物体查找第一个能接受的控件(继承自Selectable,如button等,以下称控件),让这个控件响应点击。
这个流程优点在于,可以在多个按钮重叠的时候,只响应最上方按钮。同时事件是分别处理的,可以实现能点击的同时也响应父物体的拖动。缺点在于点击穿透难以处理。
具体过程如下:
因为总体流程很复杂,我们以函数为单位,说明其所有逻辑。调用的子函数会写在下个段落。
EventSystem.Update总入口
调用所有InputModule(我们下面只说明了StandaloneInputModule.Process)执行输入模块的tick
执行GetMousePointerEventData,根据屏幕坐标执行++EventSystem.RaycastAll++(见下段落),查询能点到的物体
查询第一个结果就是玩家点到的东西。点击结果将保存在PointerInputModule.m_PointerData。复制几份,分别给左右中键使用
调用ExecuteEvents.ExecuteHierarchy查找点击物体上的selectable控件
如果没有找到,调用ExecuteEvents.GetEventHandler在父物体上查找
最终调用ExecuteEvents.Execute,执行事件回调。
EventSystem.RaycastAll
找到所有BaseRaycast(一般就一个GraphicRaycaster),分别执行++Raycast++(见下一个段落),将结果汇总。
对结果使用EventSystem.RaycastComparer进行排序。优先级分别为
RaycastResult.module.eventCamera.depth(降序)
RaycastResult.module.sortOrderPriority
RaycastResult.module.renderOrderPriority
RaycastResult.sortingLayer
RaycastResult.sortingOrder
RaycastResult.depth(仅canvas相同)
RaycastResult.distance
RaycastResult.sortingGroupID
RaycastResult.sortingGroupOrder
RaycastResult.index
GraphicRaycaster.Raycast
GraphicRaycaster.Raycast访问Canvas的GraphicRegistry来获取所有可射线检测图形。(GraphicRegistry.m_Graphics缓存所有图形,GraphicRegistry.m_RaycastableGraphics缓存所有可射线检测图形)
多屏幕支持,检查触发点击的屏幕是否为当前相机的屏幕
读取鼠标位置,使用相机将其转换为视口坐标系,超出视口的点击剔除。如果没有相机直接使用屏幕尺寸转换
根据鼠标位置和相机进行射线检测,找到最近的模型的距离,用来在下面进行模型遮挡剔除
(需要GraphicRaycaster的RenderMode不是ScreenSpaceOverlay,并且blockingObjects不是None)
调用另一个++GraphicRaycaster.Raycast++函数进行射线检测(见下方,参数不同的重载)
对所有检测到的图形进行处理:
如果打开GraphicRaycaster.ignoreReversedGraphics,旋转到背面的图形不会被点击。(仅看方向,scale为负无影响)
检查图形是否在镜头后面。镜头后的物体被排除。
与之前计算出的最近的模型的距离比较,如果模型更近,图形不会被点击
结果放入resultAppendList中返回
GraphicRaycaster.Raycast
忽略raycastTarget为false图形,忽略canvasRenderer.cull没打开图形,忽略depth==-1图形
调用RectTransformUtility.RectangleContainsScreenPoint检查点击点是否在图形的矩形内。如果修改raycast padding则矩形范围会变化。
将图形转换到镜头坐标系,检查其z轴是否超出远平面farClipPlane
调用++Graphic.Raycast++检查。此函数可重载。
检查完毕,使用图形的depth排序。结果在results中传出
Graphic.Raycast
检查isActiveAndEnabled
从本节点开始,依次向父物体查找:
如果父物体有Canvas,且overrideSorting(什么东西?)被打开,不再继续查找
查找父物体的ICanvasRaycastFilter。
如果是CanvasGroup,处理ignoreParentGroups逻辑。他会让CanvasGroup的所有父物体不再响应事件。
调用ICanvasRaycastFilter.IsRaycastLocationValid检查。遮罩等会使用这个来屏蔽所有子物体被隐藏的区域。
源码中IsRaycastLocationValid的使用情况
CanvasGroup:直接返回blocksRaycasts
Mask和RectMask2D:点击点需要在mask范围内才能响应
Image:与图片进行透明度测试。需要设置alphaHitTestMinimumThreshold
如果点击失败了,有那些可能?
graphic的raycast target没打开,图形禁用或物体隐藏
graphic的raycast padding如果被设置,会影响响应范围。
有更靠前的graphic挡住了点击
没有控件接受响应,控件禁用或物体隐藏,有其他控件先拦截了事件。
实现了ICanvasRaycastFilter方法,用代码修改了响应范围,这个接口可控制其所有子节点的响应
CanvasGroup的ignoreParentGroups打开后,之后的父物体不再影响点击范围
Image有个自带的根据透明度进行不规则形状点击功能alphaHitTestMinimumThreshold。
CanvasGroup的blocksRaycasts关闭时下面所有图形都不响应
被mask或者rectMask2D隐藏的位置不会响应点击
需要有EventSystem,Canvas上需要有GraphicRaycaster
打开GraphicRaycaster的blockingObjects时,3DUI可能会与模型遮挡,导致点不到UI
打开GraphicRaycaster的blocking mask时,检测不到没有在mask中物体
打开GraphicRaycaster.ignoreReversedGraphics时,方向为反向的图形不会被响应
镜头后方的UI不会被响应,镜头外的UI不会被响应,z轴异常可能导致不响应
注意事项
EventSystem中有个currentSelectedGameObject,标记了当前选中的物体。一般鼠标移入的物体被自动选中,可以使用键盘方向键切换,手柄摇杆可以切换。请使用这个切换选中,可以方便的支持多输入设备,如同时使用手机触屏和游戏手柄控制。
EventSystem.IsPointerOverGameObject可以用来检查鼠标是否停留在UI上,挺常用。
EventSystem.RaycastAll可以用来模拟UI点击过程,获取点击列表。能用来做点击穿透等。
调用ExecuteEvents.GetEventHandler查找点击某个物体时,会接受事件控件。会向父物体查找。
调用ExecuteEvents.Execute,可执行事件回调,与底层实现相同。
要拓展自己的控件,只要实现对应接口即可拦截事件,支持的事件见下表。官方有个EventTrigger,不过应该没人用吧?
将raycast padding调到无穷大,然后实现ICanvasRaycastFilter接口,将框检测反转,就能实现拦截不在框里的点击。这个应该能用来新手引导(禁用其他位置的点击)
PhysicsRaycaster等能实现模型的点击。
scroll下面放按钮等,就能实现可点击同时也能拖拽。
由于支持了移入选中等逻辑,事件检测是每帧都做的,并且计算量很大,对性能有较大影响。
所有支持的事件event整理
|----------|---------------------------------|
| 基类 | IEventSystemHandler |
| 指针进入 | IPointerEnterHandler |
| 指针离开 | IPointerExitHandler |
| 按下按钮 | IPointerDownHandler |
| 松开按钮 | IPointerUpHandler |
| 点击按钮 | IPointerClickHandler |
| 初始化拖拽 | IInitializePotentialDragHandler |
| 开始拖拽 | IBeginDragHandler |
| 正在拖拽 | IDragHandler |
| 结束拖拽 | IEndDragHandler |
| 拖拽目标 | IDropHandler |
| 滚轮滚动 | IScrollHandler |
| 选中对象tick | IUpdateSelectedHandler |
| 被选中 | ISelectHandler |
| 被取消选中 | IDeselectHandler |
| 移动 | IMoveHandler |
| 确定 | ISubmitHandler |
| 取消 | ICancelHandler |
优化方法
减少打开raycast的graphic。一个控件尽量只使用一个带raycast的图形。注意背景一般需要无响应raycast图形来避免点击穿透。
解决方案
默认关闭raycast。
建立静态检查,扫描出所有具有多个图形的控件,并提示修改。
提供工具快速关闭不需要raycast。
提供工具预览控件响应范围(也能用来查按钮响应范围异常)。
maskable的优化可以一起做了。关闭不使用mask图形的开关,能降低消耗。
之前写的raycast笔记,参考这篇里射线检测过程部分