README.md 54.5 KB
Newer Older
Kerven_HKW's avatar
Kerven_HKW 已提交
1 2
UnityUGUISourceCode:
# Unity UGUI 源码剖析
kerven_hankangwen's avatar
kerven_hankangwen 已提交
3

Kerven_HKW's avatar
Kerven_HKW 已提交
4 5 6 7 8 9 10 11 12 13 14
## 01 | 从Button开始解读UGUI源码
在Unity中,Button是我们最常用的组件之一了,它使用起来非常简单,例如监听一个Button点击事件。
```csharp
GetComponent<Button>().onClick.AddListener(() => { 
            //按钮点击的处理逻辑
        });
```
这样使用没有任何问题,但有时候我们会有疑问,为什么点击按钮onClick事件就会被触发呢?如何从回调函数中获取按钮的参数?让我们从源码中寻找答案。
### 查看源码前的配置
为了能够方便的查阅源码以及进行代码调试,需要重新导入UGUI包。新建Unity项目,找到Project/Packages/Unity UI,右键 Show in Explorer,将其复制到任意一个新的文件夹中(记住保存的位置,待会需要引用)。
接下来打开Window/Package Manager
kerven_hankangwen's avatar
kerven_hankangwen 已提交
15

Kerven_HKW's avatar
Kerven_HKW 已提交
16
![](https://img-blog.csdnimg.cn/img_convert/a1727f7851ec477841c983450684bd05.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
17

Kerven_HKW's avatar
Kerven_HKW 已提交
18
找到Unity UI,将其Remove
kerven_hankangwen's avatar
kerven_hankangwen 已提交
19

Kerven_HKW's avatar
Kerven_HKW 已提交
20
![](https://img-blog.csdnimg.cn/img_convert/4cb124629d9e0e0afa9ddb1891694c36.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
21

Kerven_HKW's avatar
Kerven_HKW 已提交
22
然后点击“+”号,选择Add package form disk...,找到之前保存的UI包,进入目录后选中package.json,点击打开。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
23

Kerven_HKW's avatar
Kerven_HKW 已提交
24
![](https://img-blog.csdnimg.cn/img_convert/37e4d85eb79c7dd02fe862b1a940a4b7.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
25

Kerven_HKW's avatar
Kerven_HKW 已提交
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
大功告成,现在我们可以查看/修改UGUI的源码了。
### 探究UGUI源码
#### Button的调用链
通过F12打开Button代码,容易发现它继承Selectable类,同时还继承了IPointerClickHandler、ISubmitHandler接口,这两个接口分别会在鼠标点击、点击提交按钮时调用它们的回调函数。
```csharp
 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字段。
```csharp
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事件,如下图所示
kerven_hankangwen's avatar
kerven_hankangwen 已提交
89

Kerven_HKW's avatar
Kerven_HKW 已提交
90
![](https://img-blog.csdnimg.cn/img_convert/519d0c4871d11a9690997d38ba4d3818.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
91

Kerven_HKW's avatar
Kerven_HKW 已提交
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
继续往上找,ExecuteEvents类中还定义了一个EventFunction<T1>的泛型委托以及该委托类型的属性,这个返回s_PointerClickHandler,要查找谁触发的点击事件,只需要找到谁调用了pointerClickHandler即可

```csharp
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

```csharp
// 计算和处理任何鼠标按钮状态的变化
//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多个重载函数

```csharp
//target : 需要执行事件的游戏对象
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
    var internalHandlers = s_HandlerListPool.Get();
    //获取target对象的事件
    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事件。 如下图所示(为了简洁,类图并不完整)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
176

Kerven_HKW's avatar
Kerven_HKW 已提交
177
![](https://img-blog.csdnimg.cn/img_convert/4cba69e91cd41220f9ba44acd3942274.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
178

Kerven_HKW's avatar
Kerven_HKW 已提交
179 180 181 182 183
> ReleaseMouse()是否只有鼠标左键抬起才会触发?
鼠标左、中、右键都会触发该函数,只不过Button在实现OnPointerClick()函数时忽略了鼠标中键和右键,使得只有左键能触发Button的点击事件
但现在还存在一个问题,怎么知道上述代码中事件执行目标target的值呢?探究这个问题之前,我们需要先对UGUI源码有个总体的认识,因为它涉及的知识点比较多。
### 事件系统整体概述
我们先看EventSystem源码在文件夹中的分类
kerven_hankangwen's avatar
kerven_hankangwen 已提交
184

Kerven_HKW's avatar
Kerven_HKW 已提交
185
![](https://img-blog.csdnimg.cn/img_convert/bb565f4a45080c6aa1682694ae0c3b3b.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
186

Kerven_HKW's avatar
Kerven_HKW 已提交
187 188 189
从图中就可以看出主要包含三个子板块,分别是EvnetData、InputModules和Raycasters。

再看一个整体的类图,类图中包括了许多重要的类,如EventSystem、BaseRaycast、BaseInputModule等,它们都是继承自UIBehaviour,而UIBehaviour又是继承MonoBehaviour。(类图并不完整,只涉及部分类)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
190

Kerven_HKW's avatar
Kerven_HKW 已提交
191
![在这里插入图片描述](https://img-blog.csdnimg.cn/2e153c483c0f44569317b0e15e004560.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
192

Kerven_HKW's avatar
Kerven_HKW 已提交
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
接下来对这些内容进行详细讲解。

EventSystem类
事件系统主要是基于输入(键盘、鼠标、触摸或自定义输入)向应用程序中的对象发送事件,当然这需要其他组件的配合。当你在GameObject中添加EventSystem时,你会发现它并没有太多的功能,这是因为EventSystem本身被设计成事件系统不同模块之间通信的管理者和推动者,它主要包含以下功能:

管理哪个游戏对象被认为是选中的
管理正在使用的输入模块
管理射线检测(如果需要)
根据需要更新所有输入模块
### 管理输入模块
下面看一下具体代码。首先是声明了BaseInputModule类型的List和变量,用来保存输入模块(Module)

```csharp
//系统输入模块
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)事件,部分代码如下

```csharp
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;
}
```
### 管理射线检测
EventSystem中,还有一个非常重要的函数RaycastAll(),主要是获取目标。它被PointerInputModule类调用,大致来说是当鼠标设备可用或触摸板被使用时调用。

```csharp
public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
 {
     raycastResults.Clear();
     //获取BaseRaycast对象
     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);
 }
```
它首先获取所有的BaseRaycast对象,然后调用它的Raycast方法,用以获取屏幕某个点下的所有目标(这个方法具体功能及实现的会在Raycast模块中进行讲解),最后对得到的结果进行排序,大部分情况都是根据深度(Depth)进行排序,在一些情况下也会使用距离(Distance)、排序顺序(SortingOrder,如果是UI元素则是根据Canvas面板的Sort order值,3D物体默认是0)或者排序层级(Sorting Layer)等作为排序依据。

讲了这么一大堆,来张图总结一下。EventSystem会在Update中调用输入模块的Process方法来处理输入消息,PointerInputModule会调用EventSystem中的RaycastAll方法进行射线检测,RaycastAll又会调用BastRaycaster的Raycast方法执行具体的射线检测操作,主要是获取被选中的目标信息。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
306

Kerven_HKW's avatar
Kerven_HKW 已提交
307
![在这里插入图片描述](https://img-blog.csdnimg.cn/fb6d708a56224ffdad54476de2e52afa.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
308

Kerven_HKW's avatar
Kerven_HKW 已提交
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
简单概括一下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接口配合。这个类中定义了许多接口,比如鼠标按下、点击、拖拽等,下图展示了部分接口的继承关系。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
338

Kerven_HKW's avatar
Kerven_HKW 已提交
339
![在这里插入图片描述](https://img-blog.csdnimg.cn/d39029d381724c579603a51f9d30b679.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
340

Kerven_HKW's avatar
Kerven_HKW 已提交
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
ExecuteEvent类中提供了一个方法让外部统一调用以执行事件

```csharp
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
    //从对象池中取出一个IEventSystemHandler类型的元素
    var internalHandlers = s_HandlerListPool.Get();
    //获取指定对象(target)的事件,并保存在internalHandlers中
    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方法,它主要是通过冒泡的方式查找到能够处理指定事件的对象。

```csharp
// 在游戏对象上冒泡指定的事件,找出哪个对象将实际接收事件。 
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物理元素
kerven_hankangwen's avatar
kerven_hankangwen 已提交
424

Kerven_HKW's avatar
Kerven_HKW 已提交
425
![在这里插入图片描述](https://img-blog.csdnimg.cn/0e44f4ec54dd4e84804ae91338c28157.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
426

Kerven_HKW's avatar
Kerven_HKW 已提交
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
BaseRaycaster是其他Raycaster的基类,这是是一个抽象类。在它OnEnable里将自己注册到RaycasterManager,并在OnDisable的时候从后者移除。

RaycasterManager是一个静态类,维护了一个BaseRaycaster类型的List,功能比较简单,包含获取(Get)、添加(Add)、移除(Remove)方法。

BaseRaycaster中最重要的就是Raycast方法了,它的子类都对该方法进行了重写。
### Physics Raycaster
它主要用于检测3D物理元素,并且保存被射线检测到物体的数据,下面是部分代码

```csharp
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
    //判断是否超出摄像机的远近裁剪平面的距离
    if (!ComputeRayAndDistance(eventData, ref ray, ref displayIndex, ref distanceToClipPlane))
        return;

    //采用ReflectionMethodsCache.Singleton.raycast3DAll()来获取所有射线照射到的对象
    //用反射的方式把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);
    }

    //获取到被射线照射到的对象,根据距离进行排序,然后包装成RaycastResult,加入到resultAppendList中
    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,我们在场景中添加Canvas默认都会包含一个GraphicRaycast组件。它先获取鼠标坐标,将其转换为Camera的视角坐标,然后分情况计算射线的距离(hitDistance),调用Graphic的Raycast方法来获取鼠标点下方的元素,最后将满足条件的结果添加到resultAppendList中。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
481

Kerven_HKW's avatar
Kerven_HKW 已提交
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681
![在这里插入图片描述](https://img-blog.csdnimg.cn/075dc249a77944e4b4b7664db687a790.png)

```csharp
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 it's outside the camera's 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脚本的游戏对象,该函数内容为:

```csharp
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 hasn't been processed by the canvas, which means it isn't 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。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
682

Kerven_HKW's avatar
Kerven_HKW 已提交
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748
![在这里插入图片描述](https://img-blog.csdnimg.cn/5d3603d2ab3d4fbbaab22e01fdea6cd6.png)

```csharp
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;

             //获取ICanvasRaycastFilter组件(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都继承了该接口。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
749

Kerven_HKW's avatar
Kerven_HKW 已提交
750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795
![在这里插入图片描述](https://img-blog.csdnimg.cn/a05d890cd73e4e5fb0b7dc991d990f16.png)

```csharp
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用以存储事件信息,涉及到的东西不多,不展开讲解,层级关系如下图所示
kerven_hankangwen's avatar
kerven_hankangwen 已提交
796

Kerven_HKW's avatar
Kerven_HKW 已提交
797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853
![在这里插入图片描述](https://img-blog.csdnimg.cn/416a223095af40aaaab754b2e26f0b9d.png)
### 实战:为Button的点击事件添加参数
在执行Button点击事件时,有些情况下我们需要获取触发事件的Button对象信息,这时可以自己实现一个Button点击事件

```csharp
/// <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事件中添加自己的处理函数。
### 总结
utton点击事件怎么触发的呢?首先是EventSystem在Update中调用当前输入模块的Process方法处理所有的鼠标事件,并且输入模块会调用RaycastAll来得到目标信息,通过冒泡的方式找到事件实际接收者并执行点击事件(这只是总体流程,中间省略很多具体步骤)。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
854

Kerven_HKW's avatar
Kerven_HKW 已提交
855
![在这里插入图片描述](https://img-blog.csdnimg.cn/44c8538525d14bb2b6cdbdf9a31bd099.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
856

Kerven_HKW's avatar
Kerven_HKW 已提交
857
最后来一张层级关系图
kerven_hankangwen's avatar
kerven_hankangwen 已提交
858

Kerven_HKW's avatar
Kerven_HKW 已提交
859
![在这里插入图片描述](https://img-blog.csdnimg.cn/0cb7a4ba7fa345078c7f404acf6d30c3.png)
Kerven_HKW's avatar
Kerven_HKW 已提交
860

Kerven_HKW's avatar
Kerven_HKW 已提交
861 862 863 864 865
## Unity UI重建(Rebuild)源码分析
## Unity怎么绘制UI元素的?
首先我们需要明白一个问题:Unity是怎么绘制UI元素的?

Unity中渲染的物体都是由网格(Mesh)构成的,而网格的绘制单元是图元(点、线、三角面)。在unity中添加一个Image和Text,并且将Shadings Mode设置为Wireframe模式,可以看到一个Image由四个顶点和两个三角面构成,Text也是由许多顶点和三角面构成。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
866

Kerven_HKW's avatar
Kerven_HKW 已提交
867
![在这里插入图片描述](https://img-blog.csdnimg.cn/4c59fa44bc684b1aa0fd5c2b0c10c1e1.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
868

Kerven_HKW's avatar
Kerven_HKW 已提交
869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908
绘制信息都存储在Vertexhelper类中,除了顶点外,还包括法线、UV、颜色、切线以及一些函数,下面是它的部分代码

```csharp
public class VertexHelper : IDisposable
 {
     private List<Vector3> m_Positions;
     private List<Color32> m_Colors;
     private List<Vector4> m_Uv0S;
     private List<Vector4> m_Uv1S;
     private List<Vector4> m_Uv2S;
     private List<Vector4> m_Uv3S;
     private List<Vector3> m_Normals;
     private List<Vector4> m_Tangents;
     private List<int> m_Indices;

     public void FillMesh(Mesh mesh)
     {
         InitializeListIfRequired();

         mesh.Clear();

         if (m_Positions.Count >= 65000)
             throw new ArgumentException("Mesh can not have more than 65000 vertices");

         mesh.SetVertices(m_Positions);
         mesh.SetColors(m_Colors);
         mesh.SetUVs(0, m_Uv0S);
         mesh.SetUVs(1, m_Uv1S);
         mesh.SetUVs(2, m_Uv2S);
         mesh.SetUVs(3, m_Uv3S);
         mesh.SetNormals(m_Normals);
         mesh.SetTangents(m_Tangents);
         mesh.SetTriangles(m_Indices, 0);
         mesh.RecalculateBounds();
     }
 }
```
数据存储好了,那怎么绘制呢?

这是依靠CanvasRenderer来完成的,它听起来可能比较陌生,但实际上当我们在项目中创建的一些UI元素,比如Button、Image、Text时,都包含组件CanvasRenderer,这个类提供了许多关键绘制信息,比如被渲染物体的颜色、材质和Mesh等,主要作用就是渲染包含在Canvas中的UI对象,但是在Inspector界面中并不会展示任何属性。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
909

Kerven_HKW's avatar
Kerven_HKW 已提交
910
![在这里插入图片描述](https://img-blog.csdnimg.cn/5974159c8e8646f89fe5733c1c460524.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
911

Kerven_HKW's avatar
Kerven_HKW 已提交
912
下面列出了几个比较重要的属性和方法,详情见Unity Documentation: CanvasRenderer。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
913

Kerven_HKW's avatar
Kerven_HKW 已提交
914
![在这里插入图片描述](https://img-blog.csdnimg.cn/a1f0200c340a44c5b134f1709d3b18d6.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
915

Kerven_HKW's avatar
Kerven_HKW 已提交
916 917 918 919 920 921 922 923
总结一下就是Unity会把要绘制的UI信息保存在Vertexhelper中,并且调用CanvasRenderer里面的方法进行绘制,具体的绘制时机,就是今天的重点内容了。

这是UGUI源码系列的第二篇内容,如果没有任何基础,可以先看看文章 UGUI源码入门。

## UI重建(Rebuild)
UI重建分为两类,一类是布局重建(Layout Rebuild),另一类是图形重建(Graphic Rebuild)。

一个UI若要重建,必须继承自ICanvasElement接口,因为执行重建操作的时候会调用接口中的Rebuild函数。CanvasUpdateRegistry类监听了Canvas的willRenderCanvases事件,该事件会每帧调用并执行PerformUpdate函数。PerformUpdate被调用时会遍历RebuildQueue中需要进行重建的UI元素,并调用元素的Rebuild方法。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
924

Kerven_HKW's avatar
Kerven_HKW 已提交
925
![在这里插入图片描述](https://img-blog.csdnimg.cn/75a9da05abde4eb696b193a5146714bd.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
926

Kerven_HKW's avatar
Kerven_HKW 已提交
927 928 929
下面对这些步骤展开详细的讲解。
### ICanvasElement
首先是ICanvasElement接口,重建的时候会调用它的Rebuild方法,继承它的类都会对这个函数进行重写,Unity中几乎所有的UI组件都继承自这个接口。
kerven_hankangwen's avatar
kerven_hankangwen 已提交
930

Kerven_HKW's avatar
Kerven_HKW 已提交
931
![在这里插入图片描述](https://img-blog.csdnimg.cn/ca9b3653d0f04aaa99d3c6e54464561c.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
932

Kerven_HKW's avatar
Kerven_HKW 已提交
933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110
下面是接口中包含的方法。

```csharp
public interface ICanvasElement
{
    // 根据CanvasUpdate的不同阶段重建元素
    void Rebuild(CanvasUpdate executing);

    // 获取ICanvasElement关联的变换组件
    Transform transform { get; }

    // 布局重建完成的回调函数
    void LayoutComplete();

    // 图形重建完成的回调函数
    void GraphicUpdateComplete();

    // 是否被销毁
    bool IsDestroyed();
}
```
可以看到,Rebuild函数需要提供CanvasUpdate类型的参数,它是一个枚举类型,表示Rebuild的不同阶段。

```csharp
public enum CanvasUpdate
{
    //布局重建前
    Prelayout = 0,
    //布局重建
    Layout = 1,
    //布局重建后
    PostLayout = 2,
    //渲染前(图形重建前)
    PreRender = 3,
    //PreRender后,渲染前
    LatePreRender = 4,
    //最大枚举值
    MaxUpdateValue = 5
}
```
### CanvasUpdateRegistry
一个继承自ICanvasElement接口的类如果要重建,需要将自身加入到CanvasUpdateRegistry类中的重建队列中(RebuildQueue,并不是数据结构中的队列),CanvasUpdateRegistry中包含两个索引集(IndexedSet,内部使用Dictionary和List存储数据),分别是

IndexedSet<ICanvasElement> m_LayoutRebuildQueue ,布局重建队列,当UI元素的布局需要更新时将其加入队列
IndexedSet<ICanvasElement> m_GraphicRebuildQueue ,图形重建队列,当UI元素的图像需要更新时将其加入队列
该类在构造函数中监听了Canvas的willRenderCanvases事件,这个事件会在渲染前进行每帧调用,函数大致包含以下步骤:

PerformUpdate函数对m_LayoutRebuildQueue中的元素进行排序,依据是父节点的多少。接下来依次将Prelayout、Layout和PostLayout作为参数传递给Rebuild进行布局重建,完成后通知布局队列中的元素重建完成。
调用ClipperRegistry的Cull函数进行裁剪。
进行图形重建,遍历m_GraphicRebuildQueue的值,分别将参数PreRender、LatePreRender作为参数传递给Rebuild函数进行图形重建。
最后通知图形重建完成。

```csharp
protected CanvasUpdateRegistry()
{
    Canvas.willRenderCanvases += PerformUpdate;
}

private void PerformUpdate()
{
    UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
    //清理Queue中值为null或者被销毁的元素
    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))
                    //布局重建,分别传入 Prelayout 、Layout 、PostLayout参数
                    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;

    //执行裁剪(cull)操作
    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))
                {
                    //图形重建,分别传入PreRender、LatePreRender参数
                    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);
}
```
不论是布局重建还是图形重建,都是遍历m_LayoutRebuildQueue和m_GraphicRebuildQueue中的元素并调用其Rebuild方法,这些UI元素是怎么被添加进Queue的呢?

对于m_LayoutRebuildQueue,提供了两个公开方法向其添加内容,当元素需要进行布局重建的时候,将调用该函数将自身加入队列,m_GraphicRebuildQueue同样也提供了两个函数。

```csharp
//向m_LayoutRebuildQueue中添加元素
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
    instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}

//尝试向m_LayoutRebuildQueue中添加元素
//并返回执行结果(True->成功, False->失败)
public static bool TryRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
    return instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}

//如果队列中不存在element元素,则添加
private bool InternalRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
    if (m_LayoutRebuildQueue.Contains(element))
        return false;

    return m_LayoutRebuildQueue.AddUnique(element);
}

//向m_GraphicRebuildQueue中添加元素
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
    instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}

//尝试向m_GraphicRebuildQueue中添加元素
public static bool TryRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
    return instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}

private bool InternalRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
    if (m_PerformingGraphicUpdate)
    {
        return false;
    }

    return m_GraphicRebuildQueue.AddUnique(element);
}
```
以Graphic为例,我们看一下什么时候会向重建队列中添加元素。
### Graphic
![在这里插入图片描述](https://img-blog.csdnimg.cn/b00c3c9fa6fe4bd1a8fcaad8df1c39ce.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
1111

Kerven_HKW's avatar
Kerven_HKW 已提交
1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293
以Graphic为例(Image和Text间接继承自它),看一下具体发生了什么。
首先是将自身加入重建队列,这里是通过设置“脏数据”实现的,包括布局(Layout)、材质(Material)和顶点(Vertices)三部分,设置布局为脏,将进行布局重建,设置顶点或材质为脏,则进行图形重建。布局重建会将自身加入m_LayoutRebuildQueue中,图形重建则会将自身加入m_GraphicRebuildQueue中,等待被调用。

```csharp
public virtual void SetAllDirty()
{
    if (m_SkipLayoutUpdate)
    {
        m_SkipLayoutUpdate = false;
    }
    else
    {
        SetLayoutDirty();
    }

    if (m_SkipMaterialUpdate)
    {
        m_SkipMaterialUpdate = false;
    }
    else
    {
        SetMaterialDirty();
    }

    SetVerticesDirty();
}

 public virtual void SetLayoutDirty()
 {
     if (!IsActive())
         return;

     //将元素加入布局重建队列
     LayoutRebuilder.MarkLayoutForRebuild(rectTransform);

     Debug.Log("Rebuild:" + rectTransform.name);
     if (m_OnDirtyLayoutCallback != null)
         m_OnDirtyLayoutCallback();
 }


public virtual void SetVerticesDirty()
{
    if (!IsActive())
        return;

    m_VertsDirty = true;   
    //将元素加入图形重建队列
    CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

    if (m_OnDirtyVertsCallback != null)
        m_OnDirtyVertsCallback();
}

public virtual void SetMaterialDirty()
{
    if (!IsActive())
        return;

    m_MaterialDirty = true;
    //将元素加入图形重建队列
    CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

    if (m_OnDirtyMaterialCallback != null)
        m_OnDirtyMaterialCallback();
}
```
加入重建队列之后,CanvasUpdateRegistry就会在PerformUpdate函数中调用它的Rebuild进行重建。Graphic实现了接口ICanvasElement的Rebuild方法,在满足条件的情况下将更新元素的几何网格(UpdateGeometry)和材质(UpdateMaterial)。

```csharp
public 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;
    }
}
```
UpdateGeometry函数用于确定元素的网格(Mesh)信息,这些信息包括顶点、三角面、UV、颜色等,它们将会被填充到s_VertexHelper中,并最终调用canvasRenderer.SetMesh(workerMesh)设置Mesh信息。

```csharp
//调用该函数将图形的几何网格更新到CanvasRenderer上。 
protected virtual void UpdateGeometry()
{
    //Image、RawImage、Text会在构造函数中将其设置为false
    if (useLegacyMeshGeneration)
    {
        DoLegacyMeshGeneration();
    }
    else
    {
        DoMeshGeneration();
    }
}

private void DoMeshGeneration()
{
    if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
        //UI元素需要生成顶点时的回调函数,用以填充顶点缓冲区的数据
        //其子类重写了这个方法
        OnPopulateMesh(s_VertexHelper);
    else
        s_VertexHelper.Clear();

    //获取当前对象是否有IMeshModifier接口,
    //Text的描边和阴影都是通过它的ModifyMesh方法实现的
    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);
}
```
### Image
Image间接继承自Graphic,当它的Sprite发生变化时,会调用SetAllDirty函数

```csharp
public Sprite sprite
{
    get { return m_Sprite; }
    set
    {
        if (m_Sprite != null)
        {
            if (m_Sprite != value)
            {
                m_SkipLayoutUpdate = m_Sprite.rect.size.Equals(value ? value.rect.size : Vector2.zero);
                m_SkipMaterialUpdate = m_Sprite.texture == (value ? value.texture : null);
                m_Sprite = value;

                SetAllDirty();
                TrackSprite();
            }
        }
        else if (value != null)
        {
            m_SkipLayoutUpdate = value.rect.size == Vector2.zero;
            m_SkipMaterialUpdate = value.texture == null;
            m_Sprite = value;

            SetAllDirty();
            TrackSprite();
        }
    }
}
```
设置Sprite大小的时候也会调用

```csharp
public override void SetNativeSize()
 {
     if (activeSprite != null)
     {
         float w = activeSprite.rect.width / pixelsPerUnit;
         float h = activeSprite.rect.height / pixelsPerUnit;
         rectTransform.anchorMax = rectTransform.anchorMin;
         rectTransform.sizeDelta = new Vector2(w, h);
         SetAllDirty();
     }
 }
```
即对应下图中的SetNativeSize按钮
kerven_hankangwen's avatar
kerven_hankangwen 已提交
1294

Kerven_HKW's avatar
Kerven_HKW 已提交
1295
![在这里插入图片描述](https://img-blog.csdnimg.cn/aec02e30650e41de9d59704ac0fd80c7.png)
kerven_hankangwen's avatar
kerven_hankangwen 已提交
1296

Kerven_HKW's avatar
Kerven_HKW 已提交
1297 1298 1299 1300 1301 1302
当然修改Image其他的属性也可能会引发重建,调用的地方太多了,想要进一步了解的同学可以在源码中找到答案。

Text类似,当文本的字体、大小等属性发生变化时,也会引起重建。
## 总结
以下情形都将进行UI重建,因此在项目中可以针对这些情况进行优化,比如用改变UI的Scale(1->0)来代替改变UI的Enable属性,以Image的Scale代替Slider来进行百分比展示等(内容搬运自UGUI UI重建二三事(二) - 知乎 )

kerven_hankangwen's avatar
kerven_hankangwen 已提交
1303 1304 1305 1306 1307 1308 1309 1310
- Text控件 文本的内容及颜色变化、设置是否支持富文本、更改换行模式、设置字体最大最小值、变更文本使用的对齐锚点、设置是否通过几何对齐、变更字体大小、变更是否支持水平及垂直溢出、修改行间距、变更字体样式(正常、斜体.....)。
- Image控件 颜色变化、变更显示类型(Simple、Sliced、Tiled、Filled)、变更是否应保留Sprite宽高比(Image.preserveAspect属性的变更),FillCenter属性变更(是否渲染平铺或切片图像的中心)、变更填充方式(Horizontal、Vertical、Radial360....)、变更图像填充率(fillAmount)、变更图像顺逆时针填充类型(Image.fillClockwise)、变更填充过程的原点(Image.FillOrigin)。
- RawImage控件 设置Texture、变更纹理使用的UVRcet。
- Shadow效果 改变效果的距离(effectDistance)及颜色(effectColor)、变更是否使用Graphic中的Alpha透明度(useGraphicAlpha)。
- Mask控件 设置是否展示与Mask渲染区域相关的图形(showMaskGraphic),enable发生变化
- 所有继承MaskableGraphic的控件(Image、RawImage、RectMask2D、Text) 设置此图形是否允许被遮盖、enable发生变化、父节点发生变化(TransFromParentChanged)、在Hierachy面板上发生改变(HierachyChanged)。
- 所有继承自BaseMeshEffect的效果类(目前只看到Shadow及PositionAsUV1)的enable变化及应用动画属性的操作。
- 所有继承自Graphic的UI控件材质(material)发生变化。