Unity中的UGUI源码解析之图形对象(Graphic)(2)-ICanvasElement

12 篇文章 9 订阅

Unity中的UGUI源码解析之图形对象(Graphic)(2)-ICanvasElement

在上一篇文章中, 我们对整个Graphic部分做了概述, 这篇文章我们介绍ICanvasElementCanvasUpdateRegistry.

ICanvasElement是一个接口(Interface). 抽象了能够在画布上显示的元素行为. 文件所在为: UnityEngine.UI/UI/Core/CanvasUpateRegistry.cs.

相关的辅助类有: enum CanvasUpdateclass CanvasUpdateRegistry.

在Unity中, 有以下元素实现了该接口:

在这里插入图片描述

即:

  • Toggle
  • LayoutRebuilder
  • ScrollRect
  • Graphic
  • InputField
  • Scrollbar
  • MaskableGraphic
  • Image
  • Slider
  • RawImage
  • Text

我们会在后面的文章慢慢介绍这些元素, 在本文中, 主要关注接口本身和其辅助类.

enum CanvasUpdate

CanvasUpdate是一个枚举, 本身很简单, 用于表明元素的刷新时机, 主要在重建(rebuild)和刷新(update)中使用.

/// <summary>
/// 在Canvas更新时调用的更新时机
/// </summary>
public enum CanvasUpdate
{
    /// <summary>
    /// 在布局之前调用
    /// </summary>
    Prelayout = 0,

    /// <summary>
    /// 在布局时调用
    /// </summary>
    Layout = 1,

    /// <summary>
    /// 在布局之后调用
    /// </summary>
    PostLayout = 2,

    /// <summary>
    /// 在渲染之前调用
    /// </summary>
    PreRender = 3,

    /// <summary>
    /// 稍后再渲染之前调用
    /// </summary>
    LatePreRender = 4,

    /// <summary>
    /// 最大值, 用于数组遍历上界
    /// </summary>
    MaxUpdateValue = 5
}

interface ICanvasElement

ICanvasElement抽象了在画布上显示的元素的基本行为, 用于元素在重建(rebuild)和刷新(update)的各个时机定义和触发特定的行为.

/// <summary>
/// 可以在画布上的元素行为接口
/// </summary>
public interface ICanvasElement
{
    /// <summary>
    /// 重新构建给定阶段的元素
    /// 可以将元素的更新延迟到画布刷新之前, 降低更新的消耗
    /// </summary>
    /// <param name="executing"></param>
    void Rebuild(CanvasUpdate executing);

    /// <summary>
    /// 相关联的transform
    /// </summary>
    Transform transform { get; }

    /// <summary>
    /// 此元素完成布局时的回调
    /// </summary>
    void LayoutComplete();

    /// <summary>
    /// 此元素完成Graphic重新构建时的回调
    /// </summary>
    void GraphicUpdateComplete();

    /// <summary>
    /// due to unity overriding null check
    /// we need this as something may not be null
    /// but may be destroyed
    /// 返回元素是否被销毁
    /// </summary>
    /// <returns></returns>
    bool IsDestroyed();
}

class CanvasUpdateRegistry

CanvasUpdateRegistry类是一个单例类, 主要负责驱动UGUI元素的刷新和重建, 是底层的Native代码(Canvas)和UGUI元素之间沟通的桥梁.

CanvasUpdateRegistry十分简单, 主要是维护了两个队列, 一个存放需要在布局更新时(比如位置发生变化时)刷新和重建的UGUI元素, 一个存放需要在图形更新时(也就是渲染内容发生变化时, 比如材质和顶点变化), 然后通过注册一个在画布渲染之前发送的事件, 照着布局更新到图形更新的顺序依次对两个队列进行刷新和重建.

刷新队列是各个UGUI元素自己在合适的时机, 调用CanvasUpdateRegistry单例进行注册和加入的, 在每次刷新之后会清空队列.

主要流程

主要的流程为:

  1. 在CanvasUpdateRegistry构造函数中注册画布渲染前事件: Canvas.willRenderCanvases += PerformUpdate;

  2. 各个UGUI元素在合适的时机注册布局更新队列(RegisterCanvasElementForLayoutRebuild)或者图形更新队列(RegisterCanvasElementForGraphicRebuild)

  3. 触发画布渲染前事件: PerformUpdate

  4. UI系统分析启动: UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);

  5. 清理无效的队列元素(比如那些不再存活的元素): CleanInvalidItems

  6. 标记布局更新中: m_PerformingLayoutUpdate = true;

  7. 布局队列元素排序: m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);

  8. 对布局队列中的有效元素(可能在某个时机中变得无效)分别进行各个时机的重建:

    • Prelayout(0), Layout(1), PostLayout(2);
    • int i = 0; i <= (int)CanvasUpdate.PostLayout; i++
    • ObjectValidForUpdate(rebuild)
    • rebuild.Rebuild((CanvasUpdate)i)
  9. 对布局队列中的元素分别进行布局完成的回调: m_LayoutRebuildQueue[i].LayoutComplete()

  10. 清空布局队列: m_LayoutRebuildQueue.Clear();

  11. 清理布局更新中标记: m_PerformingLayoutUpdate = false;

  12. 进行剔除(cull): ClipperRegistry.instance.Cull();

  13. 标记图形更新中: m_PerformingGraphicUpdate = true;

  14. 对图形队列中的有效元素(可能在某个时机中变得无效)分别进行各个时机的重建:

    • PreRender(3), LatePreRender(4)
    • var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++
    • ObjectValidForUpdate(element)
    • element.Rebuild((CanvasUpdate)i)
  15. 对图形队列中的元素分别进行图形更新完成的回调: m_GraphicRebuildQueue[i].GraphicUpdateComplete()

  16. 清空图形队列: m_GraphicRebuildQueue.Clear();

  17. 清理图形更新中标记: m_PerformingGraphicUpdate= false;

  18. UI系统分析结束: UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);

基本属性和字段

public class CanvasUpdateRegistry
{
    /// <summary>
    /// 单例
    /// </summary>
    private static CanvasUpdateRegistry s_Instance;

    /// <summary>
    /// 是否在更新布局(位置)
    /// </summary>
    private bool m_PerformingLayoutUpdate;

    /// <summary>
    /// 是否在更新图形(渲染)
    /// </summary>
    private bool m_PerformingGraphicUpdate;

    /// <summary>
    /// 需要在布局更新时重建的元素(位置)
    /// </summary>
    private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();

    /// <summary>
    /// 需要在图形更新时重建的元素(渲染)
    /// </summary>
    private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();
    
    /// <summary>
    /// 单例
    /// </summary>
    public static CanvasUpdateRegistry instance
    {
        get
        {
            if (s_Instance == null)
                s_Instance = new CanvasUpdateRegistry();

            return s_Instance;
        }
    }
}

注册元素和回调

// 构造函数中注册即将开始渲染画布时的回调
protected CanvasUpdateRegistry()
{
    Canvas.willRenderCanvases += PerformUpdate;
}

/// <summary>
/// 注册图形元素重建
/// </summary>
/// <param name="element"></param>
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
    instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}

/// <summary>
/// 注册布局元素重建
/// </summary>
/// <param name="element"></param>
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
    instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}

public static void UnRegisterCanvasElementForRebuild(ICanvasElement element)
{
    instance.InternalUnRegisterCanvasElementForLayoutRebuild(element);
    instance.InternalUnRegisterCanvasElementForGraphicRebuild(element);
}

工具方法

/// <summary>
/// 是否正在进行布局更新
/// </summary>
/// <returns></returns>
public static bool IsRebuildingLayout()
{
    return instance.m_PerformingLayoutUpdate;
}

/// <summary>
/// 是否正在进行图形更新
/// </summary>
/// <returns></returns>
public static bool IsRebuildingGraphics()
{
    return instance.m_PerformingGraphicUpdate;
}

/// <summary>
/// 清理无效的元素
/// </summary>
private void CleanInvalidItems()
{
    // So MB's override the == operator for null equality, which checks
    // if they are destroyed. This is fine if you are looking at a concrete
    // mb, but in this case we are looking at a list of ICanvasElement
    // this won't forward the == operator to the MB, but just check if the
    // interface is null. IsDestroyed will return if the backend is destroyed.

    for (int i = m_LayoutRebuildQueue.Count - 1; i >= 0; --i)
    {
        var item = m_LayoutRebuildQueue[i];
        if (item == null)
        {
            m_LayoutRebuildQueue.RemoveAt(i);
            continue;
        }

        if (item.IsDestroyed())
        {
            m_LayoutRebuildQueue.RemoveAt(i);
            item.LayoutComplete();
        }
    }

    for (int i = m_GraphicRebuildQueue.Count - 1; i >= 0; --i)
    {
        var item = m_GraphicRebuildQueue[i];
        if (item == null)
        {
            m_GraphicRebuildQueue.RemoveAt(i);
            continue;
        }

        if (item.IsDestroyed())
        {
            m_GraphicRebuildQueue.RemoveAt(i);
            item.GraphicUpdateComplete();
        }
    }
}

/// <summary>
/// 判断元素是否有效
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private bool ObjectValidForUpdate(ICanvasElement element)
{
    var valid = element != null;

    var isUnityObject = element is Object;
    if (isUnityObject)
        valid = (element as Object) != null; //Here we make use of the overloaded UnityEngine.Object == null, that checks if the native object is alive.

    return valid;
}

/// <summary>
/// 布局元素排序委托, 做成静态成员可以减少函数到委托的转换
/// </summary>
private static readonly Comparison<ICanvasElement> s_SortLayoutFunction = SortLayoutList;

/// <summary>
/// 排序节点
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
private static int SortLayoutList(ICanvasElement x, ICanvasElement y)
{
    Transform t1 = x.transform;
    Transform t2 = y.transform;

    return ParentCount(t1) - ParentCount(t2);
}

执行更新主流程

/// <summary>
/// 执行更新主流程
/// </summary>
private void PerformUpdate()
{
    // UI系统分析启动
    UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
    
    // 清理无效元素
    CleanInvalidItems();

    // 标记布局更新中
    m_PerformingLayoutUpdate = true;

    // 排序布局元素(根据父节点数量从小到大的排序)
    m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
    
    // 在各个时机分别触发各个布局元素的重建(Prelayout, Layout, PostLayout)
    for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
    {
        for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
        {
            var rebuild = instance.m_LayoutRebuildQueue[j];
            try
            {
                // 只对有效的元素进行触发(可能在某个时机中会改变有效性)
                if (ObjectValidForUpdate(rebuild))
                    rebuild.Rebuild((CanvasUpdate)i);
            }
            catch (Exception e)
            {
                Debug.LogException(e, rebuild.transform);
            }
        }
    }

    // 触发所有布局元素布局完成的回调
    for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
        m_LayoutRebuildQueue[i].LayoutComplete();

    // 清理布局元素队列
    instance.m_LayoutRebuildQueue.Clear();
    
    // 清理布局元素更新中的标记
    m_PerformingLayoutUpdate = false;

    // 布局元素完成更新后, 进行剔除操作(后面的文章单独介绍)
    ClipperRegistry.instance.Cull();

    // 标记图形元素更新中
    m_PerformingGraphicUpdate = true;
    
    // 在各个时机分别触发各个图形元素的重建(PreRender, LatePreRender)
    for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
    {
        for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
        {
            try
            {
                var element = instance.m_GraphicRebuildQueue[k];
                
                // 只对有效的元素进行触发(可能在某个时机中会改变有效性)
                if (ObjectValidForUpdate(element))
                    element.Rebuild((CanvasUpdate)i);
            }
            catch (Exception e)
            {
                Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
            }
        }
    }

    // 触发所有图形元素更新完成的回调
    for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
        m_GraphicRebuildQueue[i].GraphicUpdateComplete();

    // 清空图形队列
    instance.m_GraphicRebuildQueue.Clear();
    
    // 清理图形更新中标记
    m_PerformingGraphicUpdate = false;
    
    // UI系统分析结束
    UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}

举例子: Graphic的实现

/// <summary>
/// 重建接口的实现
/// </summary>
/// <param name="update"></param>
public virtual void Rebuild(CanvasUpdate update)
{
    if (canvasRenderer.cull)	
        return;

    // 在渲染之前更新所需内容(顶点或者材质)
    switch (update)
    {
        case CanvasUpdate.PreRender:
            if (m_VertsDirty)
            {
                UpdateGeometry();
                m_VertsDirty = false;
            }
            if (m_MaterialDirty)
            {
                UpdateMaterial();
                m_MaterialDirty = false;
            }
            break;
    }
}

总结

今天介绍了ICanvasElement和其相关的工具类, ICanvasElement抽象了可以在画布上显示的元素的行为, 主要是重建和一些回调, 而CanvasUpdateRegistry则是使用ICanvasElement的主体, 根据注册画布渲染前事件来触发那些需要进行重建的元素(比如元素自己知道某些情况下需要进行重建, 就要把自己注册到CanvasUpdateRegistry)的接口.

本质上CanvasUpdateRegistry的存在是为了统一UGUI元素的刷新, 一方面避免刷新时机的不统一, 一方面也能降低刷新频率. Unity在很多地方都使用了类似的机制, 值得我们借鉴.

好了, 今天的内容就是这些, 希望对大家有所帮助.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拂面清风三点水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值