Unity UI 核心类解析之Graphic

发布于:2025-06-20 ⋅ 阅读:(20) ⋅ 点赞:(0)

🧱 Unity UI 核心类解析:Graphic 类详解

一、什么是 Graphic?

在 Unity 的 UI 系统中,Graphic 是一个抽象基类,继承自 UIBehaviour 并实现了 ICanvasElement 接口。它是所有可以被绘制到屏幕上的 UI 元素的基础类。

✅ UI 控件(如按钮、文字、图片等),都直接或间接继承自 Graphic


二、核心作用与职责

Graphic 类主要负责以下几件事:

职责 功能
🎨 视觉渲染 控制颜色、材质、纹理、网格生成等
📐 布局更新 响应 RectTransform 变化,参与布局系统
🔦 射线检测 控制是否响应点击/触摸事件
🔄 脏标记机制 优化性能,只在必要时才重新绘制
⚙️ 生命周期管理 OnEnable / OnDisable / OnDestroy 等回调处理

三、关键属性详解

1. 默认材质与纹理

protected static Material s_DefaultUI = null;
protected static Texture2D s_WhiteTexture = null;

public static Material defaultGraphicMaterial
{
    get
    {
        if (s_DefaultUI == null)
            s_DefaultUI = Canvas.GetDefaultCanvasMaterial();
        return s_DefaultUI;
    }
}
public virtual Texture mainTexture => s_WhiteTexture;
  • defaultGraphicMaterial:获取默认的 UI 材质。
  • mainTexture:默认使用白色纹理,用于绘制基础图形。

2. 颜色控制

[SerializeField] private Color m_Color = Color.white;
public virtual Color color { 
	get => m_Color; 
	set { 
		SetPropertyUtility.SetColor(ref m_Color, value); 
		SetVerticesDirty();
	 } 
}
  • 支持序列化,方便在 Inspector 中修改。
  • 修改颜色会触发顶点数据“脏”状态,表示需要重新绘制。

3. 材质控制

protected Material m_Material;
public virtual Material material
{
    get
    {
        return (m_Material != null) ? m_Material : defaultMaterial;
    }
    set
    {
        if (m_Material == value)
            return;

        m_Material = value;
        SetMaterialDirty();
    }
}
  • 支持设置自定义材质,否则使用默认材质。
  • 修改材质也会触发“脏”状态。

4. 射线检测开关

private bool m_RaycastTarget = true;
public virtual bool raycastTarget
 {
     get
     {
         return m_RaycastTarget;
     }
     set
     {
         if (value != m_RaycastTarget)
         {
             if (m_RaycastTarget)
                 GraphicRegistry.UnregisterRaycastGraphicForCanvas(canvas, this);

             m_RaycastTarget = value;

             if (m_RaycastTarget && isActiveAndEnabled)
                 GraphicRegistry.RegisterRaycastGraphicForCanvas(canvas, this);
         }
         m_RaycastTargetCache = value;
     }
 }
  • 控制该 UI 是否能接收点击事件。
  • 修改此值时会注册或注销射线检测图层。

四、重建方法(Rebuild)

Rebuild方法来自接口ICanvasElement

  public interface ICanvasElement
   {
       /// <summary>
       /// Rebuild the element for the given stage.
       /// </summary>
       /// <param name="executing">The current CanvasUpdate stage being rebuild.</param>
       void Rebuild(CanvasUpdate executing);
   }

Graphic 实现了ICanvasElement接口

public abstract class Graphic : UIBehaviour, ICanvasElement

Rebuild方法如下

/// <summary>
/// 在 PreRender(预渲染)阶段重建图形的几何体及其材质。
/// </summary>
/// <param name="update">当前 CanvasUpdate 渲染循环的阶段。</param>
/// <remarks>
/// 有关画布更新循环的更多详细信息,请参阅 CanvasUpdateRegistry。
/// </remarks>
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;
    }
}

CanvasUpdateRegistry的部分源码

public class CanvasUpdateRegistry
{
	private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();
	
    protected CanvasUpdateRegistry()
    {
    	//willRenderCanvases是一个Unity内置的静态事件,它在每一帧中:
		//在所有 Canvas 被渲染之前立即触发。
         Canvas.willRenderCanvases += PerformUpdate;
    }
    
	private void PerformUpdate()
    {
          
         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 = m_LayoutRebuildQueue[j];
                 try
                 {
                     if (ObjectValidForUpdate(rebuild))
                     	//关键函数
                         rebuild.Rebuild((CanvasUpdate)i);
                 }
                 catch (Exception e)
                 {
                     Debug.LogException(e, rebuild.transform);
                 }
             }
         }
     }
}

五、脏标记机制(SetAllDirty)

为了提高性能,Unity 使用了“脏标记”机制,只有当某些数据发生改变时才会触发重绘或重新计算。

1.SetAllDirty


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

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

     SetVerticesDirty();
     SetRaycastDirty();
 }

该方法在下面几处会调用

protected override void OnTransformParentChanged()
protected override void OnEnable()
protected override void Reset()
protected override void OnDidApplyAnimationProperties()
#if UNITY_EDITOR
        protected override void OnValidate()
#endif

📋 总结表格

方法名 调用时机 是否运行时调用 是否编辑器专用 主要用途
OnTransformParentChanged GameObject 的父级发生变化时 ✅ 是 ❌ 否 处理层级变化,重新注册 Canvas
OnEnable GameObject 被激活时 ✅ 是 ❌ 否 初始化资源、注册 UI、触发重建
Reset 首次挂载脚本或点击 Reset 时 ❌ 否 ✅ 是 设置默认值,重置组件状态
OnDidApplyAnimationProperties 动画属性应用后 ✅ 是 ❌ 否 处理动画驱动的 UI 更新
OnValidate Inspector 中字段修改后失去焦点时 ❌ 否 ✅ 是 数据验证、实时预览 UI 效果

2.SetLayoutDirty

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

        LayoutRebuilder.MarkLayoutForRebuild(rectTransform);

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

以下方法会调用

public virtual void SetAllDirty()
protected override void OnRectTransformDimensionsChange()
  • 作用

    • 告诉布局系统该元素的布局信息已改变,需要重新计算布局。
  • 触发时机

    • SetAllDirty()
    • OnRectTransformDimensionsChange():尺寸发生变化时
  • 行为

    • 调用 LayoutRebuilder.MarkLayoutForRebuild(rectTransform),加入布局重建队列
    • 触发回调 m_OnDirtyLayoutCallback

3.SetMaterialDirty

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

     m_MaterialDirty = true;
     CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

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

以下方法会调用

public virtual void SetAllDirty()
public virtual Material material
 {
     get
     {
         return (m_Material != null) ? m_Material : defaultMaterial;
     }
     set
     {
         if (m_Material == value)
             return;
         m_Material = value;
         SetMaterialDirty();
     }
 }
  • 作用

    • 表示材质发生了变化,需要重新提交材质到 Canvas 渲染系统。
  • 触发时机

    • SetAllDirty()
    • 材质属性被修改(例如 material = new Material(...)
  • 行为

    • 设置标志位 m_MaterialDirty = true
    • 注册到 CanvasUpdateRegistry,参与图形重建流程
    • 触发回调 m_OnDirtyMaterialCallback

4.SetVerticesDirty

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

     m_VertsDirty = true;
     CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

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

以下方法会调用

public virtual Color color { 
	get { return m_Color; } set { 
	if (SetPropertyUtility.SetColor(ref m_Color, value)) 
		SetVerticesDirty(); 
	} 
}
public virtual void SetAllDirty()
protected override void OnRectTransformDimensionsChange()
  • 作用

    • 表示顶点数据(网格)发生了变化,需要重新生成 UI 网格。
  • 触发时机

    • SetAllDirty()
    • 颜色属性变更(如 color = Color.red
    • 尺寸变化(通过 OnRectTransformDimensionsChange()
    • 自定义顶点修改(如自定义 Graphic)
  • 行为

    • 设置标志位 m_VertsDirty = true
    • 注册到 CanvasUpdateRegistry,等待重建
    • 触发回调 m_OnDirtyVertsCallback

5.SetRaycastDirty

 public void SetRaycastDirty()
  {
      if (m_RaycastTargetCache != m_RaycastTarget)
      {
          if (m_RaycastTarget && isActiveAndEnabled)
              GraphicRegistry.RegisterRaycastGraphicForCanvas(canvas, this);

          else if (!m_RaycastTarget)
              GraphicRegistry.UnregisterRaycastGraphicForCanvas(canvas, this);
      }
      m_RaycastTargetCache = m_RaycastTarget;
  }

以下方法会调用

public virtual void SetAllDirty()
  • 作用

    • 表示是否允许响应射线检测的状态发生改变。
  • 触发时机

    • SetAllDirty()
    • 修改 raycastTarget 属性时
  • 行为

    • 如果启用射线检测,则注册到 GraphicRegistry
    • 如果禁用,则从 GraphicRegistry 移除
    • 更新缓存值 m_RaycastTargetCache

6.总结

生命周期方法 是否调用 SetAllDirty() 是否调用其他 Dirty 方法
OnEnable() ✅ 是 ❌ 否(但会触发 SetAllDirty)
Reset() ✅ 是 ❌ 否
OnDidApplyAnimationProperties() ✅ 是 ❌ 否
OnTransformParentChanged() ✅ 是 ❌ 否
OnValidate()(编辑器) ✅ 是 ❌ 否
OnRectTransformDimensionsChange() ❌ 否 ✅ 是(调用 SetVerticesDirty()SetLayoutDirty()

六、几何体生成(UpdateGeometry)

这是 Graphic 类最核心的部分之一,它决定了 UI 如何绘制到屏幕上。

关键方法:

protected virtual void UpdateGeometry()
{
    if (useLegacyMeshGeneration)
        DoLegacyMeshGeneration();
    else
        DoMeshGeneration();
}
1. 新版网格生成(DoMeshGeneration)

使用 VertexHelper 构建顶点数据,并支持 IMeshModifier 修改网格。

private void DoMeshGeneration()
{
    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.

    var components = ListPool<Component>.Get();
    GetComponents(typeof(IMeshModifier), components);

    //获取当前对象是否有IMeshModifier接口,
    //Text的描边和阴影都是通过它的ModifyMesh方法实现的
    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);
}
2. 旧版网格生成(DoLegacyMeshGeneration)

直接操作 Mesh 对象,兼容旧版本逻辑。

private void DoLegacyMeshGeneration()
{
    if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
    {
#pragma warning disable 618
        OnPopulateMesh(workerMesh);
#pragma warning restore 618
    }
    else
    {
        workerMesh.Clear();
    }

    var components = ListPool<Component>.Get();
    GetComponents(typeof(IMeshModifier), components);

    for (var i = 0; i < components.Count; i++)
    {
#pragma warning disable 618
        ((IMeshModifier)components[i]).ModifyMesh(workerMesh);
#pragma warning restore 618
    }

    ListPool<Component>.Release(components);
    canvasRenderer.SetMesh(workerMesh);
}

实现细节:OnPopulateMesh(VertexHelper vh)

protected virtual void OnPopulateMesh(VertexHelper vh)
{
    var r = GetPixelAdjustedRect();
    var v = new Vector4(r.x, r.y, r.x + r.width, r.y + r.height);

    Color32 color32 = color;
    vh.Clear();
    vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(0f, 0f));
    vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(0f, 1f));
    vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(1f, 1f));
    vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(1f, 0f));

    vh.AddTriangle(0, 1, 2);
    vh.AddTriangle(2, 3, 0);
}

这是一个虚方法,提供了默认实现,子类可以重写实现自己的图形形状。


七、生命周期与事件响应

1. 启用与禁用

protected override void OnEnable()
{
    base.OnEnable();
    CacheCanvas();
    GraphicRegistry.RegisterGraphicForCanvas(canvas, this);

#if UNITY_EDITOR
    GraphicRebuildTracker.TrackGraphic(this);
#endif
    if (s_WhiteTexture == null)
        s_WhiteTexture = Texture2D.whiteTexture;

    SetAllDirty();
}
        
protected override void OnDisable()
{
#if UNITY_EDITOR
    GraphicRebuildTracker.UnTrackGraphic(this);
#endif
    GraphicRegistry.DisableGraphicForCanvas(canvas, this);
    CanvasUpdateRegistry.DisableCanvasElementForRebuild(this);

    if (canvasRenderer != null)
        canvasRenderer.Clear();

    LayoutRebuilder.MarkLayoutForRebuild(rectTransform);

    base.OnDisable();
}
  • 启用时注册到 Canvas 和重建系统。
  • 禁用时取消注册并清除缓存。

2. 销毁

protected override void OnDestroy()
{
#if UNITY_EDITOR
    GraphicRebuildTracker.UnTrackGraphic(this);
#endif
    GraphicRegistry.UnregisterGraphicForCanvas(canvas, this);
    CanvasUpdateRegistry.UnRegisterCanvasElementForRebuild(this);
    if (m_CachedMesh)
        Destroy(m_CachedMesh);
    m_CachedMesh = null;

    base.OnDestroy();
}
  • 清除资源,防止内存泄漏。

3. RectTransform 变化

protected override void OnRectTransformDimensionsChange()
{
    if (gameObject.activeInHierarchy)
    {
        // prevent double dirtying...
        if (CanvasUpdateRegistry.IsRebuildingLayout())
            SetVerticesDirty();
        else
        {
            SetVerticesDirty();
            SetLayoutDirty();
        }
    }
}
  • 当尺寸变化时触发顶点或布局更新。

4. Canvas 层级变化

protected override void OnCanvasHierarchyChanged()
{
    // Use m_Cavas so we dont auto call CacheCanvas
    Canvas currentCanvas = m_Canvas;

    // Clear the cached canvas. Will be fetched below if active.
    m_Canvas = null;

    if (!IsActive())
    {
        GraphicRegistry.UnregisterGraphicForCanvas(currentCanvas, this);
        return;
    }

    CacheCanvas();

    if (currentCanvas != m_Canvas)
    {
        GraphicRegistry.UnregisterGraphicForCanvas(currentCanvas, this);

        // Only register if we are active and enabled as OnCanvasHierarchyChanged can get called
        // during object destruction and we dont want to register ourself and then become null.
        if (IsActive())
            GraphicRegistry.RegisterGraphicForCanvas(canvas, this);
    }
}
  • 当父级 Canvas 发生变化时,重新注册自己。

八、性能优化技巧

  1. 避免频繁调用 SetAllDirty()

    • 只在真正需要更新时才调用。
    • 多个属性变更后统一调用一次即可。
  2. 利用 IMeshModifierIMaterialModifier

    • 可以扩展图形外观,而无需继承 Graphic
  3. 合理使用 raycastTarget

    • 不需要交互的 UI 应关闭射线检测,减少性能开销。
  4. 使用对象池(ListPool)

    • 比如 ListPool<Component>.Get()ListPool<Component>.Release(),避免频繁 GC。

九、总结

Graphic 类是 Unity UI 系统中最底层、最重要的类之一。它不仅提供了基础的视觉表现能力,还通过高效的脏标记机制和灵活的扩展接口,使得能够轻松构建各种复杂的 UI 控件。


网站公告

今日签到

点亮在社区的每一天
去签到