01 | 从Button已经开始阐释UGUI源标识符
在Unity中,Button是他们最常见的模块众所周知了,它采用出来比较简单,比如窃听两个Button点选该事件
GetComponent<Button>().onClick.AddListener(() => {
…//按键点选的处置方法论
});
?让他们从源标识符中找寻标准答案。
翻查源标识符前的实用性
为的是能方便快捷的翻查源标识符和展开标识符增容,须要再次引入UGUI包。增建Unity工程项目,找出Project/Packages/Unity UI,滑鼠 Show in Explorer,将其引入到任一两个捷伊实用性文件中(读懂留存的边线,待会须要提及)。
接下去关上Window/Package Manager
找出Unity UI,将其Remove
接着点选“+”号,优先选择Add package form disk…,找出以后留存的UI包,步入产品目录后选上package.json,点选关上。
截叶,那时他们能翻查/修正UGUI的源标识符了。
探究UGUI源标识符
Button的调用链
通过F12关上Button标识符,容易发现它继承Selectable类,同时还继承了IPointerClickHandler、ISubmitHandler接口,这两个接口分别会在鼠标点选、点选提交按键时调用它们的回调函数。
public class Button : Selectable, IPointerClickHandler, ISubmitHandler
{
[Serializable]
//定义两个点选该事件
public class ButtonClickedEvent : UnityEvent {}
// 实例化两个ButtonClickedEvent的该事件
[FormerlySerializedAs(“onClick”)]
[SerializeField]
private ButtonClickedEvent m_OnClick = new ButtonClickedEvent();
protected Button()
{}
//常见的onClick.AddListener()就是窃听这个该事件
public ButtonClickedEvent onClick
{
get { return m_OnClick; }
set { m_OnClick = value; }
}
//如果按键处于活跃状态并且可交互(Interactable设置为true),则触发该事件
private void Press()
{
if (!IsActive() || !IsInteractable())
return;
UISystemProfilerApi.AddMarker(“Button.onClick”, this);
m_OnClick.Invoke();
}
//鼠标点选时调用该函数,继承自 IPointerClickHandler 接口
public virtual void OnPointerClick(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
Press();
}
//按下“提交”键后触发(须要先选上该游戏物体),继承自 ISubmitHandler
//”提交”键能在 Edit->Project Settings->Input->Submit 中自定义
public virtual void OnSubmit(BaseEventData eventData){…}
private IEnumerator OnFinishSubmit(){…}
}
IPointerClickHandler接口仅包含两个OnPointerClick()方法,当鼠标点选时会调用该接口的方法。而Button能触发点选该事件是因为继承自IPointerClickHandler接口,并且重写了OnPointerClick方法。
那IPointerClickHandler接口的方法又是被谁调用的呢?查找提及,发现是ExecuteEvents类的Execute方法(该类相当于该事件执行器,提供了许多通用的该事件处置方法),并且Execute方法赋值给s_PointerClickHandler字段。
private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{
handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}
为的是能看的更清楚,总结一下调用关系,即Button继承自Selectable、IPointercliClickHandler、ISubmitHandler,而IPointercliClickHandler、ISubmitHandler继承自IEventSystemHandler,ExecuteEvent会在鼠标松开时通过Execute函数调用IPointercliClickHandler、ISubmitHandler接口的方法,从而触发Button的onClick该事件,如下图所示
继续往上找,ExecuteEvents类中还定义了两个EventFunction<T1>的泛型委托和该委托类型的属性,这个返回s_PointerClickHandler,要查找谁触发的点选该事件,只须要找出谁调用了pointerClickHandler即可
public delegate void EventFunction<T1>(T1 handler, BaseEventData eventData);
public static EventFunction<IPointerClickHandler> pointerClickHandler
{
get { return s_PointerClickHandler; }
}
容易发现,StandaloneInputModule和TouchInputModule类对其有调用,这两个类继承自BaseInput,主要用以处置鼠标、键盘、控制器等设备的输入,EventSystem类会在Update中每帧检查可用的输入模块的状态是否发生变化,并调用TickModules()和当前输入模块(m_CurrentInputModule)的Process()函数(后面会展开讲解)。下面是StandaloneInputModule的部分标识符,它继承自BaseInputModule
// 计算和处置任何鼠标按键状态的变化
//Process函数间接对其展开调用(调用链过长,不一一展示)protected void ProcessMousePress(MouseButtonEventData data)
{
…//省略部分标识符
//鼠标按键抬起时调用(按键包括鼠标左键、中间滑轮和滑鼠) if (data.ReleasedThisFrame())
{
ReleaseMouse(pointerEvent, currentOverGo);
}
…
}
//满足松开鼠标的条件时调用
//currentOverGo :当前选上的游戏物体private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
{
…//省略部分标识符
if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
{
//执行Execute函数,传入ExecuteEvents.pointerClickHandler委托 ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
}
…
}
翻查ExecuteEvents.Execute的实现
上面已经翻查过Execute方法,为什么那时又出来两个?
因为ExecuteEvents中有N多个重载函数//target : 须要执行该事件的游戏对象
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
var internalHandlers = s_HandlerListPool.Get();
GetEventList<T>(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
{
//执行EventFunction<T>委托,比如pointerClickHandler(arg,eventData)
functor(arg, eventData);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
var handlerCount = internalHandlers.Count;
s_HandlerListPool.Release(internalHandlers);
return handlerCount > 0;
}
也就是说,EventSystem会在Update()中调用当前可用BaseInputModule的Process()方法,该方法会处置鼠标的按下、抬起等该事件,当鼠标抬起时调用ReleaseMouse()方法,并最终调用Execute()方法并触发IPointerClick该事件。 如下图所示(为的是简洁,类图并不完整)
ReleaseMouse()是否只有鼠标左键抬起才会触发?
鼠标左、中、滑鼠都会触发该函数,只不过Button在实现OnPointerClick()函数时忽略了鼠标中键和滑鼠,使得只有左键能触发Button的点选该事件但那时还存在两个问题,怎么知道上述标识符中该事件执行目标target的值呢?探究这个问题以后,他们须要先对UGUI源标识符有个总体的认识,因为它涉及的知识点比较多。
该事件系统整体概述
他们先看EventSystem源标识符在实用性文件中的分类
从图中就能看出主要包含三个子板块,分别是EvnetData、InputModules和Raycasters。
再看两个整体的类图,类图中包括了许多重要的类,如EventSystem、BaseRaycast、BaseInputModule等,它们都是继承自UIBehaviour,而UIBehaviour又是继承MonoBehaviour。(类图并不完整,只涉及部分类)
接下去对这些内容展开详细讲解。
EventSystem类
该事件系统主要是基于输入(键盘、鼠标、触摸或自定义输入)向应用程序中的对象发送该事件,当然这须要其他模块的配合。当你在GameObject中添加EventSystem时,你会发现它并没有太多的功能,这是因为EventSystem本身被设计成该事件系统不同模块之间通信的管理者和推动者,它主要包含以下功能:
管理哪个游戏对象被认为是选上的管理正在采用的输入模块管理射线检测(如果须要)根据须要更新所有输入模块管理输入模块
下面看一下具体标识符。首先是声明了BaseInputModule类型的List和变量,用来留存输入模块(Module)
//系统输入模块private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();
//当前输入模块
private BaseInputModule m_CurrentInputModule;
接下去,它会在Update中处置这些模块,调用TickModules方法,更新每两个模块,并且会在满足条件的情况下调用当前模块的Process方法
protected virtual void Update()
{
//遍历m_SystemInputModules,如果其中的Module不为null,则调用UpdateModule方法
TickModules();
//遍历m_SystemInputModules判断其中的输入模块是否支持当前平台
//如果支持并且能激活,则将其赋值给当前输入模块并Break
bool changedModule = false;
var systemInputModulesCount = m_SystemInputModules.Count;
for (var i = 0; i < systemInputModulesCount; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported() && module.ShouldActivateModule())
{
if (m_CurrentInputModule != module)
{
ChangeEventModule(module);
changedModule = true;
}
break;
}
}
//如果上面没找出符合条件的模块,则使用第两个支持当前平台的模块 if (m_CurrentInputModule == null)
{
for (var i = 0; i < systemInputModulesCount; i++)
{
var module = m_SystemInputModules[i];
if (module.IsModuleSupported())
{
ChangeEventModule(module);
changedModule = true;
break;
}
}
}
//如果当前模块没有发生变化并且当前模块不为空
if (!changedModule && m_CurrentInputModule != null)
m_CurrentInputModule.Process();
}
private void TickModules()
{
var systemInputModulesCount = m_SystemInputModules.Count;
for (var i = 0; i < systemInputModulesCount; i++)
{
if (m_SystemInputModules[i] != null)
m_SystemInputModules[i].UpdateModule();
}
}
Process()方法主要是将各种输入该事件(如点选、拖拽等该事件)传递给EventSystem当前选上的GameObject(即m_CurrentSelected)
管理选上的游戏对象
当场景中的游戏物体(Button、Dropdown、InputField等)被选上时,会通知以后选上的对象执行被取消(OnDeselect)该事件,通知当前选上的对象执行选上(OnSelect)该事件,部分标识符如下
public void SetSelectedGameObject(GameObject selected, BaseEventData pointer)
{
……//省略部分标识符
//通知以后被选上取消选上
ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.deselectHandler);
m_CurrentSelected = selected;
//通知当前物体被选上
ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.selectHandler);
m_SelectionGuard = false;
}
管理射线检测
备可用或触摸板被采用时调用。
public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
{
raycastResults.Clear();
var modules = RaycasterManager.GetRaycasters();
var modulesCount = modules.Count;
for (int i = 0; i < modulesCount; ++i)
{
var module = modules[i];
if (module == null || !module.IsActive())
continue;
//调用Raycast方法,
module.Raycast(eventData, raycastResults);
}
raycastResults.Sort(s_RaycastComparer);
}
,大部分情况都是根据深度(Depth)展开排序,在一些情况下也会采用距离(Distance)、排序顺序(SortingOrder,如果是UI元素则是根据Canvas面板的Sort order值,3D物体默认是0)或者排序层级(Sorting Layer)等作为排序依据。
讲了这么一大堆,来张图总结一下。EventSystem会在Update中调用输入模块的Process方法来处置输入消息,PointerInputModule会调用EventSystem中的RaycastAll方法展开射线检测,RaycastAll又会调用BastRaycaster的Raycast方法执行具体的射线检测操作,主要是
简单概括一下UML图的含义,比如实线+三角形表示继承,实线+箭头表示关联,虚线+箭头表示依赖,关联和依赖的区别主要是提及其他类作为成员变量代表的是关联关系,将其他类作为局部变量、方法参数,或者提及它的静态方法,就属于依赖关系。
InputModules
输入模块是实用性和定制该事件系统主方法论的地方。 自带的输入模块有两个,两个是为独立输入(StandaloneInputModule),另两个是为触摸输入(TouchInputModule)。 StandaloneInputModule是PC、Mac&Linux上的具体实现,而TouchInputModule是IOS、Android等移动平台上的具体实现,每个模块都按照给定实用性接收和分派该事件。 运行EventSystem后,它会翻查附加了哪些输入模块,并将该事件传递给特定的模块。 内置的输入模块旨在支持常见的游戏实用性,如触摸输入、控制器输入、键盘输入和鼠标输入等。
它的主要任务有三个,分别是
处置输入管理该事件状态发送该事件到场景对象在讲Button的时候他们提到鼠标的点选该事件是在BaseInputModule中触发的,除此之外,EventInterface接口中的其他该事件也都是由输入模块产生的,具体触发条件如下:
当鼠标或触摸步入、退出当前对象时执行pointerEnterHandler、pointerExitHandler。在鼠标或者触摸按下、松开时执行pointerDownHandler、pointerUpHandler。在鼠标或触摸松开并且与按下时是同两个响应物体时执行pointerClickHandler。在鼠标或触摸边线发生偏移(偏移值大于两个很小的常量)时执行beginDragHandler。在鼠标或者触摸按下且当前对象能响应拖拽该事件时执行initializePotentialDrag。对象正在被拖拽且鼠标或触摸移动时执行dragHandler。对象正在被拖拽且鼠标或触摸松开时执行endDragHandler。鼠标或触摸松开且对象未响应pointerClickHandler情况下,如果对象正在被拖拽,执行dropHandler。当鼠标滚动差值大于零执行scrollHandler。当输入模块切换到StandaloneInputModule时执行updateSelectedHandler。(不须要Input类)当鼠标移动导致被选上的对象改变时,执行selectHandler和deselectHandler。导航该事件可用情况下,按下上下左滑鼠,执行moveHandler,按下确认键执行submitHandler,按下取消键执行cancelHandler。更加底层的调用还是UnityEngine.Input类,但可惜的是这部分Unity并没有开源。
每次该事件系统中只能有两个输入模块处于活跃状态,并且必须与EventSystem模块处于相同的游戏对象上。执行该事件
既然InputModule主要就是处置设备输入,发送该事件到场景对象,那这些该事件是怎么执行的呢?在讲Button的时候,他们提到过ExecuteEvent类,其实该事件的执行都是通过这个类展开的,不过也须要EventInterface接口配合。这个类中定义了许多接口,比如鼠标按下、点选、拖拽等,下图展示了部分接口的继承关系。
ExecuteEvent类中提供了两个方法让外部统一调用以执行该事件
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
//从对象池中取出两个IEventSystemHandler类型的元素
var internalHandlers = s_HandlerListPool.Get();
GetEventList<T>(target, internalHandlers);
// if (s_InternalHandlers.Count > 0)
// Debug.Log(“Executinng ” + typeof (T) + ” on ” + target);
var internalHandlersCount = internalHandlers.Count;
for (var i = 0; i < internalHandlersCount; 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;
}
这个方法以后有讲过,主要就是查找target对象上的T类型的模块列表,并遍历执行。
除此之外,还有两个GetEventHandler方法,它主要是通过冒泡的方式查找出能处置指定该事件的对象。
// 在游戏对象上冒泡指定的该事件,找出哪个对象将实际接收该事件。public static GameObject GetEventHandler<T>(GameObject root) where T : IEventSystemHandler
{
if (root == null)
return null;
Transform t = root.transform;
//冒泡查找,如果物体本身不能处置输入的该事件,交予parent处置
while (t != null)
{
if (CanHandleEvent<T>(t.gameObject))
return t.gameObject;
t = t.parent;
}
return null;
}
// 指定的游戏对象是否能处置指定的该事件
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;
}
比如他们在场景中创建两个Button,那这个Button还包含了Text模块,当鼠标点选的时候会调用GetEventHandler函数,该函数的root参数其实是Text,但是会通过冒泡的方式查找出它的父物体Button,接着调用Button的点选该事件。
Raycasters
该事件系统须要两个方法来检测当前输入该事件须要发送到哪里,这是由Raycasters提供的。 给定两个屏幕空间边线,它们将收集所有潜在目标,找出它们是否在给定边线下,接着返回离屏幕最近的对象。 系统提供了以下几种类型的Raycaster:
Graphic Raycaster: 检测UI元素Physics 2D Raycaster: 用于2D物理元素Physics Raycaster: 用于3D物理元素
BaseRaycaster是其他Raycaster的基类,这是是两个抽象类。在它OnEnable里将自己注册到RaycasterManager,并在OnDisable的时候从后者移除。
RaycasterManager是两个静态
BaseRaycaster中最重要的就是Raycast方法了,它的子类都对该方法展开了重写。
Physics Raycaster
它主要用于检测3D物理元素,并且留存被射线检测到物体的数据,下面是部分标识符
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
//判断是否超出摄像机的远近裁剪平面的距离
if (!ComputeRayAndDistance(eventData, ref ray, ref displayIndex, ref distanceToClipPlane))
return;
//采用ReflectionMethodsCache.Single //用反射的方式把Physics.RaycastAll()方法缓存下来,让Unity的Physics模块与UI模块,保持低耦合,没有过分依赖。 if (m_MaxRayIntersections == 0)
{
m_Hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, finalEventMask);
hitCount = m_Hits.Length;
}
else
{
if (m_LastMaxRayIntersections != m_MaxRayIntersections)
{
m_Hits = new RaycastHit[m_MaxRayIntersections];
m_LastMaxRayIntersections = m_MaxRayIntersections;
}
hitCount = ReflectionMethodsCache.Singleton.getRaycastNonAlloc(ray, m_Hits, distanceToClipPlane, finalEventMask);
}
if (hitCount != 0)
{
if (hitCount > 1)
System.Array.Sort(m_Hits, 0, hitCount, RaycastHitComparer.instance);
for (int b = 0, bmax = hitCount; b < bmax; ++b)
{
var result = new RaycastResult
{
…//为result赋值
};
resultAppendList.Add(result);
}
}
}
Physics2DRaycaster继承自PhysicsRaycaster,实现功能和方式基本一致,只不过是用于检测2D物体,这里不具体讲解
GraphicRaycast
GraphicRaycast用于检测UI元素,它依赖于Canvas,我
一大波标识符来袭,不感兴趣能跳过
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
if (canvas == null)
return;
//返回Canvas上的所有包含Graphic脚本并且raycastTarget=true的游戏物体 var canvasGraphics = GraphicRegistry.GetRaycastableGraphicsForCanvas(canvas);
if (canvasGraphics == null || canvasGraphics.Count == 0)
return;
int displayIndex;
//画布在ScreenSpaceOverlay模式下默认为null var currentEventCamera = eventCamera; // Property can call Camera.main, so cache the reference
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
displayIndex = canvas.targetDisplay;
else
displayIndex = currentEventCamera.targetDisplay;
var eventPosition = Display.RelativeMouseAt(eventData.position);
if (eventPosition != Vector3.zero)
{
int eventDisplayIndex = (int)eventPosition.z;
if (eventDisplayIndex != displayIndex)
return;
}
else
{
eventPosition = eventData.position;
}
// Convert to view space //将鼠标点在屏幕上的坐标转换成摄像机的视角坐标,如果超出范围则return
Vector2 pos;
if (currentEventCamera == null)
{
float w = Screen.width;
float h = Screen.height;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
w = Display.displays[displayIndex].systemWidth;
h = Display.displays[displayIndex].systemHeight;
}
pos = new Vector2(eventPosition.x / w, eventPosition.y / h);
}
else
pos = currentEventCamera.ScreenToViewportPoint(eventPosition);
// If its outside the cameras viewport, do nothing
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
return;
float hitDistance = float.MaxValue;
Ray ray = new Ray();
//如果currentEventCamera不为空,摄像机发射射线
if (currentEventCamera != null)
ray = currentEventCamera.ScreenPointToRay(eventPosition);
//如果当前画布不是ScreenSpaceOverlay模式并且blockingObjects != BlockingObjects.None //计算hitDistance的值
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 PACKAGE_PHYSICS
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;
}
}
#endif
#if PACKAGE_PHYSICS2D
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;
}
}
#endif
}
m_RaycastResults.Clear();
//调用Raycast函数重载
Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
//遍历m_RaycastResults,判断Graphic的方向向量和Camera的方向向量是否相交,接着判断Graphic是否在Camera的前面,并且距离小于等于hitDistance,满足了这些条件,才会把它打包成RaycastResult添加到resultAppendList里。 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 cameraForward = currentEventCamera.transform.rotation * Vector3.forward * currentEventCamera.nearClipPlane;
appendGraphic = Vector3.Dot(go.transform.position – currentEventCamera.transform.position – cameraForward, go.transform.forward) >= 0;
}
}
if (appendGraphic)
{
float distance = 0;
Transform trans = go.transform;
Vector3 transForward = trans.forward;
if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
distance = 0;
else
{
// 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;
var castResult = new RaycastResult
{
……
};
resultAppendList.Add(castResult);
}
}
上述标识符中调用了Raycast函数重载,作用是向屏幕投射射线并收集屏幕下方所有挂载了Graphic脚本的游戏对象,该函数内容为:
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
{
// Necessary for the event system
//遍历场景内Graphic对象(挂载Graphic脚本的对象)
int totalCount = foundGraphics.Count;
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];
// -1 means it hasnt been processed by the canvas, which means it isnt actually drawn if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == –1)
continue;
//目标点是否在矩阵中
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera, graphic.raycastPadding))
continue;
//超出摄像机范围
if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
continue;
//调用符合条件的Graphic的Raycast方法
if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
}
}
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
totalCount = s_SortedGraphics.Count;
for (int i = 0; i < totalCount; ++i)
results.Add(s_SortedGraphics[i]);
s_SortedGraphics.Clear();
}
函数中又调用了Graphic类的Raycast函数,它主要是做两件事,一件是采用RectTransform的值过滤元素,另一件是采用Raycast函数确定射线击中的元素。RawImage、Image和Text都间接继承自Graphic。
![[UGUI源码一]6千字带你入门UGUI源码11 [UGUI源码一]6千字带你入门UGUI源码](data:image/svg+xml;utf8,)
public virtual bool Raycast(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled)
return false;
//UI元素,比如Image,Button等
var t = transform;
var components = ListPool<Component>.Get();
bool ignoreParentGroups = false;
bool continueTraversal = true;
while (t != null)
{
t.GetComponents(components);
for (var i = 0; i < components.Count; i++)
{
Debug.Log(components[i].name);
var canvas = components[i] as Canvas;
if (canvas != null && canvas.overrideSorting)
continueTraversal = false;
(Image,Mask,RectMask2D) var filter = components[i] as ICanvasRaycastFilter;
if (filter == null)
continue;
var raycastValid = true;
//判断sp点是否在有效的范围内
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<Component>.Release(components);
return false;
}
}
//遍历它的父物体
t = continueTraversal ? t.parent : null;
}
ListPool<Component>.Release(components);
return true;
}
这里也采用了ICanvasRaycastFilter接口中的IsRaycastLocationValid函数,主要还是判断点的边线是否有效,不过这里采用了Alpha测试。Image、Mask和RectMask2D都继承了该接口。
![[UGUI源码一]6千字带你入门UGUI源码12 [UGUI源码一]6千字带你入门UGUI源码](data:image/svg+xml;utf8,)
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
//小于阈值(alphaHitTestMinimumThreshold)的Alpha值将导致射线该事件穿透图像。
//值为1将导致只有完全不透明的像素在图像上注册相应射线该事件。 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);
// Convert local coordinates to texture space. Rect spriteRect = activeSprite.textureRect;
float x = (spriteRect.x + local.x) / activeSprite.texture.width;
float y = (spriteRect.y + local.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;
}
}
EventData
EventData用以存储该事件信息,涉及到的东西不多,不展开讲解,层级关系如下图所示
实战:为Button的点选该事件添加参数
在执行Button点选该事件时,有些情况
/// <summary>
/// UI该事件窃听器(与Button等UI挂在同两个物体上):管理所有UGUI该事件,提供该事件参数类/// 若想看所有相关委托 自行翻查EventTrigger类
/// </summary>
public class UIEventListener : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
{
//2.定义委托
public delegate void PointerEventHandler(PointerEventData eventData);
//3.声明该事件
public event PointerEventHandler PointerClick;
public event PointerEventHandler PointerDown;
public event PointerEventHandler PointerUp;
/// <summary>
/// </summary> /// <param name=”transform”></param>
/// <returns></returns>
public static UIEventListener GetEventListener(Transform transform)
{
UIEventListener uIEventListener = transform.GetComponent<UIEventListener>();
if (uIEventListener == null)
uIEventListener = transform.gameObject.AddComponent<UIEventListener>();
return uIEventListener;
}
//1.实现接口
public void OnPointerClick(PointerEventData eventData)
{
//表示抽象的有 抽象类 接口(多类抽象行为) 委托(一类抽象行为) //4.引发该事件
if (PointerClick != null)
PointerClick(eventData);
}
public void OnPointerDown(PointerEventData eventData)
{
PointerDown?.Invoke(eventData);
}
public void OnPointerUp(PointerEventData eventData)
{
PointerUp?.Invoke(eventData);
}
}
采用的时候,他们只须要将它挂载到Button模块上,接着在PointerClick该事件中添加自己的处置函数。
总结
Button点选该事件怎么触发的呢?首先是EventSystem在Update中调用当前输入模块的Process方法处置所有的鼠标该事件,并且输入模块会调用RaycastAll来得到目标信息,通过冒泡的方式找出该事件实际接收者并执行点选该事件(这只是总体流程,中间省略很多具体步骤)。
下面是UGUI系列第二篇文章,UI重建。码字不易,欢迎点赞支持! ❤️
最后来一张层级关系图