UGUI 源码分析

UGUI 源码分析
NyxX原文地址 www.zhangwei.press
基础回顾 在正式分析 UGUI 源码前,让我们回顾一下,模型的组成,以及渲染。
在正式分析 UGUI 源码前,让我们回顾一下,模型的组成,以及渲染。
模型组成
模型是由一系列的三角形组成,三角形又是由三个顶点构成的,每个顶点上保存了相关的顶点属性(顶点位置,顶点颜色,顶点 UV,顶点法线,顶点切线)在 Unity 中使用 Mesh 类来表示。
模型渲染
模型筛选:
- 首先根据相机的 Culling Mask 剔除那些不需要此相机渲染的模型。
- 然后根据摄像机的视锥体大小,剔除完全在视锥体外的模型。
模型分类:
Unity 将根据需要渲染的模型 Shader 中 RenderQueue 对模型进行分类,分为不透明模型 (小于 2500) 和透明模型(大于等于 2500)。
排序渲染模型:
- 首先通过 sortLayer 和 sortingOrder 对不透明模型进行进行排序,越低的越先渲染;然后再通过 Shader 中的 RenderQueue 进行排序;再然后就通过包围盒中心点的深度(距离摄像机的位置)进行排序;最后如果深度相同则还可以通过 ZBias 进行深度调节。
- 透明物体同样的使用上述规则进行排序。
根据排序的结果逐个渲染模型:
渲染一个模型主要分为以下 2 步:
- 设置渲染状态(提交模型数据,贴图,设置深度, 模板和混合参数等 )
- 调用绘制命令
当然这个只是它的渲染顺序,并不是说最后渲染的物体就一定会进入颜色缓冲,最后还的看深度和模板测试的结果。
设计结构
UGUI 主要由显示组件,布局系统,事件系统,交互组件组成。这里主要介绍 UGUI 显示组件相关的几个核心类
UIBehaviour, 此类继承至 MonoBehaviour,主要定义了 UI 组件的生命周期以及 RectTransform 的一些事件函数(比如:RectTransform 的大小或层次结构的改变等)
Canvas, 主要定义了渲染相关的参数,比如渲染模式,渲染顺序,使用的相机以及缩放因子等。此类也用来进行组织 UI 渲染和 Mesh 合并。Canvas 的渲染, 在有相机 (ScreenSpaceCamera 和 WorldSpace 渲染模式) 的情况下是在渲染透明物体阶段进行渲染的
在没有相机(ScreenSpaceOverlay 渲染模式)的情况下是在所有渲染完成后在进行单独渲染的,所以能够保证 Canvas 在最上面。
ICanvasElement, 此接口是所有 Canvas 元素需要实现的接口,接口中最重要的函数就是 Rebuild 函数和其关联的 transform 字段,当一个 Canvas 元素需要重建时则会调用此函数。Canvas 元素包含显示组件和交互组件。
CanvasRenderer, 定义了显示组件需要用到的 Mesh,材质,纹理,是否剔除,裁剪区域以及深度信息等。Canvas 则会根据这些信息进行排序、动态合并 Mesh 和最终的渲染。
CanvasUpdateRegistry, 此用用于管理需要更新的 Canvas 元素,更新主要分为两类,一类是:布局更新,另一类是:Graphic 更新(包含 UpdateGeometry 和 UpdateMaterial)Canvas 元素的更新主要包含以下 5 个阶段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public enum CanvasUpdate
{
Prelayout = 0,
Layout = 1,
PostLayout = 2,
PreRender = 3,
LatePreRender = 4,
MaxUpdateValue = 5
}CanvasUpdateRegistry 类在其构造函数中监听了 Canvas 将要渲染的事件,当收到此事件后则会进行上述几个阶段的更新,核心代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80protected CanvasUpdateRegistry()
{
Canvas.willRenderCanvases += PerformUpdate;
}
private void PerformUpdate()
{
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
CleanInvalidItems();
m_PerformingLayoutUpdate = true;
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
{
for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
{
var rebuild = instance.m_LayoutRebuildQueue[j];
try
{
if (ObjectValidForUpdate(rebuild))
rebuild.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, rebuild.transform);
}
}
}
for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue[i].LayoutComplete();
instance.m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
ClipperRegistry.instance.Cull();
m_PerformingGraphicUpdate = true;
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
{
for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
{
try
{
var element = instance.m_GraphicRebuildQueue[k];
if (ObjectValidForUpdate(element))
element.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
}
}
}
for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue[i].GraphicUpdateComplete();
instance.m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}
总结:设计的主要结构就是 Canvas 元素负责更新 CanvasRenderer 中的 Mesh, 材质等,然后 Canvas 则管理 CanvasRenderer 中 Mesh 的合并,并根据其渲染模式进行渲染。接下来我们将继续探索 Canvas 元素的具体更新流程。
显示组件 RawImage,Image 和 Text
显示组件关联的几个核心类:
Graphic, 继承至 UIBehaviour 并实现了 ICanvasElement 是所有显示组件的基类,此类维护了显示组件的生命周期,管理了布局,顶点和材质改变,一旦顶点或材质改变则会将自己添加到 CanvasUpdateRegistry 的更新列表里,在下帧则会进行重建(调用 Rebuild 函数)。如果布局改变则会将自己添加到 LayoutRebuilder 的队列里,在下一帧进行构建。 Graphic 的核心函数:
Graphic.Rebuild, 此函数会负责具体的材质更新和 Mesh 的构建;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public virtual void Rebuild(CanvasUpdate update)
{
if (canvasRenderer == null || canvasRenderer.cull)
return;
switch (update)
{
case CanvasUpdate.PreRender:
if (m_VertsDirty)
{
UpdateGeometry();
m_VertsDirty = false;
}
if (m_MaterialDirty)
{
UpdateMaterial();
m_MaterialDirty = false;
}
break;
}
}Graphic.DoMeshGeneration, 此函数负责具体的 Mesh 构建(基础 Mesh 和附加 Mesh(描边和阴影 Mesh 的生成));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21private void DoMeshGeneration()
{
if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
OnPopulateMesh(s_VertexHelper);
else
s_VertexHelper.Clear();
var components = ListPool<Component>.Get();
GetComponents(typeof(IMeshModifier), components);
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);
ListPool<Component>.Release(components);
s_VertexHelper.FillMesh(workerMesh);
canvasRenderer.SetMesh(workerMesh);
}Graphic.OnPopulateMesh, 此函数主要是用于派生类去覆写 Mesh 的基础构建;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18protected virtual void OnPopulateMesh(VertexHelper vh)
{
var r = GetPixelAdjustedRect();
var v = new Vector4(r.x, r.y, r.x + r.width, r.y + r.height);
Color32 color32 = color;
vh.Clear();
vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(0f, 0f));
vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(0f, 1f));
vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(1f, 1f));
vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(1f, 0f));
vh.AddTriangle(0, 1, 2);
vh.AddTriangle(2, 3, 0);
}Graphic.materialForRendering,此字段用于获取渲染时使用的材质,这主要做了一个提供修改材质的机制,当对象上有 IMaterialModifier 接口的组件对象时,则会使用它的材质进行渲染,主要用于 Mask 的材质修改,当然你也可以新建一个自定义的组件用于修改渲染时的材质。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public virtual Material materialForRendering
{
get
{
var components = ListPool<Component>.Get();
GetComponents(typeof(IMaterialModifier), components);
var currentMat = material;
for (var i = 0; i < components.Count; i++)
currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
ListPool<Component>.Release(components);
return currentMat;
}
}Graphic.Raycast, 此函数由于检查是否能被击中
MaskableGraphic, 继承至 Graphic 新增了 Mask 的支持,Mask 相关的内容,在下一节去学习。
VertexHelper, 此类是一个辅助产生 Mesh 的类,记录每个顶点以及顶点的相关属性(顶点位置,顶点颜色,顶点 UV,顶点法线,顶点切线和三角形索引)。
RawImage, 直接使用一张纹理显示一张图片,一个 RawImage 就是一个 Drawcall,此类一版用于背景。核心函数就是覆写的 OnPopulateMesh 支持了 uv 的修改。
Imge, 此类也是显示一张图片,但是显示的不是一张完整的纹理而是显示一个图集中的一个 Sprite。支持 Simple, Sliced, Tiled 和 Filled 类型。在 OnPopulateMesh 函数中根据不同的类型进行顶点的生成。
Text, 此类是用来显示文字的。此类在 OnPopulateMesh 函数中,通过 TextGenerator 类进行顶点的生成,并将每 4 个顶点组成一个 Quad。
类图:
Mask 实现原理
UGUI 在实现 Mask 的时候有两种方式:
- 使用模板 (Stencil) 缓冲进行裁剪,缺点是在 Mask 下没有任何显示对象的情况下都需要 2 个 Drawcall,一个 Drawcall 绘制蒙版,另一个 Drawcall 清除绘制的蒙版,并且还会打断合批。
- 直接在应用层级根据 Mask 的矩形区域进行裁剪剔除,这样保证在绘制前,那些超出 Mask 区域的顶点已经被裁剪,优点是不需要额外的 2 个 Drawcall,缺点是需要 CPU 进行额外的裁剪操作,对象越多 CPU 执行裁剪的消耗就越多,并且也只能进行 2D 对象的裁剪。
所用我们在选用蒙版的时候也要综合考虑两种方式的优缺点,在子对象比较多,CPU 成为了性能瓶颈的时候,可以考虑使用第 1 种方式,如果子对象比较少则可以使用第 2 种方式;还的综合考虑合批的影响。
模板 (Stencil) 缓冲实现 Mask
IMaskable, 实现这个接口的元素是可以被蒙版裁剪剔除的,现在只有 MaskableGraphic 实现了这个接口,也就是说,上一节讨论的所有显示组件(RawImage,Image 和 Text)都能被蒙版裁剪剔除。就一个接口 RecalculateMasking。
Mask, 此类主要实现了 IMaterialModifier 接口中的 GetModifiedMaterial 函数,用于修改显示组件使用的材质,参见上一节的 Graphic.materialForRendering。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85public virtual Material GetModifiedMaterial(Material baseMaterial)
{
if (!MaskEnabled())
return baseMaterial;
var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
if (stencilDepth >= 8)
{
Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
return baseMaterial;
}
int desiredStencilBit = 1 << stencilDepth;
if (desiredStencilBit == 1)
{
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial;
var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);
return m_MaskMaterial;
}
var maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial2;
graphic.canvasRenderer.hasPopInstruction = true;
var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial2;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);
return m_MaskMaterial;
}
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
var toUse = baseMaterial;
if (m_ShouldRecalculateStencil)
{
if (maskable)
{
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
m_StencilValue = MaskUtilities.GetStencilDepth(transform, rootCanvas);
}
else
m_StencilValue = 0;
m_ShouldRecalculateStencil = false;
}
if (m_StencilValue > 0 && !isMaskingGraphic)
{
var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMat;
toUse = m_MaskMaterial;
}
return toUse;
}MaskUtilities, 两种类型的 Mask 都用的工具类。
StencilMaterial, 模板 (Stencil) 缓冲的材质生成类,主要的功能是基于基础材质添加上模板参数生成新的材质给 Mask 使用,也使用了引用计数的方式对相同的材质进行了缓存,避免重复创建相同的。核心代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public static Material Add(Material baseMat, int stencilID, StencilOp operation, CompareFunction compareFunction, ColorWriteMask colorWriteMask, int readMask, int writeMask)
{
if ((stencilID <= 0 && colorWriteMask == ColorWriteMask.All) || baseMat == null)
return baseMat;
newEnt.customMat.SetInt("_Stencil", stencilID);
newEnt.customMat.SetInt("_StencilOp", (int)operation);
newEnt.customMat.SetInt("_StencilComp", (int)compareFunction);
newEnt.customMat.SetInt("_StencilReadMask", readMask);
newEnt.customMat.SetInt("_StencilWriteMask", writeMask);
newEnt.customMat.SetInt("_ColorMask", (int)colorWriteMask);
newEnt.customMat.SetInt("_UseUIAlphaClip", newEnt.useAlphaClip ? 1 : 0);
if (newEnt.useAlphaClip)
newEnt.customMat.EnableKeyword("UNITY_UI_ALPHACLIP");
else
newEnt.customMat.DisableKeyword("UNITY_UI_ALPHACLIP");
m_List.Add(newEnt);
return newEnt.customMat;
}
应用程序裁剪 Mask 实现 RectMask2D
IClipper, 定义了裁剪器接口,现在只有 RectMask2D 实现了此接口,核心函数 PerformClipping。
IClippable, 定义了能够被裁剪的接口,现在只有 MaskableGraphic 实现了此接口,核心函数 RecalculateClipping,Cull 和 SetClipRect。
ClipperRegistry, 此类缓存了所有的 IClipper 对象, 核心函数 Cull,在布局完后调用,对所有的裁剪器调用 PerformClipping 进行裁剪。
RectangularVertexClipper, 唯一一个函数 GetCanvasRect 获取在 Canvas 下的 Rect 信息。
Clipping, 唯一一个函数 FindCullAndClipWorldRect 查找所有 RectMask2D 的公共 Rect 区域.
RectMask2D, 此类实现了 IClipper 接口,核心函数就是 IClipper 接口中的 PerformClipping,此函数执行计算了裁剪的矩形区域,并将计算的裁剪区域设置给了 IClippable 接口的对象,现在只有 MaskableGraphic 类,MaskableGraphic 又将裁剪区域设置给了 CanvasRenderer,每个显示组件的裁剪是 Unity 底层去进行裁剪的。核心代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77public virtual void PerformClipping()
{
if (ReferenceEquals(Canvas, null))
{
return;
}
if (m_ShouldRecalculateClipRects)
{
MaskUtilities.GetRectMasksForClip(this, m_Clippers);
m_ShouldRecalculateClipRects = false;
}
bool validRect = true;
Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
RenderMode renderMode = Canvas.rootCanvas.renderMode;
bool maskIsCulled =
(renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) &&
!clipRect.Overlaps(rootCanvasRect, true);
if (maskIsCulled)
{
clipRect = Rect.zero;
validRect = false;
}
if (clipRect != m_LastClipRectCanvasSpace)
{
foreach (IClippable clipTarget in m_ClipTargets)
{
clipTarget.SetClipRect(clipRect, validRect);
}
foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
{
maskableTarget.SetClipRect(clipRect, validRect);
maskableTarget.Cull(clipRect, validRect);
}
}
else if (m_ForceClip)
{
foreach (IClippable clipTarget in m_ClipTargets)
{
clipTarget.SetClipRect(clipRect, validRect);
}
foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
{
maskableTarget.SetClipRect(clipRect, validRect);
if (maskableTarget.canvasRenderer.hasMoved)
maskableTarget.Cull(clipRect, validRect);
}
}
else
{
foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
{
maskableTarget.Cull(clipRect, validRect);
}
}
m_LastClipRectCanvasSpace = clipRect;
m_ForceClip = false;
UpdateClipSoftness();
}
总结:ClipperRegistry.Cull 会在每帧布局完成后调用,此函数执行所有 IClipper 的裁剪计算,并将计算好的裁剪矩形区域通过 MaskableGraphic.SetClipRect 传递给 CanvasRenderer 对象,最后 Unity 内部根据这些裁剪矩形来进行顶点的裁剪。
Mesh 效果
Mesh 效果是在,基础 Mesh 构建完后,再重修或添加新的顶点构成最终的 Mesh。UGUI 自带的 Mesh 效果有 Outline,PositionAsUV1 和 Shadow 效果。
- IMeshModifier, 定义了 ModifyMesh 函数
- BaseMeshEffect, Mesh 效果的基类,核心函数是接口 IMeshModifier 的 ModifyMesh 函数。
- Outline, 在 ModifyMesh 添加描边的顶点。
- PositionAsUV1, 将顶点的位置设置为该顶点的 UV1(一般是法线贴图坐标),为图片或者文字添加法线贴图效果。
类图:
布局基础 - RectTransform
RectTransform 组件是 Transform 组件对应的 2D 表示。 其中 Transform 表示单个点,RectTransform 表示可以放置 UI 元素的矩形。如果 RectTransform 的父项也是 RectTransform,则子项 RectTransform 也可以指定它相对于父矩形的位置和大小。RectTransform 用于存储和操作矩形的位置、大小和锚点,并支持基于父 RectTransform 的各种形式的缩放。
基础属性:
布局的基础知识了解完了,接下来看下 UGUI 自带的布局器。
布局器
布局器大致很两类,一类控制自己,另一类控制子对象。
控制自己
- ILayoutElement, 定义布局元素的相关属性和方法(CalculateLayoutInputHorizontal, CalculateLayoutInputVertical,minWidth/Height, preferredWidth/Height, flexibleWidth/Height)。
- LayoutElement, 覆盖对象的布局元素参数,使用这个组件定义的大小。
- ILayoutSelfController,ILayoutElement 负责计算布局,ILayoutController 负责更新布局,ILayoutSelfController 直接继承至 ILayoutController,其中两个核心函数(SetLayoutHorizontal 和 SetLayoutVertical)。
- LayoutUtility, 对获取布局元素中的 min, preferred 和 flexible 便捷的获取,并且获取优先级最高的。
- AspectRatioFitter,调整自身的大小以适应指定的纵横比。
- ContentSizeFitter,根据自身内容调整 RectTransfrom 的大小。
控制子对象
ILayoutGroup,ILayoutElement 负责计算布局,ILayoutController 负责更新布局,ILayoutGroup 直接继承至 ILayoutController,其中两个核心函数(SetLayoutHorizontal 和 SetLayoutVertical)。
LayoutGroup,所有控制子对象布局器的基类。主要完成的工作包括:定义对其参数以及相关的操作,获取有效的布局子对象和获取布局对象总的 min, preferred 和 flexible 的值。
1
2
3
4
5
6
7
8protected float GetStartOffset(int axis, float requiredSpaceWithoutPadding)
{
float requiredSpace = requiredSpaceWithoutPadding + (axis == 0 ? padding.horizontal : padding.vertical);
float availableSpace = rectTransform.rect.size[axis];
float surplusSpace = availableSpace - requiredSpace;
float alignmentOnAxis = GetAlignmentOnAxis(axis);
return (axis == 0 ? padding.left : padding.top) + surplusSpace * alignmentOnAxis;
}HorizontalOrVerticalLayoutGroup, 水平或垂直布局的基类,核心函数 CalcAlongAxis 计算自己的总的大小和 SetChildrenAlongAxis 设置子的位置和大小。
HorizontalLayoutGroup 和 VerticalLayoutGroup, 继承至 HorizontalOrVerticalLayoutGroup 这个类,分别在计算和设置函数中调用了 HorizontalOrVerticalLayoutGroup 类的两个核心函数。
GridLayoutGroup, 网格布局的。
LayoutRebuilder,此类是 RectTransform 对应的布局重建器,核心函数 MarkLayoutForRebuild 关联一个需要布局重建的 RectTransfrom,Rebuild 函数(ICanvasElement 的接口函数)负责布局重建,Rebuild 函数是在 Canvas 更新循环(CanvasUpdateRegistry)的布局阶段进行调用的(常见前面章节)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56public static void MarkLayoutForRebuild(RectTransform rect)
{
if (rect == null || rect.gameObject == null)
return;
var comps = ListPool<Component>.Get();
bool validLayoutGroup = true;
RectTransform layoutRoot = rect;
var parent = layoutRoot.parent as RectTransform;
while (validLayoutGroup && !(parent == null || parent.gameObject == null))
{
validLayoutGroup = false;
parent.GetComponents(typeof(ILayoutGroup), comps);
for (int i = 0; i < comps.Count; ++i)
{
var cur = comps[i];
if (cur != null && cur is Behaviour && ((Behaviour)cur).isActiveAndEnabled)
{
validLayoutGroup = true;
layoutRoot = parent;
break;
}
}
parent = parent.parent as RectTransform;
}
if (layoutRoot == rect && !ValidController(layoutRoot, comps))
{
ListPool<Component>.Release(comps);
return;
}
MarkLayoutRootForRebuild(layoutRoot);
ListPool<Component>.Release(comps);
}
public void Rebuild(CanvasUpdate executing)
{
switch (executing)
{
case CanvasUpdate.Layout:
PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement).CalculateLayoutInputHorizontal());
PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController).SetLayoutHorizontal());
PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement).CalculateLayoutInputVertical());
PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController).SetLayoutVertical());
break;
}
}
屏幕适配
CanvasScaler, 定义几种缩放模式,此类是主要是计算 Canvas 的缩放因子(Canvas.scaleFactor), 每种缩放模式计算缩放因子的方式不一样,这里主要看哈 ScaleWithScreenSize 模式的缩放因子计算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59public enum ScaleMode
{
ConstantPixelSize,
ScaleWithScreenSize,
ConstantPhysicalSize
}
protected virtual void HandleScaleWithScreenSize()
{
Vector2 screenSize = new Vector2(Screen.width, Screen.height);
int displayIndex = m_Canvas.targetDisplay;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
Display disp = Display.displays[displayIndex];
screenSize = new Vector2(disp.renderingWidth, disp.renderingHeight);
}
float scaleFactor = 0;
switch (m_ScreenMatchMode)
{
case ScreenMatchMode.MatchWidthOrHeight:
{
float logWidth = Mathf.Log(screenSize.x / m_ReferenceResolution.x, kLogBase);
float logHeight = Mathf.Log(screenSize.y / m_ReferenceResolution.y, kLogBase);
float logWeightedAverage = Mathf.Lerp(logWidth, logHeight, m_MatchWidthOrHeight);
scaleFactor = Mathf.Pow(kLogBase, logWeightedAverage);
break;
}
case ScreenMatchMode.Expand:
{
scaleFactor = Mathf.Min(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);
break;
}
case ScreenMatchMode.Shrink:
{
scaleFactor = Mathf.Max(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);
break;
}
}
SetScaleFactor(scaleFactor);
SetReferencePixelsPerUnit(m_ReferencePixelsPerUnit);
}
事件系统分为主要分为三个模块输入模块,射线器和事件系统。
输入模块
输入模块负责获取处理系统的各种输入(屏幕输入,鼠标等)。下文中的 “指针” 是指鼠标指针或 Touch 的触摸点。核心类:
BaseInput, 输入的基类,主要转接了 Unity 的 Input 的部分功能,当然我们也可以从此类派生一个类,用于处理自定义的输入。
BaseInputModule, 输入模块的基类,其中包括了关联的 EventSystem,BaseInput 和事件数据对象。其中 BaseInput 用于获取设备的输入情况。核心函数
Process, 此函数是一个抽象函数,主要是用来处理输入模块的更新,这个函数在 EventSystem.Update 函数中调用的
HandlePointerExitAndEnter, 此函数负责处理指针的进入和离开。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget)
{
if (newEnterTarget == null || currentPointerData.pointerEnter == null)
{
for (var i = 0; i < currentPointerData.hovered.Count; ++i)
ExecuteEvents.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler);
currentPointerData.hovered.Clear();
if (newEnterTarget == null)
{
currentPointerData.pointerEnter = null;
return;
}
}
if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget)
return;
GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);
if (currentPointerData.pointerEnter != null)
{
Transform t = currentPointerData.pointerEnter.transform;
while (t != null)
{
if (commonRoot != null && commonRoot.transform == t)
break;
ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);
currentPointerData.hovered.Remove(t.gameObject);
t = t.parent;
}
}
currentPointerData.pointerEnter = newEnterTarget;
if (newEnterTarget != null)
{
Transform t = newEnterTarget.transform;
while (t != null && t.gameObject != commonRoot)
{
ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);
currentPointerData.hovered.Add(t.gameObject);
t = t.parent;
}
}
}ActivateModule,定义的输入模块被激活时调用的函数,这函数是在 EventSystem 中调用的
DeactivateModule, 定义的输入模块被禁用时调用的函数,这函数是在 EventSystem 中调用的
PointerInputModule, 派生至 BaseInputModule,用于处理指针(Touch 和 Mouse)的输入。核心函数
GetPointerData: 此函数是从 m_PointerData 列表中获取或创建一个 PointerEventData 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14protected bool GetPointerData(int id, out PointerEventData data, bool create)
{
if (!m_PointerData.TryGetValue(id, out data) && create)
{
data = new PointerEventData(eventSystem)
{
pointerId = id,
};
m_PointerData.Add(id, data);
return true;
}
return false;
}GetTouchPointerEventData, 获取 Touch 指针的事件数据,并返回是否按下或释放以及通过 EventSystem.RaycastAll 函数获取当前指针击中的第一个射线结果,并将结果保存在指针事件数据的 pointerCurrentRaycast 字段中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
{
PointerEventData pointerData;
var created = GetPointerData(input.fingerId, out pointerData, true);
pointerData.Reset();
pressed = created || (input.phase == TouchPhase.Began);
released = (input.phase == TouchPhase.Canceled) || (input.phase == TouchPhase.Ended);
if (created)
pointerData.position = input.position;
if (pressed)
pointerData.delta = Vector2.zero;
else
pointerData.delta = input.position - pointerData.position;
pointerData.position = input.position;
pointerData.button = PointerEventData.InputButton.Left;
if (input.phase == TouchPhase.Canceled)
{
pointerData.pointerCurrentRaycast = new RaycastResult();
}
else
{
eventSystem.RaycastAll(pointerData, m_RaycastResultCache);
var raycast = FindFirstRaycast(m_RaycastResultCache);
pointerData.pointerCurrentRaycast = raycast;
m_RaycastResultCache.Clear();
}
return pointerData;
}StateForMouseButton, 获取指定鼠标指针的按下或释放状态。
1
2
3
4
5
6
7
8
9
10
11
12
13protected 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;
}GetMousePointerEventData, 获取鼠标指针的事件数据,同 GetTouchPointerEventData 功能一样只是这里的输入状态是获取鼠标的。
ProcessMove, 处理以移动,根据指针事件数据的 pointerCurrentRaycast 进行指针进入或离开处理
ProcessDrag, 处理拖拽.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32protected virtual void ProcessDrag(PointerEventData pointerEvent)
{
if (!pointerEvent.IsPointerMoving() ||
Cursor.lockState == CursorLockMode.Locked ||
pointerEvent.pointerDrag == null)
return;
if (!pointerEvent.dragging
&& ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold))
{
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler);
pointerEvent.dragging = true;
}
if (pointerEvent.dragging)
{
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);
}
}
StandaloneInputModule, 继承至 PointerInputModule 模块,此类处理了鼠标,键盘,控制器和 Touch 的输入,以前 TouchInputModule 模块已经合并到此类中。核心函数是 Process, ProcessTouchEvents 和 ProcessMouseEvent。
Process, 此函数是 EventSystem.Update 函数中调用过来的,用于每帧处理 Touch 和鼠标的输入信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public override void Process()
{
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
return;
bool usedEvent = SendUpdateEventToSelectedObject();
if (!ProcessTouchEvents() && input.mousePresent)
ProcessMouseEvent();
if (eventSystem.sendNavigationEvents)
{
if (!usedEvent)
usedEvent |= SendMoveEventToSelectedObject();
if (!usedEvent)
SendSubmitEventToSelectedObject();
}
}ProcessTouchEvents, 此函数处理 Touch 的事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31private bool ProcessTouchEvents()
{
for (int i = 0; i < input.touchCount; ++i)
{
Touch touch = input.GetTouch(i);
if (touch.type == TouchType.Indirect)
continue;
bool released;
bool pressed;
var pointer = GetTouchPointerEventData(touch, out pressed, out released);
ProcessTouchPress(pointer, pressed, released);
if (!released)
{
ProcessMove(pointer);
ProcessDrag(pointer);
}
else
RemovePointerData(pointer);
}
return input.touchCount > 0;
}ProcessTouchPress, 处理 Touch 的按下或释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
{
var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
if (pressed)
{
pointerEvent.eligibleForClick = true;
pointerEvent.delta = Vector2.zero;
pointerEvent.dragging = false;
pointerEvent.useDragThreshold = true;
pointerEvent.pressPosition = pointerEvent.position;
pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;
DeselectIfSelectionChanged(currentOverGo, pointerEvent);
if (pointerEvent.pointerEnter != currentOverGo)
{
HandlePointerExitAndEnter(pointerEvent, currentOverGo);
pointerEvent.pointerEnter = currentOverGo;
}
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
if (newPressed == null)
newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
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;
pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);
if (pointerEvent.pointerDrag != null)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
m_InputPointerEvent = pointerEvent;
}
if (released)
{
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
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;
ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler);
pointerEvent.pointerEnter = null;
m_InputPointerEvent = pointerEvent;
}
}ProcessMouseEvent,同 ProcessTouchEvents 函数一样,只是输入源是鼠标。
ProcessMousePress,同 ProcessTouchPress 函数一样,只是处理输入源不一样。
事件的处理流程如下:
首先 EventSystem 的 Update 处理了输入模块的切换,以及调用了当前输入模块的 Process 函数,然后在 StandaloneInputModule 输入模块中处理所有的输入,并在输入模块中通过射线器去更新当前指针击中的的对象和指针的位置,并处理了所有的事件。
射线器
射线器负责根据输入的位置发射射线或通过区域检查的方式检查对象是否被击中。
BaseRaycaster, 此类是射线发射器的基类,主要功能是定义了排序优先级,Raycast 函数以及在 OnEnable 和 OnDisable 的时候将自己自己添加到 RaycasterManager 中或从 RaycasterManager 中移除。
- eventCamera: 射线器使用的相机。
- rootRaycaster: 缓存顶层父节点的射线器。
- sortOrderPriority:排序优先级(sortingOrder)
- renderOrderPriority:渲染优先级 (renderOrder)
PhysicsRaycaster, 继承至 BaseRaycaster,主要功能是负责 3D 射线的相关功能,其核心函数就是父类定义的抽象函数 Raycast。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
Ray ray = new Ray();
float distanceToClipPlane = 0;
if (!ComputeRayAndDistance(eventData, ref ray, ref distanceToClipPlane))
return;
int hitCount = 0;
if (m_MaxRayIntersections == 0)
{
if (ReflectionMethodsCache.Singleton.raycast3DAll == null)
return;
m_Hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, finalEventMask);
hitCount = m_Hits.Length;
}
else
{
if (ReflectionMethodsCache.Singleton.getRaycastNonAlloc == null)
return;
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 > 1)
System.Array.Sort(m_Hits, (r1, r2) => r1.distance.CompareTo(r2.distance));
if (hitCount != 0)
{
for (int b = 0, bmax = hitCount; b < bmax; ++b)
{
var result = new RaycastResult
{
gameObject = m_Hits[b].collider.gameObject,
module = this,
distance = m_Hits[b].distance,
worldPosition = m_Hits[b].point,
worldNormal = m_Hits[b].normal,
screenPosition = eventData.position,
index = resultAppendList.Count,
sortingLayer = 0,
sortingOrder = 0
};
resultAppendList.Add(result);
}
}
}Physics2DRaycaster, 继承至 PhysicsRaycaster 类,其核心函数也是 Raycast 函数,功能和基类的 Raycast 函数相同只是使用的是 Physics2D。
RaycastResult, 此结构体是用于存储射线器击中结果的信息,具体信息包括:
- gameObject: 击中的对象
- module: 使用哪个射线器击中的
- distance: 距离相机的距离
- index: 在所用击中列表的索引值
- worldPosition: 击中位置的事件坐标
- worldNormal: 击中位置的法线
- screenPosition: 射线发出时的屏幕位置
GraphicRaycaster, 继承至 PhysicsRaycaster 类的图像射线器,通过矩形区域检查是否击中,必须挂在 Canvas 对象上,核心函数也是 Raycast。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
if (canvas == null)
return;
var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
if (canvasGraphics == null || canvasGraphics.Count == 0)
return;
int displayIndex;
var currentEventCamera = eventCamera;
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;
}
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 (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
return;
float hitDistance = float.MaxValue;
Ray ray = new Ray();
if (currentEventCamera != null)
ray = currentEventCamera.ScreenPointToRay(eventPosition);
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;
}
}
}
m_RaycastResults.Clear();
Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
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)
{
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
}
else
{
var cameraFoward = currentEventCamera.transform.rotation * Vector3.forward;
var dir = go.transform.rotation * Vector3.forward;
appendGraphic = Vector3.Dot(cameraFoward, dir) > 0;
}
}
if (appendGraphic)
{
float distance = 0;
Transform trans = go.transform;
Vector3 transForward = trans.forward;
if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
distance = 0;
else
{
distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));
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,
worldPosition = ray.origin + ray.direction * distance,
worldNormal = -transForward
};
resultAppendList.Add(castResult);
}
}
}
private static void Raycast(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, IList<Graphic> foundGraphics, List<Graphic> results)
{
int totalCount = foundGraphics.Count;
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];
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));
totalCount = s_SortedGraphics.Count;
for (int i = 0; i < totalCount; ++i)
results.Add(s_SortedGraphics[i]);
s_SortedGraphics.Clear();
}RaycasterManager, 管理所有射线器,当射线器启动时会加到 RaycasterManager 中,核心函数就是 AddRaycaster, RemoveRaycasters 和 GetRaycasters 函数。
事件系统
事件系统负责处理输入,发射射线并发出事件。
IEventSystemHandler, 事件接口顶层类,派生了 IPointerEnterHandler,IPointerExitHandler,IPointerDownHandler,IPointerUpHandler 和 IPointerClickHandler 接口等。
EventTrigger, 此类实现了所有的事件接口,提供了一种在 Unity 对象上挂事件回调的方法。
ExecuteEvents, 此类是为了在对象上执行事件接口的一个便利类,核心函数 Excute 在指定的对象上执行指定的接口函数,ExecuteHierarchy 函数从子一直往上执行指定的接口函数,GetEventHandler 函数回去指定对象或它的上级对象上第一个能有指定事件的对象。
AbstractEventData, 事件数据的顶层基类,此类定义了一个事件是否使用的标记。
BaseEventData, 事件数据的基类,此类定义了关联的 EventSystem,BaseInputModule 和当前选择的对象。
AxisEventData, 输入控制器(手柄之类)或键盘关联的轴事件数据,主要包含了轴的移动向量和移动方向。
PointerEventData, 触摸或鼠标事件关联的数据类,此数据类是整个事件系统的核心数据类,包含类如下的数据:
- pointerId: 指针 ID
- position: 当前指针位置
- delta:最后一次指针移动的偏移量
- pressPosition:指针按下时的位置
- clickTime:最后一次点击事件,主要用于双击的检查
- clickCount:点击次数
- scrollDelta:滚轮的偏移量
- useDragThreshold:是否使用拖拽阈值,如果不想使用拖拽阈值可以在 IInitializePotentialDragHandler.OnInitializePotentialDrag 函数里设置为 false
- dragging:是否在拖拽中
- IsPointerMoving(): 指针是否在移动中
- IsScrolling(): 滚轮是否在滚动中
- button: 当前指针使用的按钮(Left, Right, Middle)
- pointerEnter: 当前指针进入的对象(有 OnPointerEnter 的对象)
- pointerPress: 当前指针按下的对象 (有 OnPointerDown 的对象)
- lastPress: 上一次指针按下的对象(不一定有 OnPointerDown 的对象)
- rawPointerPress: 当前指针按下时的对象(不一定有 OnPointerDown 的对象)
- pointerDrag: 当前指针拖拽的对象(有 OnDrag 的对象)
- pointerCurrentRaycast: 当前事件关联的射线击中结果对象 (RaycastResult)
- pointerPressRaycast: 与指针按下时关联的射线击中结果对象 (RaycastResult)
- hovered: 保存指针移动时,射线击中的对象。指针进入一个对象或离开一个对象时都会向此字段中添加或移除对象
- eligibleForClick: 在指针谈起时,是否有资格进行点击操作(拖拽的时候不执行点击操作)
EventSystem, 是驱动整个系统更新的入口,EventSystem 管理了所有的输入模块但只有当前输入模块有效,我们也可以在场景中添加多个 EventSystem 但也只有第一个有效。EventSystem 记录当前选择的对象和首个选择的对象,可以通过 SetSelectedGameObject 进行当前选择对象的切换,并且会对应对象发送 OnDeselect 和 OnSelect 事件。 核心函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80public 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;
}
}
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();
}
public void RaycastAll(PointerEventData eventData, List<RaycastResult> 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);
}
本文通过对 UGUI 源码的阅读,了解了整个 UI 系统的处理流程,其中包括了显示组件,布局系统和事件系统三大主要模块,UGUI 还有一些控件比如像 ScrollRect, InputFields, Button 和 Slider 等,这里就没再继续阅读它们的代码了,因为这些控件都是基于三大模块的组合并额外加入了每个控件的自身逻辑,比如像 ScrollRect 就在三大模块的基础上加了滚动的处理。

在没有相机(ScreenSpaceOverlay 渲染模式)的情况下是在所有渲染完成后在进行单独渲染的,所以能够保证 Canvas 在最上面。 






