基础回顾
在正式分析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个阶段:
CanvasUpdateRegistry类在其构造函数中监听了Canvas将要渲染的事件,当收到此事件后则会进行上述几个阶段的更新,核心代码如下: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
}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// 此类是个单例
protected CanvasUpdateRegistry()
{
Canvas.willRenderCanvases += PerformUpdate;
}
//核心的更新函数,所有需要更新的Canvas元素都不被添加到m_LayoutRebuildQueue或m_GraphicRebuildQueue列表中,如果没有Canvas元素Dirty则队列里面是空
private void PerformUpdate()
{
//在Profile里可以查看完成一次完整的更新需要多少时间
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;
//布局完后进行裁剪剔除 RectMask2D的方式,与传统的Mask使用模板(Stencil)缓冲不同,RectMask2D是直接在应用层级根据Mask的矩形区域直接进行裁剪
ClipperRegistry.instance.Cull();
//开始Graphic更新
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);
}
}
}
//Graphic更新完成
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);
}
//Canvas元素的布局Dirty会调用此函数记录一下应该对Canvas元数进行布局更新
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}
//Canvas元素的Graphic Dirty会调用此函数记录一下应该对Canvas元数进行Graphic更新
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)
{
// 此函数会调用DoMeshGeneration进行Mesh的构建
UpdateGeometry();
m_VertsDirty = false;
}
if (m_MaterialDirty)
{
// 更新材质到CanvasRenderer
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()
{
// 矩形有大小的大小的时候,才去构建Mesh,否则就清空顶点
if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
OnPopulateMesh(s_VertexHelper);
else
s_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw.
// 这里是获取修改顶点的组件 这类组件的基类是class BaseMeshEffect:UIBehaviour, IMeshModifier
var components = ListPool<Component>.Get();
GetComponents(typeof(IMeshModifier), components);
// 具体的顶点修改在BaseMeshEffect的子类的ModifyMesh函数里进行,具体有哪些Mesh效果的子类,在下一节进行介绍
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);
ListPool<Component>.Release(components);
// 将顶点信息,填充到Mesh里, 并设置个CanvasRenderer对象
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));
// 定义顶点构成的三角形,这里构建了2个三角形,刚好构成一个Quad
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
{
// 获取一个Component的列表, 并回去自己对象上实现了IMaterialModifier接口的组件
var components = ListPool<Component>.Get();
GetComponents(typeof(IMaterialModifier), components);
// 调用IMaterialModifier接口的GetModifiedMaterial的函数,此函数返回修改后的材质
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
85
86// Mask类的GetModifiedMaterial函数
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
// 检查是否启用了Mask组件
if (!MaskEnabled())
return baseMaterial;
// 在父节点中获取最近的overringsort的Canvas如果没则获取最顶层的Canvas
var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
// 获取自己在继承结构中的深度,用于后续的Stencil计算
var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
// 蒙版不能嵌套超过8层(因为stencil buffer每个像素只有8个bit)
if (stencilDepth >= 8)
{
Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
return baseMaterial;
}
// 定义蒙版的Bit值,用于后续的计算,每个深度的Mask占用一位bit
int desiredStencilBit = 1 << stencilDepth;
// 如果是最外层的Mask,我们则直接替换stencil buffer中的值为1
if (desiredStencilBit == 1)
{
// 根据基础材质,创建带stencil参数的材质,直接替换stencil的值为1,并且根据如果要显示模板对应的显示对象,那么就让让所有颜色通道通过,否则就不通过任何颜色通道
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial;
// 当Mask下的子对象渲染完后,Unity会为我们再次渲染这个Mask对象,作用是为了清除设置的stencil buffer中的对应值。最外层的Mask的子对象绘制完成后,直接就将stencil buffer的值清零了。
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;
}
// 构建写入内层Mask的材质,这里把stencil值写入后,在渲染它的子对象时,需要匹配stencil buffer的值,在MaskableGraphic.GetModifiedMaterial中根据m_StencilValue的进行模板测试,如果模板测试未通过则不绘制制定区域,如果通过则绘制
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;
// 构建内层清理stencil buffer值的材质
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;
}
// MaskableGraphic类的GetModifiedMaterial函数
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
var toUse = baseMaterial;
// 根据层次结构(Hierarchy)的深度获取m_StencilValue,如果不需要maskable这直接使用0
if (m_ShouldRecalculateStencil)
{
if (maskable)
{
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
m_StencilValue = MaskUtilities.GetStencilDepth(transform, rootCanvas);
}
else
m_StencilValue = 0;
m_ShouldRecalculateStencil = false;
}
// 如果需要进行模板计算则构建新的stencil材质, isMaskingGraphic表示要排除用于模板绘制的显示对象,如果stencil buffer值和参考值相等,则通过次像素,否则不通过, stencil buffer值是在Mask.GetModifiedMaterial函数中生成的材质设置的
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;
}
// 如果不用使用stencil材质,则直接使用原材质进行渲染
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;
//省略部分代码
//...... 缓存引用的代码省略
// 设置shader参数
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()
{
// 检查有没有Canvas对象
if (ReferenceEquals(Canvas, null))
{
return;
}
// 当结构改变时需要重新获取RectMask2D的对象列表(存在嵌套)
if (m_ShouldRecalculateClipRects)
{
MaskUtilities.GetRectMasksForClip(this, m_Clippers);
m_ShouldRecalculateClipRects = false;
}
//获取所有RectMask2D的公共区域
bool validRect = true;
Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
// 检查自己的裁剪区域是否和公共的裁剪区域有交集,如果没有则剔除这个RectMask2D
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)
{
// 更新IClippable对象,保留对其他非MaskableGraphic对象的裁剪区域设置
foreach (IClippable clipTarget in m_ClipTargets)
{
clipTarget.SetClipRect(clipRect, validRect);
}
// 对MaskableGraphic对象的裁剪区域设置,并将完全不在这个clipRect内的MaskableGraphic标记为剔除
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)
{
//Case 1170399 - hasMoved is not a valid check when animating on pivot of the object
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的各种形式的缩放。
基础属性:
- 轴心点(Pivot),轴心点定义了物体自身原点的位置以及作为旋转和缩放基点。
- 锚点(Anchors),子对象锚定在父对象上的点,使用RectTransform的anchorMin和anchorMax两个属性表示,anchorMin和anchorMax两个属性的范围是[0,1],anchorMin表示锚定在父对象左下角的位置,anchorMax表示锚定在父对象右上角的位置,值[0,1]表示是锚定在父对象X轴和Y轴上的百分百位置。锚定在父对象上,意思是子对象的Pivot(轴心点)到(父对象上)Anchors(锚点)的相对位置不变。
- 位置,子对象在父对象下的位置,有两种:一种是:localPosition,是基于轴心点的一个本地位置;另一种:anchoredPosition和anchoredPosition3D, 是基于锚点的位置(就是轴心点在锚点空间下的坐标),如下图:
- 偏移(offset),指的是RectTransform的左下角或右上角到锚点的左下角或右上角的偏移值,即RectTransform的offsetMin和offsetMax字段。如下图:
- sizeDelta,表示的是RectTransform的区域与Anchors区域的差值,即offsetMax - offsetMin。在锚点重合的时候,offsetMax - offsetMin刚好是RectTransform的宽度和高度。
- 矩形区域(rect), RectTransform对象的矩形区域通过rect字段表示,其中x,y是RectTransform左下角到轴心点(Pivot)的相对位置,with和height是RectTransform的宽度和高度。
- SetSizeWithCurrentAnchors,此函数设置大小。
- SetInsetAndSizeFromParentEdge,此函数设置边距和大小(会改变锚点(Anchors))。
- RectTransformUtility,此类是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
56
57// 关联LayoutRebuilder
public static void MarkLayoutForRebuild(RectTransform rect)
{
// 检查输入参数
if (rect == null || rect.gameObject == null)
return;
// 找到顶层的布局器,如果没有就把传入的对象标记为需要重建的,如果有顶层的组布局器,子布局器就不用重新计算了,因为父布局器计算算的时候,会递归计算所有子对象(在PerformLayoutCalculation,和PerformLayoutControl函数中会进行递归处理)
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;
}
// 构建一个LayoutRebuilder并且关联传入的RectTransform对象rect,并将其添加到CanvasUpdateRegistry的m_LayoutRebuildQueue列表里,在下一次更新的时候则会计算布局,如果需要立即更新则可以调用LayoutRebuilder.ForceRebuildLayoutImmediate函数。
MarkLayoutRootForRebuild(layoutRoot);
ListPool<Component>.Release(comps);
}
// 进行布局计算并布局
public void Rebuild(CanvasUpdate executing)
{
switch (executing)
{
case CanvasUpdate.Layout:
// 下面的每个函数都会递归调用遍历完所有的子布局元素(ILayoutElement),在遍历每个对象时都会通过调用回调函数将当前操作的对象回传并这里调用当前对象自己的计算和控制布局
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,
// 位置和大小都是通过物理单位(Centimeters, Millimeters, Inches, Points和Picas)标定的
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:
{
// 使用宽,高或宽和高的中间值来缩放Canvas
// We take the log of the relative width and height before taking the average.
// Then we transform it back in the original space.
// the reason to transform in and out of logarithmic space is to have better behavior.
// If one axis has twice resolution and the other has half, it should even out if widthOrHeight value is at 0.5.
// In normal space the average would be (0.5 + 2) / 2 = 1.25
// In logarithmic space the average is (-1 + 1) / 2 = 0
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:
{
// 保证缩放后Canvas的大小不小于设计分辨率(保证能够铺满全屏,但是会存在裁剪)
scaleFactor = Mathf.Min(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);
break;
}
case ScreenMatchMode.Shrink:
{
// 保证缩放后的Canvas的大小不大于设计分辨率(保证设计的类容全部显示,但是会有黑边)
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
60
61// 派生类都调用此函数进行处理指针的进入和退出
protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget)
{
// 如果没有新的指针进入对象或当前没有指针进入对象,则清除所有被追踪的对象
if (newEnterTarget == null || currentPointerData.pointerEnter == null)
{
// 执行对象上的OnPointerExit函数
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;
}
}
// 如果当前指针进入的对象和新目标对象像同,则说明没有切换目标当前事件数据的hovered没有变化,直接返回,否则就需要处理进入和退出
if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget)
return;
// 查找事件数据的当前对象和新对象是否具有相同的父节点,目的是为了减少添加移除的数量,如果有公共父加节点时,只需要处理到公共父节点就可以了
GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);
// 如果当前事件数据对象中已经有指针进入的对象了,现在指针进入了新对象,那么就需要将当前事件数据对象中的已经标记为指针进入的对象移除并调用它们OnPointerExit
if (currentPointerData.pointerEnter != null)
{
Transform t = currentPointerData.pointerEnter.transform;
while (t != null)
{
// 处理到公共父节点,如果没有则直接处理到最顶层的父节点
if (commonRoot != null && commonRoot.transform == t)
break;
// 执行退出函数OnPointerExit
ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);
// 移除老的对象
currentPointerData.hovered.Remove(t.gameObject);
t = t.parent;
}
}
// 将新的对象加入到hovered中
currentPointerData.pointerEnter = newEnterTarget;
if (newEnterTarget != null)
{
Transform t = newEnterTarget.transform;
// 也是处理到公共父就不处理了,因为公共父节点上面的节点已经在hovered里了(前面移除就没有移除公共父节点以上的节点)
while (t != null && t.gameObject != commonRoot)
{
// 同样的执行进入OnPointerEnter
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)
{
// 从m_PointerData中获取指针数据
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)
{
// 获取当前TouchID对应的指针数据,如果没有则创建一个指正数据
PointerEventData pointerData;
var created = GetPointerData(input.fingerId, out pointerData, true);
pointerData.Reset();
// 检查当前Touch的数据状态
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;
// 将button类型设置为Left, 主要是兼容鼠标的(Left,Middle和Right)
pointerData.button = PointerEventData.InputButton.Left;
// 如果不是取消则通过EventSystem的RaycastAll检查第一个击中的对象,并记录到当前指针事件数据的pointerCurrentRaycast中
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)
{
// 检查是否需要处理Drag,指针没有移动或者没有拖拽对象的时
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;
}
// 调用OnDrag函数
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.dragHandler);
}
}
- GetPointerData: 此函数是从m_PointerData列表中获取或创建一个PointerEventData对象。
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()
{
// 当APP不是当前激活程序时,且又应该忽略事件在没有聚焦时候则返回,不用进行处理
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
return;
// 像当前选中的对象发送OnUpdateSelected
bool usedEvent = SendUpdateEventToSelectedObject();
// 有Touch输入设备则优先处理Touch的事件,如果没有Touch设备且有鼠标则处理鼠标的事件
if (!ProcessTouchEvents() && input.mousePresent)
ProcessMouseEvent();
// 是否应该发送导航事件(move, submit和cancel)
if (eventSystem.sendNavigationEvents)
{
// 事件没有被吞掉时发送OnMove
if (!usedEvent)
usedEvent |= SendMoveEventToSelectedObject();
// 事件没有被吞掉时发送OnSubmit事件
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()
{
// 处理Touch输入
for (int i = 0; i < input.touchCount; ++i)
{
// 遍历的当前Touch
Touch touch = input.GetTouch(i);
// 如果是间接(远程)的Touch,则不处理
if (touch.type == TouchType.Indirect)
continue;
// 获取当前Touch的指针位置的事件数据,并返回是按下还是释放
bool released;
bool pressed;
var pointer = GetTouchPointerEventData(touch, out pressed, out released);
// 处理Touch的按下或释放
ProcessTouchPress(pointer, pressed, released);
// 如果没有释放,则就处理父类定义的处理移动和处理拖拽,释放了则移除当前TouchID对应的指针事件数据
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;
// 处理Touch按下时
if (pressed)
{
// 重置指针事件数据
pointerEvent.eligibleForClick = true;
pointerEvent.delta = Vector2.zero;
pointerEvent.dragging = false;
pointerEvent.useDragThreshold = true;
pointerEvent.pressPosition = pointerEvent.position;
pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;
// 检查如果当前选中的对象是否发生改变如果发生则更改EventSystem中的当前选中对象并发送
DeselectIfSelectionChanged(currentOverGo, pointerEvent);
// 当前的OnEnter对象不是当前射线击中的对象时,所以击中对象改变了,则需要更新指针事件数据的hoverd列表
if (pointerEvent.pointerEnter != currentOverGo)
{
HandlePointerExitAndEnter(pointerEvent, currentOverGo);
pointerEvent.pointerEnter = currentOverGo;
}
// 在当前的对象继承结构中查找并执行OnPointerDown函数
var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
if (newPressed == null)
newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
// 处理双击
float time = Time.unscaledTime;
// 如果当前按下的对象和一次按下的对象相同,则需要统计点击次数,如果两次点击的间隔在0.3秒以下则可以识别为双击
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);
// 如果有需要拖拽的对象,则调用OnInitializePotentialDrag函数
if (pointerEvent.pointerDrag != null)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
// 缓存当前的指针事件对象
m_InputPointerEvent = pointerEvent;
}
// 处理Touch释放时
if (released)
{
// 执行OnPointerUp函数
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
// 获取指针释放时有OnPointerClick的对象
var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
// 如果指针按下时的对象和当前有OnPointerClick的对象相同,则说明按下和释放在同一个对象,则需要处理点击事件。在启动拖拽的时候eligibleForClick被设置成了false,也就是拖拽和点击时不可以同时出现的
if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
{
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
}
// 如果没有点击的则检查是否有拖拽的对象,有并且在拖拽中释放的Touch则执行OnDrop
else if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
{
ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
}
// 释放后则重置按下的对象为空
pointerEvent.eligibleForClick = false;
pointerEvent.pointerPress = null;
pointerEvent.rawPointerPress = null;
// 释放的时候再拖拽中,则还要执行OnEndDrag
if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);
// 重置拖拽状态
pointerEvent.dragging = false;
pointerEvent.pointerDrag = null;
// 在已经指针进入的对象上执行OnPointerExit,请清除了当前指针事件数据中的pointerEnter
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
64
65// 通过物理射线进行击中检查,并将结果放入resultAppendList中
public 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;
// 最大击中数量,0:表示发送射线时分配,即调用Physics.RaycastAll时,由此函数分配RaycastHit数组
if (m_MaxRayIntersections == 0)
{
// 通过放射的方式获取了Physics.RaycastAll函数
if (ReflectionMethodsCache.Singleton.raycast3DAll == null)
return;
// 调用Physics.RaycastAll,并将结果缓存到m_Hits中
m_Hits = ReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, finalEventMask);
hitCount = m_Hits.Length;
}
// 使用优化的版本,事先分配好RaycastHit数组
else
{
// 通过反射方式获取Physics.RaycastNonAlloc函数
if (ReflectionMethodsCache.Singleton.getRaycastNonAlloc == null)
return;
// 根据设置的大小分配RaycastHit数组
if (m_LastMaxRayIntersections != m_MaxRayIntersections)
{
m_Hits = new RaycastHit[m_MaxRayIntersections];
m_LastMaxRayIntersections = m_MaxRayIntersections;
}
// 调用Physics.RaycastNonAlloc函数获取击中的结果
hitCount = ReflectionMethodsCache.Singleton.getRaycastNonAlloc(ray, m_Hits, distanceToClipPlane, finalEventMask);
}
// 根据到摄像机的距离进行排序
if (hitCount > 1)
System.Array.Sort(m_Hits, (r1, r2) => r1.distance.CompareTo(r2.distance));
// 生成射线结果对象RaycastResult,并添加到返回列表中
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)
{
// 没有Canvas或Canva下没有对象则不进行射线检查
if (canvas == null)
return;
var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
if (canvasGraphics == null || canvasGraphics.Count == 0)
return;
// 获取Canvas显示在哪个显示设备上(可能会存在多个显示设备)
int displayIndex;
var currentEventCamera = eventCamera;
// 在屏幕覆盖模式或没有相机的时候,根据Canvas中的targetDisplay来确定显示设备索引,如果有相机则使用相机中指定的显示设备索引
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;
// 屏蔽2D或3D对象的最小距离, 使用的方式是只要选择屏蔽对象类型(2D,3D和ALL),就通过射线去打,大于最近的这个击中距离的Graphic对象就会被排除,只个只对带相机的Canvas有效
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;
}
}
}
// 开始进行图像射线击中检查,通过检查指针当前点是否在Graphic的矩形区域内
m_RaycastResults.Clear();
Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
// 在图像射线击中的结果中,筛选满足条件的Graphic
int totalCount = m_RaycastResults.Count;
for (var index = 0; index < totalCount; index++)
{
var go = m_RaycastResults[index].gameObject;
bool appendGraphic = true;
// 如果忽略了背向的Graphic,则需要剔除那些背向相机的
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;
}
}
// 筛选后可以进行事件检查的Graphic
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));
// 距离小于零表示在摄像机后面,不可见
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)
{
// Canvas下的所有Graphic对象
int totalCount = foundGraphics.Count;
for (int i = 0; i < totalCount; ++i)
{
Graphic graphic = foundGraphics[i];
// 剔除不需要被射线检的(Canvas已经处理的,不需要射线的,已经被剔除的)
// -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;
// 此函数会调用ICanvasRaycastFilter接口的IsRaycastLocationValid函数检查并过滤掉无效的对象,没有ICanvasRaycastFilter接口的对象则不需要进行过滤检查
if (graphic.Raycast(pointerPosition, eventCamera))
{
s_SortedGraphics.Add(graphic);
}
}
// 根据CanvasRenderer.absoluteDepth的绝对深度进行排序。
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
// 将排序后的Graphic对象添加到结果列表中
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
80
81// 启动的时候OnEnable中,调用此函数缓存了所有的输入模块,并将没有被激活的输入模块移除掉。
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;
// 此函数遍历所有的输入模块,并调用输入模块的UpdateModule函数,UpdateModule函数主要处理应用程序失去焦点时释放按下的指针,并更新输入模块中记录的当前指针位置
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();
// 在RaycasterManager中获取所有获取所有激活的射线器,并调用射线器中的Raycast函数进行击中检查
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);
}
// 将所射线器击中的对象进行排序,排序规则为:
// 1. 如果是不同的射线器且都有相机则优先使用相机的深度进行排序,没有相机或相机深度相同时则根据Canvas的sortOrder, 期次根据Canvas的renderOrder
// 2. 如果射线器相同,则优先根据Canvas的sortingLayer排序,再根据Canvas的sortingOrder排序,再根据Graphic的depth排序,再根据对象的距离排序,最后根据击中结果列表的索引来进行排序
raycastResults.Sort(s_RaycastComparer);
}
总结
本文通过对UGUI源码的阅读,了解了整个UI系统的处理流程,其中包括了显示组件,布局系统和事件系统三大主要模块,UGUI还有一些控件比如像ScrollRect, InputFields, Button和Slider等,这里就没再继续阅读它们的代码了,因为这些控件都是基于三大模块的组合并额外加入了每个控件的自身逻辑,比如像ScrollRect就在三大模块的基础上加了滚动的处理。