为了方便理解UGUI的整个事件系统,这里以Button控件的Click操作为例,详细介绍整个事件触发的流程,包括如下内容:
- UGUI的EventData
- UGUI如何监听用户的输入
- UGUI如何处理和管理输入的数据
- UGUI如何知道用户点击了哪些控件
- UGUI的EventSystemHandler
- UGUI控件如何监听用户的点击
先给出涉及到的脚本类图,以方便理解代码
1.0 UGUI的EventData
EventData主要分三类来存储事件信息,对应具体的EventSystemHandler
BaseEventData:基础的事件信息
PointerEventData: 存储 触摸/点击/鼠标操作 事件信息
AxisEventData:数据轴移动相关的事件信息,注:拖动的操作属于PointerEventData
相关继承关系可以看上文的类图。
2.0 UGUI如何监听用户的输入
通过查看UGUI的源码BaseInput.cs,我们可以发现BaseInput实际上是对Unity的Input的进一步封装,以方便UGUI系统的使用,BaseInput包含了对键盘,鼠标,Touch,方向轴的检测,内部都是调用Unity的Input类。
例如:对于检测鼠标的按下,与抬起事件,BaseInput.cs部分源码如下:
///
/// Interface to Input.GetMouseButtonDown. Can be overridden to provide custom input instead of using the Input class.
///
///
///
public virtual bool GetMouseButtonDown(int button)
{
return Input.GetMouseButtonDown(button);
}
///
/// Interface to Input.GetMouseButtonUp. Can be overridden to provide custom input instead of using the Input class.
///
public virtual bool GetMouseButtonUp(int button)
{
return Input.GetMouseButtonUp(button);
}
///
/// Interface to Input.GetMouseButton. Can be overridden to provide custom input instead of using the Input class.
///
public virtual bool GetMouseButton(int button)
{
return Input.GetMouseButton(button);
}
3.0 UGUI如何处理和管理输入的数据
通过上文我们已经可以获取到用户的输入,那UGUI获取到输入的数据,是如何处理和管理的呢?
这里涉及UGUI几个重要的脚本
- BaseInputModule.cs,PointerInputModule.cs,StandaloneInputModule.cs(单点触控),
TouchInputModule.cs(多点触控,该模块已经整合进Standalone中):InputModule 管理更新EventData 判断当前的操作事件,并通知具体的EventSystemHandler 进行逻辑处理。
相关继承关系可以看上文的类图。 - EventSystem.cs:管理所有的输入检测模块(InputModule)并帧调用Module的执行(Process),调动射线模块(Raycasters),为InputModule提供RaycastResult。
3.1源码分析
- 在InputModule的OnEnable和OnDisable方法中,会更新EventSystem的InputModule。
BaseInputModule.cs部分源码如下:
protected override void OnEnable()
{
base.OnEnable();
m_EventSystem = GetComponent();
m_EventSystem.UpdateModules();
}
protected override void OnDisable()
{
m_EventSystem.UpdateModules();
base.OnDisable();
}
- EventSystem中,获取当前最新可用的InputModule模块,然后在Update中帧调用InputModule的Process方法。
EventSystem.cs部分源码如下:
private List m_SystemInputModules = new List();
public void UpdateModules()
{
GetComponents(m_SystemInputModules);
for (int i = m_SystemInputModules.Count - 1; i >= 0; i--)
{
if (m_SystemInputModules[i] && m_SystemInputModules[i].IsActive())
continue;
m_SystemInputModules.RemoveAt(i);
}
}
protected virtual void Update()
{
if (current != this)
return;
TickModules();
bool changedModule = false;
for (var i = 0; i < m_SystemInputModules.Count; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported() && module.ShouldActivateModule())
{
if (m_CurrentInputModule != module)
{
ChangeEventModule(module);
changedModule = true;
}
break;
}
}
// no event module set... set the first valid one...
if (m_CurrentInputModule == null)
{
for (var i = 0; i < m_SystemInputModules.Count; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported())
{
ChangeEventModule(module);
changedModule = true;
break;
}
}
}
if (!changedModule && m_CurrentInputModule != null)
m_CurrentInputModule.Process();
}
- InputModule在Process()中会处理各种相关事件,由于源码较多,这里只介绍与Button的Click相关事件,其他事件相似,可自行查看。ProcessMouseEvent()会处理所有鼠标事件,ProcessTouchEvents()会处理所有触点事件。
StandaloneInputModule.cs部分源码如下:
public override void Process()
{
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
return;
bool usedEvent = SendUpdateEventToSelectedObject();
if (eventSystem.sendNavigationEvents)
{
if (!usedEvent)
usedEvent |= SendMoveEventToSelectedObject();
if (!usedEvent)
SendSubmitEventToSelectedObject();
}
// touch needs to take precedence because of the mouse emulation layer
if (!ProcessTouchEvents() && input.mousePresent)
ProcessMouseEvent();
}
- ProcessMouseEvent()中处理鼠标按键事件,包括Mouse Press,Move,Drag。
///
/// Process all mouse events.
///
protected void ProcessMouseEvent(int id)
{
var mouseData = GetMousePointerEventData(id);
var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;
m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;
// 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(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
}
}
- ProcessMousePress()中根据当前按键数据是否被按下或者释放,通过ExecuteEvents.Execute()方法响应目标UI控件的IEventSystemHandler接口,至此整个事件流程走通。
///
/// Calculate and process any mouse button state changes.
///
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);
// search for the control that will receive the press
// if we can't find a press handler set the press
// handler to be what would receive a click.
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
// didnt find a press handler... search for a click handler
if (newPressed == null)
newPressed = ExecuteEvents.GetEventHandler(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(currentOverGo);
if (pointerEvent.pointerDrag != null)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
m_InputPointerEvent = pointerEvent;
}
// 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);
// see if we mouse up on the same element that we clicked on...
var pointerUpHandler = ExecuteEvents.GetEventHandler(currentOverGo);
// PointerClick and Drop events
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;
// redo pointer enter / exit to refresh state
// so that if we moused over somethign that ignored it before
// due to having pressed on something else
// it now gets it.
if (currentOverGo != pointerEvent.pointerEnter)
{
HandlePointerExitAndEnter(pointerEvent, null);
HandlePointerExitAndEnter(pointerEvent, currentOverGo);
}
m_InputPointerEvent = pointerEvent;
}
}
- 回过头来,mouseData中保存着当前帧鼠标按键的数据(包括左键,中键,右键),我们来看一下GetMousePointerEventData(id)是如何返回鼠标数据mouseData的。
GetMousePointerEventData()方法会实时的将鼠标的数据信息(mousePosition,mouseScrollDelta等)保存到PointerEventData对象中,进一步赋值给mouseData。
PointerInputModule.cs源码如下:
///
/// Return the current MouseState.
///
protected virtual MouseState GetMousePointerEventData(int id)
{
// Populate the left button...
PointerEventData leftData;
var created = GetPointerData(kMouseLeftId, out leftData, true);
leftData.Reset();
if (created)
leftData.position = input.mousePosition;
Vector2 pos = input.mousePosition;
if (Cursor.lockState == CursorLockMode.Locked)
{
// We don't want to do ANY cursor-based interaction when the mouse is locked
leftData.position = new Vector2(-1.0f, -1.0f);
leftData.delta = Vector2.zero;
}
else
{
leftData.delta = pos - leftData.position;
leftData.position = pos;
}
leftData.scrollDelta = input.mouseScrollDelta;
leftData.button = PointerEventData.InputButton.Left;
eventSystem.RaycastAll(leftData, m_RaycastResultCache);
var raycast = FindFirstRaycast(m_RaycastResultCache);
leftData.pointerCurrentRaycast = raycast;
m_RaycastResultCache.Clear();
// copy the apropriate data into right and middle slots
PointerEventData rightData;
GetPointerData(kMouseRightId, out rightData, true);
CopyFromTo(leftData, rightData);
rightData.button = PointerEventData.InputButton.Right;
PointerEventData middleData;
GetPointerData(kMouseMiddleId, out middleData, true);
CopyFromTo(leftData, middleData);
middleData.button = PointerEventData.InputButton.Middle;
m_MouseState.SetButtonState(PointerEventData.InputButton.Left, StateForMouseButton(0), leftData);
m_MouseState.SetButtonState(PointerEventData.InputButton.Right, StateForMouseButton(1), rightData);
m_MouseState.SetButtonState(PointerEventData.InputButton.Middle, StateForMouseButton(2), middleData);
return m_MouseState;
}
注意这行代码,下文会说到,与射线检测有关
eventSystem.RaycastAll(leftData, m_RaycastResultCache);
那按键状态呢?是通过StateForMouseButton()获取
///
/// Given a mouse button return the current state for the frame.
///
/// Mouse button ID
protected PointerEventData.FramePressState StateForMouseButton(int buttonId)
{
var pressed = input.GetMouseButtonDown(buttonId);
var released = input.GetMouseButtonUp(buttonId);
if (pressed && released)
return PointerEventData.FramePressState.PressedAndReleased;
if (pressed)
return PointerEventData.FramePressState.Pressed;
if (released)
return PointerEventData.FramePressState.Released;
return PointerEventData.FramePressState.NotChanged;
}
总结: InputModule被EventSystem帧调用,检查BaseInput中各项数值 => 判断当前操作状态,更新/创建 PointerEventData并以当前操作状态进行数据重组 => MouseButton,然后根据ProcessMousePress、ProcessMove、ProcessDrag 检测各个MouseButton中ButtonState下的PointerEventData,满足条件则执行相应的事件(ExecuteEvents)。
4.0UGUI如何知道用户点击了哪些控件
我们现在监听了鼠标操作,保存了输入数据,然后处理数据,并且根据鼠标操作状态通过ExecuteEvents.Execute()相应相关事件,那么ExecuteEvents.Execute()又是如何让UI响应事件的?又有哪些UI会响应相关的事件呢?相关的事件又有哪些呢?
4.1源码分析
4.1.1 相关脚本
下文将一一解答这些问题,我们先来看看UGUI如何知道用户点击了哪些控件的,这里涉及如下几个脚本。
- ReflectionMethodsCache.cs:以反射的形式,来访问Unity引擎内部的2D,3D射线接口,供BaseRaycaster使用
ReflectionMethodsCache.cs部分源码如下:
internal class ReflectionMethodsCache
{
public delegate bool Raycast3DCallback(Ray r, out RaycastHit hit, float f, int i);
public delegate RaycastHit2D Raycast2DCallback(Vector2 p1, Vector2 p2, float f, int i);
public delegate RaycastHit[] RaycastAllCallback(Ray r, float f, int i);
public delegate RaycastHit2D[] GetRayIntersectionAllCallback(Ray r, float f, int i);
public delegate int GetRayIntersectionAllNonAllocCallback(Ray r, RaycastHit2D[] results, float f, int i);
public delegate int GetRaycastNonAllocCallback(Ray r, RaycastHit[] results, float f, int i);
// We call Physics.Raycast and Physics2D.Raycast through reflection to avoid creating a hard dependency from
// this class to the Physics/Physics2D modules, which would otherwise make it impossible to make content with UI
// without force-including both modules.
public ReflectionMethodsCache()
{
var raycast3DMethodInfo = typeof(Physics).GetMethod("Raycast", new[] {typeof(Ray), typeof(RaycastHit).MakeByRefType(), typeof(float), typeof(int)});
if (raycast3DMethodInfo != null)
raycast3D = (Raycast3DCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(Raycast3DCallback), raycast3DMethodInfo);
var raycast2DMethodInfo = typeof(Physics2D).GetMethod("Raycast", new[] {typeof(Vector2), typeof(Vector2), typeof(float), typeof(int)});
if (raycast2DMethodInfo != null)
raycast2D = (Raycast2DCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(Raycast2DCallback), raycast2DMethodInfo);
var raycastAllMethodInfo = typeof(Physics).GetMethod("RaycastAll", new[] {typeof(Ray), typeof(float), typeof(int)});
if (raycastAllMethodInfo != null)
raycast3DAll = (RaycastAllCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(RaycastAllCallback), raycastAllMethodInfo);
var getRayIntersectionAllMethodInfo = typeof(Physics2D).GetMethod("GetRayIntersectionAll", new[] {typeof(Ray), typeof(float), typeof(int)});
if (getRayIntersectionAllMethodInfo != null)
getRayIntersectionAll = (GetRayIntersectionAllCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(GetRayIntersectionAllCallback), getRayIntersectionAllMethodInfo);
var getRayIntersectionAllNonAllocMethodInfo = typeof(Physics2D).GetMethod("GetRayIntersectionNonAlloc", new[] { typeof(Ray), typeof(RaycastHit2D[]), typeof(float), typeof(int) });
if (getRayIntersectionAllNonAllocMethodInfo != null)
getRayIntersectionAllNonAlloc = (GetRayIntersectionAllNonAllocCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(GetRayIntersectionAllNonAllocCallback), getRayIntersectionAllNonAllocMethodInfo);
var getRaycastAllNonAllocMethodInfo = typeof(Physics).GetMethod("RaycastNonAlloc", new[] { typeof(Ray), typeof(RaycastHit[]), typeof(float), typeof(int) });
if (getRaycastAllNonAllocMethodInfo != null)
getRaycastNonAlloc = (GetRaycastNonAllocCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(GetRaycastNonAllocCallback), getRaycastAllNonAllocMethodInfo);
}
}
- BaseRaycaster.cs,GraphicRaycaster.cs,Physics2DRaycaster.cs,PhysicsRaycaster:分别是射线检测基类,图形检测,2D物体检测,3D物体检测。
相关继承关系可以看上文的类图。
BaseRaycaster在OnEnable()与OnDisable()会将当前射线检测模块传入RaycasterManager进行管理。
BaseRaycaster.cs部分源码如下:
protected override void OnEnable()
{
base.OnEnable();
RaycasterManager.AddRaycaster(this);
}
protected override void OnDisable()
{
RaycasterManager.RemoveRaycasters(this);
base.OnDisable();
}
- RaycasterManager.cs:管理所有的射线检测模块,供EventSystem使用。
RaycasterManager.cs源码如下:
internal static class RaycasterManager
{
private static readonly List s_Raycasters = new List();
public static void AddRaycaster(BaseRaycaster baseRaycaster)
{
if (s_Raycasters.Contains(baseRaycaster))
return;
s_Raycasters.Add(baseRaycaster);
}
public static List GetRaycasters()
{
return s_Raycasters;
}
public static void RemoveRaycasters(BaseRaycaster baseRaycaster)
{
if (!s_Raycasters.Contains(baseRaycaster))
return;
s_Raycasters.Remove(baseRaycaster);
}
}
- RaycastResult.cs:射线检测的结果数据结构,包含如下数据结构。
RaycastResult.cs部分源码如下:
public struct RaycastResult
{
private GameObject m_GameObject; // Game object hit by the raycast
///
/// The GameObject that was hit by the raycast.
///
public GameObject gameObject
{
get { return m_GameObject; }
set { m_GameObject = value; }
}
///
/// BaseInputModule that raised the hit.
///
public BaseRaycaster module;
///
/// Distance to the hit.
///
public float distance;
///
/// Hit index
///
public float index;
///
/// Used by raycasters where elements may have the same unit distance, but have specific ordering.
///
public int depth;
///
/// The SortingLayer of the hit object.
///
///
/// For UI.Graphic elements this will be the values from that graphic's Canvas
/// For 3D objects this will always be 0.
/// For 2D objects if a SpriteRenderer is attached to the same object as the hit collider that SpriteRenderer sortingLayerID will be used.
///
public int sortingLayer;
///
/// The SortingOrder for the hit object.
///
///
/// For Graphic elements this will be the values from that graphics Canvas
/// For 3D objects this will always be 0.
/// For 2D objects if a SpriteRenderer is attached to the same object as the hit collider that SpriteRenderer sortingOrder will be used.
///
public int sortingOrder;
///
/// The world position of the where the raycast has hit.
///
public Vector3 worldPosition;
///
/// The normal at the hit location of the raycast.
///
public Vector3 worldNormal;
///
/// The screen position from which the raycast was generated.
///
public Vector2 screenPosition;
///
/// Is there an associated module and a hit GameObject.
///
public bool isValid
{
get { return module != null && gameObject != null; }
}
}
4.1.2检测流程
还记得上文提到的eventSystem.RaycastAll(leftData, m_RaycastResultCache);这行代码吗?没错,这个就是检测UI被点击的入口。
- InputModule检测到了点击/触摸,向eventSystem.RaycastAll()请求数据,EventSystem通过RaycasterManager.GetRaycasters()获取射线检测模块,启动射线获取数据,获取检测结果通过RaycastComparer按照一定的规则进行排序。
EventSystem.cs部分源码如下:
private static int RaycastComparer(RaycastResult lhs, RaycastResult rhs)
{
if (lhs.module != rhs.module)
{
var lhsEventCamera = lhs.module.eventCamera;
var rhsEventCamera = rhs.module.eventCamera;
if (lhsEventCamera != null && rhsEventCamera != null && lhsEventCamera.depth != rhsEventCamera.depth)
{
// need to reverse the standard compareTo
if (lhsEventCamera.depth < rhsEventCamera.depth)
return 1;
if (lhsEventCamera.depth == rhsEventCamera.depth)
return 0;
return -1;
}
if (lhs.module.sortOrderPriority != rhs.module.sortOrderPriority)
return rhs.module.sortOrderPriority.CompareTo(lhs.module.sortOrderPriority);
if (lhs.module.renderOrderPriority != rhs.module.renderOrderPriority)
return rhs.module.renderOrderPriority.CompareTo(lhs.module.renderOrderPriority);
}
if (lhs.sortingLayer != rhs.sortingLayer)
{
// Uses the layer value to properly compare the relative order of the layers.
var rid = SortingLayer.GetLayerValueFromID(rhs.sortingLayer);
var lid = SortingLayer.GetLayerValueFromID(lhs.sortingLayer);
return rid.CompareTo(lid);
}
if (lhs.sortingOrder != rhs.sortingOrder)
return rhs.sortingOrder.CompareTo(lhs.sortingOrder);
if (lhs.depth != rhs.depth)
return rhs.depth.CompareTo(lhs.depth);
if (lhs.distance != rhs.distance)
return lhs.distance.CompareTo(rhs.distance);
return lhs.index.CompareTo(rhs.index);
}
private static readonly Comparison s_RaycastComparer = RaycastComparer;
///
/// Raycast into the scene using all configured BaseRaycasters.
///
/// Current pointer data.
/// List of 'hits' to populate.
public void RaycastAll(PointerEventData eventData, List raycastResults)
{
raycastResults.Clear();
var modules = RaycasterManager.GetRaycasters();
for (int i = 0; i < modules.Count; ++i)
{
var module = modules[i];
if (module == null || !module.IsActive())
continue;
module.Raycast(eventData, raycastResults);
}
raycastResults.Sort(s_RaycastComparer);
}
- 在PointerInputModule中获取检测的第一个结果,保存到PointerEventData中,在StandaloneInputModule中将PointerEventData通过ExecuteEvents.Execute()发送到具体的目标UI控件,对于EventSystemHandler,下文有详细介绍。
PointerInputModule.cs的GetMousePointerEventData()方法部分源码如下:
var raycast = FindFirstRaycast(m_RaycastResultCache);
leftData.pointerCurrentRaycast = raycast;
StandaloneInputModule.cs的ProcessMousePress()方法部分源码如下:
if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
{
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
}
- 这里我们来详细看下GraphicRaycaster.cs,首先根据GraphicRaycaster组件的
BlockingObjects
和BlockingMask
属性设置,对检测的对象进行过滤。
GraphicRaycaster.cs部分源码如下:
if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
{
float distanceToClipPlane = 100.0f;
if (currentEventCamera != null)
{
float projectionDirection = ray.direction.z;
distanceToClipPlane = Mathf.Approximately(0.0f, projectionDirection)
? Mathf.Infinity
: Mathf.Abs((currentEventCamera.farClipPlane - currentEventCamera.nearClipPlane) / projectionDirection);
}
if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All)
{
if (ReflectionMethodsCache.Singleton.raycast3D != null)
{
var hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, (int)m_BlockingMask);
if (hits.Length > 0)
hitDistance = hits[0].distance;
}
}
if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All)
{
if (ReflectionMethodsCache.Singleton.raycast2D != null)
{
var hits = ReflectionMethodsCache.Singleton.getRayIntersectionAll(ray, distanceToClipPlane, (int)m_BlockingMask);
if (hits.Length > 0)
hitDistance = hits[0].distance;
}
}
}
- Raycast()方法,根据当前的Canvas,EventCamera,鼠标事件数据,Canvas下所有的UI元素,将检测到Graphic保存到m_RaycastResults
[NonSerialized] private List m_RaycastResults = new List();
var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
- 遍历检测到Graphic对象,如果GraphicRaycaster组件的
IgnoreReversedGraphics
为true,过滤掉与相机反向的Graphic对象。
int totalCount = m_RaycastResults.Count;
for (var index = 0; index < totalCount; index++)
{
var go = m_RaycastResults[index].gameObject;
bool appendGraphic = true;
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;
}
}
- 计算Graphic与EventCamera距离,过滤不在相机范围内的Graphic对象。
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 - ray.origin) / Vector3.Dot(transForward, ray.direction));
// Check to see if the go is behind the camera.
if (distance < 0)
continue;
}
if (distance >= hitDistance)
continue;
- 最终将检测的结果保存到RaycastResult对象中。
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);
}
}
- 我们再来看看Raycast方法(),遍历当前Canvas下所有的Graphic对象,然后使用!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera)方法,过滤出所有在当前鼠标位置内的并且位置z小于等于eventCamera.farClipPlane的Graphic对象,然后按照Graphic.depth,从大到小排序,保存到s_SortedGraphics列表中。
从这里也可以看出,GraphicRaycaster实际上并不是使用射线进行检测鼠标是否点击UI上的,而Physics2DRaycaster与PhysicsRaycaster是使用射线检测的,有兴趣的话,可以看下源码。
///
/// Perform a raycast into the screen and collect all graphics underneath it.
///
[NonSerialized] static readonly List s_SortedGraphics = new List();
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList foundGraphics, List results)
{
// Debug.Log("ttt" + pointerPoision + ":::" + camera);
// Necessary for the event system
int totalCount = foundGraphics.Count;
for (int i = 0; i < totalCount; ++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 || graphic.canvasRenderer.cull)
continue;
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
continue;
if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
continue;
if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
}
}
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
// StringBuilder cast = new StringBuilder();
totalCount = s_SortedGraphics.Count;
for (int i = 0; i < totalCount; ++i)
results.Add(s_SortedGraphics[i]);
// Debug.Log (cast.ToString());
s_SortedGraphics.Clear();
}
- 在上文的代码中,在排序最终Graphic前,还会调用graphic.Raycast(pointerPosition, eventCamera)方法,对Graphic自身进行检测过滤。
获取自身的所有的ICanvasRaycastFilter对象,进行射线检测过滤。
Graphic.cs部分源码如下:
///
/// When a GraphicRaycaster is raycasting into the scene it does two things. First it filters the elements using their RectTransform rect. Then it uses this Raycast function to determine the elements hit by the raycast.
///
/// Screen point being tested
/// Camera that is being used for the testing.
/// True if the provided point is a valid location for GraphicRaycaster raycasts.
public virtual bool Raycast(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled)
return false;
var t = transform;
var components = ListPool.Get();
bool ignoreParentGroups = false;
bool continueTraversal = true;
while (t != null)
{
t.GetComponents(components);
for (var i = 0; i < components.Count; i++)
{
var canvas = components[i] as Canvas;
if (canvas != null && canvas.overrideSorting)
continueTraversal = false;
var filter = components[i] as ICanvasRaycastFilter;
if (filter == null)
continue;
var raycastValid = true;
var group = components[i] as CanvasGroup;
if (group != null)
{
if (ignoreParentGroups == false && group.ignoreParentGroups)
{
ignoreParentGroups = true;
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
else if (!ignoreParentGroups)
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
else
{
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
if (!raycastValid)
{
ListPool.Release(components);
return false;
}
}
t = continueTraversal ? t.parent : null;
}
ListPool.Release(components);
return true;
}
- CanvasGroup实现了ICanvasRaycastFilter接口,这也是为什么CanvasGroup可以截取点击事件的原因,遗憾的是,CanvasGroup.cs并没有开源,反编译我们只能看到如下源码。
CanvasGroup.cs部分源码如下:
[NativeClass("UI::CanvasGroup")]
[NativeHeader("Runtime/UI/CanvasGroup.h")]
public sealed class CanvasGroup : Behaviour, ICanvasRaycastFilter
{
public CanvasGroup();
[NativeProperty("Alpha", false, TargetType.Function)]
public float alpha { get; set; }
[NativeProperty("Interactable", false, TargetType.Function)]
public bool interactable { get; set; }
[NativeProperty("BlocksRaycasts", false, TargetType.Function)]
public bool blocksRaycasts { get; set; }
[NativeProperty("IgnoreParentGroups", false, TargetType.Function)]
public bool ignoreParentGroups { get; set; }
public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera);
}
- 除此之外,Image也实现实现了ICanvasRaycastFilter接口,在这里return activeSprite.texture.GetPixelBilinear(x, y).a >= alphaHitTestMinimumThreshold;我们可以通过修改alphaHitTestMinimumThreshold值,来简单的实现不规则透明图片的点击,前提需要将图片设置中的Read/Write Enabled设置为true。
Image.cs部分源码如下:
public class Image : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
{
///
/// Calculate if the ray location for this image is a valid hit location. Takes into account a Alpha test threshold.
///
/// The screen point to check against
/// The camera in which to use to calculate the coordinating position
/// If the location is a valid hit or not.
/// Also see See:ICanvasRaycastFilter.
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
if (alphaHitTestMinimumThreshold <= 0)
return true;
if (alphaHitTestMinimumThreshold > 1)
return false;
if (activeSprite == null)
return true;
Vector2 local;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local))
return false;
Rect rect = GetPixelAdjustedRect();
// Convert to have lower left corner as reference point.
local.x += rectTransform.pivot.x * rect.width;
local.y += rectTransform.pivot.y * rect.height;
local = MapCoordinate(local, rect);
// Normalize local coordinates.
Rect spriteRect = activeSprite.textureRect;
Vector2 normalized = new Vector2(local.x / spriteRect.width, local.y / spriteRect.height);
// Convert to texture space.
float x = Mathf.Lerp(spriteRect.x, spriteRect.xMax, normalized.x) / activeSprite.texture.width;
float y = Mathf.Lerp(spriteRect.y, spriteRect.yMax, normalized.y) / activeSprite.texture.height;
try
{
return activeSprite.texture.GetPixelBilinear(x, y).a >= alphaHitTestMinimumThreshold;
}
catch (UnityException e)
{
Debug.LogError("Using alphaHitTestMinimumThreshold greater than 0 on Image whose sprite texture cannot be read. " + e.Message + " Also make sure to disable sprite packing for this sprite.", this);
return true;
}
}
}
5.0 UGUI的EventSystemHandler
UGUI所有的事件接口均继承自 IEventSystemHandler
IPointerEnterHandler:鼠标进入事件
IPointerExitHandler:鼠标退出事件
IPointerDownHandler:鼠标按下事件:在可视元素矩形区域内按下时响应
IPointerClickHandler:鼠标点击事件:在可视元素矩形区域内按下后,且在同一可视元素矩形区域内抬起时响应
IPointerUpHandler:鼠标抬起事件:在可视元素矩形区域内按下后,在任意地方抬起时响应
IScrollHandler:鼠标滚动事件
IInitializePotentialDragHandler:初始化潜在的拖动事件,在IBeginDragHandler之前触发
IBeginDragHandler:拖动开始事件
IDragHandler:拖动中事件
IDropHandler:拖动释放事件
IEndDragHandler:拖动结束事件
ISelectHandler:选中事件:选中(点击)一个Selectable对象时调用
IUpdateSelectedHandler:选中一个Selectable对象每帧触发事件
IDeselectHandler:取消选中事件:选中另一gameObject后,原来选中的自动取消
IMoveHandler:移动事件(上下左右):与InputManager里的Horizontal和Vertical按键相对应=>Input.GetAxisRaw()
ISubmitHandler:提交事件:按下InputManager里的submit对应的按键(Enter)=> Input.GetButtonDown()
ICancelHandler:取消事件:按下InputManager里的cancel对应的按键(Esc)=> Input.GetButtonDown()
移动、提交、取消操作,必须先选中一个Selectable对象才能触发
所有被继承的事件接口,需要实现其方法,参数一般是EventData(BaseEventData/PointerEventData),利用这些参数可以完成相应操作。
6.0 UGUI控件如何监听用户的点击
- Button实现了IPointerClickHandler接口,在回调中里调用Press()方法,触发onClick事件。
Button.cs部分源码如下:
public class Button : Selectable, IPointerClickHandler, ISubmitHandler
{
[Serializable]
public class ButtonClickedEvent : UnityEvent
{
}
// Event delegates triggered on click.
[FormerlySerializedAs("onClick")] [SerializeField]
private ButtonClickedEvent m_OnClick = new ButtonClickedEvent();
protected Button()
{
}
public ButtonClickedEvent onClick
{
get { return m_OnClick; }
set { m_OnClick = value; }
}
private void Press()
{
if (!IsActive() || !IsInteractable())
return;
UISystemProfilerApi.AddMarker("Button.onClick", this);
m_OnClick.Invoke();
}
public virtual void OnPointerClick(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
Press();
}
}
- 我们来看看接口IPointerClickHandler什么时候被调用?在上文的
3.0 UGUI如何处理和管理输入的数据
节中可以找到如下代码,当鼠标按下时,执行ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);那这个ExecuteEvents.Execute()又是什么呢?
StandaloneInputModule.cs部分源码如下:
protected void ProcessMousePress(MouseButtonEventData data)
{
var pointerEvent = data.buttonData;
var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
// PointerDown notification
if (data.PressedThisFrame())
{
//省略...
}
// 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);
// see if we mouse up on the same element that we clicked on...
var pointerUpHandler = ExecuteEvents.GetEventHandler(currentOverGo);
// PointerClick and Drop events
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);
}
//省略...
}
}
- 通过ExecuteEvents.cs源码我们可以看到Execute执行过程:
- GetEventList
(target, internalHandlers); 获取物体上所有包含IEventSystemHandler且可用的组件 - arg = (T)internalHandlers[i]; 找到具体T类型组件
- 调用functor(arg, eventData)回调;
- 触发handler.OnPointerClick(ValidateEventData
(eventData));
ExecuteEvents.cs部分源码如下:
public static class ExecuteEvents
{
public delegate void EventFunction(T1 handler, BaseEventData eventData);
public static T ValidateEventData(BaseEventData data) where T : class
{
if ((data as T) == null)
throw new ArgumentException(String.Format("Invalid type: {0} passed to event expecting {1}", data.GetType(), typeof(T)));
return data as T;
}
private static readonly EventFunction s_PointerClickHandler = Execute;
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{
handler.OnPointerClick(ValidateEventData(eventData));
}
public static EventFunction pointerClickHandler
{
get { return s_PointerClickHandler; }
}
private static readonly ObjectPool> s_HandlerListPool = new ObjectPool>(null, l => l.Clear());
public static bool Execute(GameObject target, BaseEventData eventData, EventFunction functor) where T : IEventSystemHandler
{
var internalHandlers = s_HandlerListPool.Get();
GetEventList(target, internalHandlers);
// if (s_InternalHandlers.Count > 0)
// Debug.Log("Executinng " + typeof (T) + " on " + target);
for (var i = 0; i < internalHandlers.Count; i++)
{
T arg;
try
{
arg = (T)internalHandlers[i];
}
catch (Exception e)
{
var temp = internalHandlers[i];
Debug.LogException(new Exception(string.Format("Type {0} expected {1} received.", typeof(T).Name, temp.GetType().Name), e));
continue;
}
try
{
functor(arg, eventData);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
var handlerCount = internalHandlers.Count;
s_HandlerListPool.Release(internalHandlers);
return handlerCount > 0;
}
private static void GetEventList(GameObject go, IList results) where T : IEventSystemHandler
{
// Debug.LogWarning("GetEventList<" + typeof(T).Name + ">");
if (results == null)
throw new ArgumentException("Results array is null", "results");
if (go == null || !go.activeInHierarchy)
return;
var components = ListPool.Get();
go.GetComponents(components);
for (var i = 0; i < components.Count; i++)
{
if (!ShouldSendToComponent(components[i]))
continue;
// Debug.Log(string.Format("{2} found! On {0}.{1}", go, s_GetComponentsScratch[i].GetType(), typeof(T)));
results.Add(components[i] as IEventSystemHandler);
}
ListPool.Release(components);
// Debug.LogWarning("end GetEventList<" + typeof(T).Name + ">");
}
}
终于写完了,好累ヽ( ̄▽ ̄)و!!!