目录
StandaloneInputModule 的主函数 ProcessMouseEvent()
ExecuteEvents.ExecuteHierarchy
本篇中使用的源码是 Unity 2017 版本。Unity UGUI 公开源码地址
UGUI 事件系统源码剖析
它把 UGUI 系统分为输入事件,动画,核心渲染三部分。
动画部分:用了 tween 补间动画的形式,对颜色,位置,大小做了渐进的操作。
tween 的原理:在启动一个协程,在协程里对元素的属性渐进式的修改,除了修改属性数值,tween还有多种曲线可以选择。
输入事件源码
UGUI 系统把输入事件模块分为四部分,即事件数据模块、输入事件捕获模块、射线碰撞检测模块、事件逻辑处理及回调模块。
事件数据模块
作用:它主要定义并且存储了事件发生时的位置、与事件对应的物体、事件的位移大小、触发事件的输入类型及事件的设备信息等。主要是为了获取数据,提供数据服务。
PointerEventData 和 AxisEventData 继承自 BaseEventData。
BaseEventData
事件基础数据类,定义了几个常用的接口。
public abstract class AbstractEventData
{
protected bool m_Used;
public virtual void Reset()
{
m_Used = false;
}
public virtual void Use()
{
m_Used = true;
}
public virtual bool used
{
get { return m_Used; }
}
}
public class BaseEventData : AbstractEventData
{
private readonly EventSystem m_EventSystem;
public BaseEventData(EventSystem eventSystem)
{
m_EventSystem = eventSystem;
}
public BaseInputModule currentInputModule
{
get { return m_EventSystem.currentInputModule; }
}
public GameObject selectedObject
{
get { return m_EventSystem.currentSelectedGameObject; }
set { m_EventSystem.SetSelectedGameObject(value, this); }
}
}
AxisEventData
滚轮事件数据类,只需要提供滚轮的方向信息。
namespace UnityEngine.EventSystems
{
public class AxisEventData : BaseEventData
{
//移动方向
public Vector2 moveVector { get; set; }
public MoveDirection moveDir { get; set; }
public AxisEventData(EventSystem eventSystem)
: base(eventSystem)
{
moveVector = Vector2.zero;
moveDir = MoveDirection.None;
}
}
}
PointerEventData
点位事件数据类,最常用的事件数据。
public class PointerEventData : BaseEventData
{
public GameObject pointerEnter { get; set; }
// 接收OnPointerDown事件的物体
private GameObject m_PointerPress;
// 上一下接收OnPointerDown事件的物体
public GameObject lastPress { get; private set; }
// 接收按下事件的无法响应处理的物体
public GameObject rawPointerPress { get; set; }
// 接收OnDrag事件的物体
public GameObject pointerDrag { get; set; }
public RaycastResult pointerCurrentRaycast { get; set; }
public RaycastResult pointerPressRaycast { get; set; }
public List<GameObject> hovered = new List<GameObject>();
public bool eligibleForClick { get; set; }
public int pointerId { get; set; }
// 鼠标或触摸时的点位
public Vector2 position { get; set; }
// 滚轮的移速
public Vector2 delta { get; set; }
// 按下时的点位
public Vector2 pressPosition { get; set; }
// 为双击服务的上次点击时间
public float clickTime { get; set; }
// 为双击服务的点击次数
public int clickCount { get; set; }
public Vector2 scrollDelta { get; set; }
public bool useDragThreshold { get; set; }
public bool dragging { get; set; }
public InputButton button { get; set; }
}
PointerEventData 存储了大部分的事件系统逻辑需要的数据,包括按下时的位置、松开与按下的时间差、拖曳的位移差、点击的物体等,承载了所有输入事件需要的数据。
输入事件捕获模块源码
输入事件捕获模块由 BaseInputModule、PointerInputModule、StandaloneInputModule、TouchInputModule 四个类组成。
- BaseInputModule 是抽象(abstract)基类,提供必须的空接口和基本变量。
- PointerInputModule 继承自 BaseInputModule,并且在其基础上扩展了关于点位的输入逻辑,增加了输入的类型和状态。
- StandaloneInputModule 和 TouchInputModule 又继承了 PointerInputModule,它们从父类开始延展向不同的方向。
- StandaloneInputModule:向标准键盘鼠标输入方向拓展。
- TouchInputModule:向触控板输入方向拓展。
StandaloneInputModule 的主函数 ProcessMouseEvent()
/// <summary>
/// 处理所有的鼠标事件
/// </summary>
protected void ProcessMouseEvent(int id)
{
var mouseData = GetMousePointerEventData(id);
var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;
// Process the first mouse button fully
// 处理鼠标左键相关的事件
ProcessMousePress(leftButtonData);
ProcessMove(leftButtonData.buttonData);
ProcessDrag(leftButtonData.buttonData);
// Now process right / middle clicks
// 处理鼠标右键和中建的点击事件
ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);
//滚轮事件处理
if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
{
var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
}
}
它从鼠标键盘输入事件上扩展了输入的逻辑,处理了鼠标的按下、移动、滚轮、拖曳等操作事件。其中比较重要的函数为 ProcessMousePress()、ProcessMove()、ProcessDrag() 这三个函数。
ProcessMousePress
/// <summary>
/// Process the current mouse press.
/// 处理鼠标按下事件
/// </summary>
protected void ProcessMousePress(MouseButtonEventData data)
{
var pointerEvent = data.buttonData;
var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
// PointerDown notification
// 按下通知
if (data.PressedThisFrame())
{
pointerEvent.eligibleForClick = true;
pointerEvent.delta = Vector2.zero;
pointerEvent.dragging = false;
pointerEvent.useDragThreshold = true;
pointerEvent.pressPosition = pointerEvent.position;
pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;
DeselectIfSelectionChanged(currentOverGo, pointerEvent);
// 搜索元件中按下事件的句柄,并执行按下事件句柄
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
// didnt find a press handler... search for a click handler
// 搜索后找不到句柄,就设置一个自己的
if (newPressed == null)
newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
// Debug.Log("Pressed: " + newPressed);
float time = Time.unscaledTime;
if (newPressed == pointerEvent.lastPress)
{
var diffTime = time - pointerEvent.clickTime;
if (diffTime < 0.3f)
++pointerEvent.clickCount;
else
pointerEvent.clickCount = 1;
pointerEvent.clickTime = time;
}
else
{
pointerEvent.clickCount = 1;
}
pointerEvent.pointerPress = newPressed;
pointerEvent.rawPointerPress = currentOverGo;
pointerEvent.clickTime = time;
// Save the drag handler as well
// 保存拖拽信息
pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);
// 执行拖拽启动事件句柄
if (pointerEvent.pointerDrag != null)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
}
// PointerUp notification
// 抬起通知
if (data.ReleasedThisFrame())
{
//执行抬起事件的句柄
// Debug.Log("Executing pressup on: " + pointer.pointerPress);
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
// Debug.Log("KeyCode: " + pointer.eventData.keyCode);
var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
// 如果抬起时与按下时为同一个元素,那就是点击
if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
{
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
}
// 否则也可能是拖拽的释放
else if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
{
ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
}
pointerEvent.eligibleForClick = false;
pointerEvent.pointerPress = null;
pointerEvent.rawPointerPress = null;
// 如果正在拖拽则抬起事件等于拖拽结束事件
if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);
pointerEvent.dragging = false;
pointerEvent.pointerDrag = null;
// 如果当前接收事件的物体和事件的刚开始的物体不一致,则对两个物体做进和出的事件处理
if (currentOverGo != pointerEvent.pointerEnter)
{
HandlePointerExitAndEnter(pointerEvent, null);
HandlePointerExitAndEnter(pointerEvent, currentOverGo);
}
}
}
它不仅仅处理的是按下的操作,还处理了鼠标松开时的操作,以及拖曳启动和拖曳松开与结束的事件。在调用处理相关句柄的前后,事件数据都会保存在 pointerEvent 类中,然后被传递给业务层中设置的输入事件句柄。
ProcessDrag
protected virtual void ProcessDrag(PointerEventData pointerEvent)
{
bool moving = pointerEvent.IsPointerMoving();
// 如果已经在移动,且还没开始拖拽启动事件,则调用拖拽启动句柄,并设置拖拽中标记为true
if (moving && pointerEvent.pointerDrag != null
&& !pointerEvent.dragging
&& ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold))
{
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler);
pointerEvent.dragging = true;
}
// 拖拽时的句柄处理
if (pointerEvent.dragging && moving && pointerEvent.pointerDrag != null)
{
// 如果按下的物体和拖拽的物体不是同一个则视为抬起拖拽,并清除前面按下时的标记
if (pointerEvent.pointerPress != pointerEvent.pointerDrag)
{
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
pointerEvent.eligibleForClick = false;
pointerEvent.pointerPress = null;
pointerEvent.rawPointerPress = null;
}
// 执行拖拽中句柄
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.dragHandler);
}
}
与 ProcessMousePress() 类似对拖曳事件逻辑进行了判断,包括拖曳开始事件处理、判断结束拖曳事件及拖曳句柄的调用。
ProcessMove
protected virtual void ProcessMove(PointerEventData pointerEvent)
{
var targetGO = pointerEvent.pointerCurrentRaycast.gameObject;
HandlePointerExitAndEnter(pointerEvent, targetGO);
}
每帧都会直接调用处理句柄。
TouchInputModule 的核心函数
/// <summary>
/// Process all touch events.
/// 处理所有触屏事件
/// </summary>
private void ProcessTouchEvents()
{
for (int i = 0; i < Input.touchCount; ++i)
{
Touch input = Input.GetTouch(i);
bool released;
bool pressed;
var pointer = GetTouchPointerEventData(input, out pressed, out released);
ProcessTouchPress(pointer, pressed, released);
if (!released)
{
ProcessMove(pointer);
ProcessDrag(pointer);
}
else
RemovePointerData(pointer);
}
}
ProcessMove() 和 ProcessDrag() 与前面鼠标事件处理是一样的,只是按下的事件处理不同,而且它对每个触点都做了相同的操作处理。其实 ProcessTouchPress() 和鼠标按下处理函数 ProcessMousePress() 基本上一模一样,只是传入时的数据类型不同而已。
ExecuteEvents.ExecuteHierarchy
private static readonly List<Transform> s_InternalTransformList = new List<Transform>(30);
public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler
{
// 获取物体的所有父节点,包括它自己
GetEventChain(root, s_InternalTransformList);
for (var i = 0; i < s_InternalTransformList.Count; i++)
{
var transform = s_InternalTransformList[i];
// 对每个父节点包括自己依次执行句柄响应
if (Execute(transform.gameObject, eventData, callbackFunction))
return transform.gameObject;
}
return null;
}
上述代码对所有父节点都调用句柄函数。也就是说,当前节点的事件会通知给其上面的父节点。
射线碰撞检测模块源码
主要工作:从摄像机的屏幕位置上进行射线碰撞检测并获取碰撞结果,把结果返回给事件处理逻辑类,交由事件处理模块处理。
主要包含三个类,分别作用于 2D射线碰撞检测、3D射线碰撞检测和 GraphicRaycaster 图形射线碰撞检测。
2D、3D射线碰撞测试用射线的形式做碰撞测试,区别在于 2D 射线碰撞结果里预留了 2D 的层级次序,以便在后面的碰撞结果排序时,以这个层级次序为依据进行排序,而 3D 射线碰撞检测结果则是以距离大小为依据排序的。
GraphicRaycaster 为 UGUI 元素点位检测的类,它被放在了 Core 渲染块里。它主要针对 Screen Space-Overlay 模式下输入点位进行碰撞检测,因为这个模式下的检测并不依赖于射线碰撞,而是遍历所有可点击的 UGUI 元素来检测比较,从而判断是该响应哪个UI元素。因此 GraphicRaycaster 是比较特殊的。
GraphicRaycaster 的核心源码
/// <summary>
/// Perform a raycast into the screen and collect all graphics underneath it.
/// </summary>
[NonSerialized] static readonly List<Graphic> s_SortedGraphics = new List<Graphic>();
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, List<Graphic> results)
{
// Debug.Log("ttt" + pointerPoision + ":::" + camera);
// Necessary for the event system
var foundGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
for (int i = 0; i < foundGraphics.Count; ++i)
{
Graphic graphic = foundGraphics[i];
// -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
if (graphic.depth == -1 || !graphic.raycastTarget)
continue;
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
continue;
if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
}
}
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
// StringBuilder cast = new StringBuilder();
for (int i = 0; i < s_SortedGraphics.Count; ++i)
results.Add(s_SortedGraphics[i]);
// Debug.Log (cast.ToString());
s_SortedGraphics.Clear();
}
GraphicRaycaster() 对每个可以点击的元素(raycastTarget是否为true,并且 depth 不为 -1,为可点击元素)进行计算,判断点位是否落在该元素上。再通过 depth 变量排序,判断最先该落在哪个元素上,从而确定哪个元素响应输入事件。
所有检测碰撞的结果数据结构均为 RaycastResult 类,它承载了所有碰撞检测结果,包括了距离、世界点位、屏幕点位、2D层级次序和碰撞物体等,为后面事件处理提供数据上的依据。
事件逻辑处理模块
- 事件主逻辑处理模块,主要的逻辑都集中在 EventSystem 类中,其余的类都是对它起辅助作用的。
- EventInterfaces、EventTrigger、EventTriggerType 定义了事件回调函数。
- ExecuteEvents 编写了所有执行事件的回调接口。
- EventSystem 只有300行代码,基本上都在处理由射线碰撞检测后引起的各类事件。比如判断事件是否成立,若成立,则发起事件回调,若不成立,则继续轮询检查,等待事件的发生。
EventSystem 是事件处理模块中唯一继承 MonoBehavior 并且有在 Update 帧循环中做轮询的。也就是说,所有 UI 事件的发生都是通过 EventSystem 轮询监测并且实施的。EventSystem 通过调用输入事件检测模块、检测碰撞模块来形成自己主逻辑部分。因此可以说 EventSystem 是主逻辑类,是整个事件模块的入口。
架构者在设计时将整个事件层各自的职能拆分的很清楚,使得源码看起来并没有那么难。输入监测由输入事件捕捉模块完成,碰撞检测由碰撞检测模块完成,事件的数据类都有各自的定义,EventSystem 主要作用是把这些模块拼装起来成为主逻辑块。