UGUI源码试探究 (二) 事件系统
1. 按钮响应的过程
按钮注册事件
设置断点, 鼠标点击按钮, 触发断点
查看此时的调用堆栈
我们来到栈底: 鼠标按下之后, EventSystem.Update
调用了第一个方法
来到了StandaloneInputModule.Process
如果鼠标抬起的元素跟鼠标按下的元素一样的话
如何获得鼠标按下的元素呢?
由下图可知, MouseButtonEventData
记录了鼠标进入的物体(Text
)和鼠标按下的物体(Button
)
如何获得鼠标抬起的元素呢?
通过射线检测 , pointerEvent.pointerCurrentRaycast.gameObject
可以获得当前鼠标抬起的物体为Text (UnityEngine.GameObject)
GetEventHandler
可以找到Text (UnityEngine.GameObject)
其父物体上实现了IPointerClickHandler
接口的Button (UnityEngine.GameObject)
相关方法有:
/// <summary>
/// Bubble the specified event on the game object, figuring out which object will actually receive the event.
/// 从root物体开始(向其父物体遍历), 找出哪个对象实际上会处理指定事件。
/// </summary>
public static GameObject GetEventHandler<T>(GameObject root) where T : IEventSystemHandler
{
if (root == null)
return null;
Transform t = root.transform;
while (t != null)
{
if (CanHandleEvent<T>(t.gameObject))
return t.gameObject;
t = t.parent;
}
return null;
}
/// <summary>
/// Whether the specified game object will be able to handle the specified event.
/// 指定的对象是否能够处理指定的事件。
/// </summary>
public static bool CanHandleEvent<T>(GameObject go) where T : IEventSystemHandler{
var internalHandlers = s_HandlerListPool.Get();
GetEventList<T>(go, internalHandlers);
var handlerCount = internalHandlers.Count;
s_HandlerListPool.Release(internalHandlers);
return handlerCount != 0;
}
ExecuteEvents.Execute
执行的是实现了IPointerClickHandler
接口OnPointerClick
方法的物体(就是Button
啦)
我们来到了Button
的OnPointerClick
方法, 发现它调用了Press
方法!!!
然后如果我们在某个自定义方法例如SetImageColor
注册了按钮的onClick
方法, 就会执行该方法的代码了
public ButtonClickedEvent onClick
{
get { return m_OnClick; }
set { m_OnClick = value; }
}
2. 事件触发的过程
如果不是按钮, 想要响应鼠标点击事件呢??
继承IPointerClickHandler
方法就好了, 可以使用EventTrigger
组件
例如在Image
上面添加ventTrigger
组件, 添加PointerClick
事件
3. 射线检测的过程
鼠标如何检测到物体的呢???
由这里我们可以看出leftButtonData
已经储存了鼠标按下的信息,
又因为GetMousePointerEventData(id) ->mouseData -> leftButtonData
来到GetMousePointerEventData
, 发现它的功能是填充鼠标左, 中, 右三键信息
在m_RaycastResultCache.Clear
处加入断点, 可以查看相关信息
进入RaycastAll
中查看, 此时的module
为Canvas
按下F11
, 来到了GraphicRaycaster.Raycast
, Raycast
有两个重载函数
//①
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
//②
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
Raycast
函数①是用于筛选三个条件的
函数①的执行步骤为:
通过
BlockingObjects
和BlockingMask
获取hitDistance
BlockingObjects
: 射线检测时, 可以阻塞射线的物体(None / TwoD / Three D / All)BlockingMask
: 射线检测时, 对哪一些layer
检测hitDistance
: 射线碰撞到第一个指定类型物体时两点的射线的距离(RaycastHit2D.distance
)
通过
Raycast
函数②筛选符合条件的Graphic
物体函数②筛选条件为:
RectangleContainsScreenPoint
判断pointerPosition
是否在graphic.rectTransform
范围内(即判断x, y值, 此时Image
不在范围内, 被剔除)WorldToScreenPoint
判断pointerPosition.z
是否在相机可见的范围内(即判断z
值)graphic.Raycast
通过ICanvasRaycastFilter.IsRaycastLocationValid
判断屏幕上的点pointerPosition
是否满足检测有效条件在
Image
,Mask
,RectMask2D
中继承接口ICanvasRaycastFilter
并重写方法IsRaycastLocationValid
在
Graphic
中继承接口ICanvasRaycastFilter
并调用方法IsRaycastLocationValid
通过
ignoreReversedGraphics
筛选最后符合条件的可以响应鼠标检测的物体- 通过两个向量的点乘结果判断
if (ignoreReversedGraphics)
{
if (currentEventCamera == null)
{
// If we dont have a camera we know that we should always be facing forward
// 如果没有相机,屏幕的正前方与物体的向量点乘
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
}
else
{
// If we have a camera compare the direction against the cameras forward.
// 如果有相机,相机的正前方与物体的向量点乘
var cameraFoward = currentEventCamera.transform.rotation * Vector3.forward;
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(cameraFoward, dir) > 0;
}
}
distance
(射线到物体所在平面的距离)与hitDistance
(射线碰撞到第一个BlockingObjects
时射线的距离)做比较
if (appendGraphic)
{
float distance = 0;
if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
distance = 0;
else
{
Transform trans = go.transform;
Vector3 transForward = trans.forward;
// 计算射线到物体所在平面的距离
// 算法参考 http://geomalgorithms.com/a06-_intersect-2.html
distance = (Vector3.Dot(transForward, trans.position - currentEventCamera.transform.position) / Vector3.Dot(transForward, ray.direction));
// Check to see if the go is behind the camera.
if (distance < 0)
continue;
}
if (distance >= hitDistance)
continue;
var castResult = new RaycastResult
{
gameObject = go,
module = this,
distance = distance,
screenPosition = eventPosition,
index = resultAppendList.Count,
depth = m_RaycastResults[index].depth,
sortingLayer = canvas.sortingLayerID,
sortingOrder = canvas.sortingOrder
};
resultAppendList.Add(castResult);
}
按下F11
, RaycastAll
出栈, 通过FindFirstRaycast
获得第一个射线检测到的物体为Text, 然后就可以进行愉快地操作(按钮响应, 事件触发)啦