🧱 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_EDITORprotected 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();elseDoMeshGeneration();
}
1. 新版網格生成(DoMeshGeneration)
使用 VertexHelper
構建頂點數據,并支持 IMeshModifier
修改網格。
private void DoMeshGeneration()
{if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)OnPopulateMesh(s_VertexHelper);elses_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 618OnPopulateMesh(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_EDITORGraphicRebuildTracker.TrackGraphic(this);
#endifif (s_WhiteTexture == null)s_WhiteTexture = Texture2D.whiteTexture;SetAllDirty();
}protected override void OnDisable()
{
#if UNITY_EDITORGraphicRebuildTracker.UnTrackGraphic(this);
#endifGraphicRegistry.DisableGraphicForCanvas(canvas, this);CanvasUpdateRegistry.DisableCanvasElementForRebuild(this);if (canvasRenderer != null)canvasRenderer.Clear();LayoutRebuilder.MarkLayoutForRebuild(rectTransform);base.OnDisable();
}
- 啟用時注冊到 Canvas 和重建系統。
- 禁用時取消注冊并清除緩存。
2. 銷毀
protected override void OnDestroy()
{
#if UNITY_EDITORGraphicRebuildTracker.UnTrackGraphic(this);
#endifGraphicRegistry.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 CacheCanvasCanvas 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 發生變化時,重新注冊自己。
八、性能優化技巧
-
避免頻繁調用
SetAllDirty()
- 只在真正需要更新時才調用。
- 多個屬性變更后統一調用一次即可。
-
利用
IMeshModifier
和IMaterialModifier
- 可以擴展圖形外觀,而無需繼承
Graphic
。
- 可以擴展圖形外觀,而無需繼承
-
合理使用
raycastTarget
- 不需要交互的 UI 應關閉射線檢測,減少性能開銷。
-
使用對象池(ListPool)
- 比如
ListPool<Component>.Get()
和ListPool<Component>.Release()
,避免頻繁 GC。
- 比如
九、總結
Graphic
類是 Unity UI 系統中最底層、最重要的類之一。它不僅提供了基礎的視覺表現能力,還通過高效的臟標記機制和靈活的擴展接口,使得能夠輕松構建各種復雜的 UI 控件。