> 技术文档 > Unity(2)_unity 3d相机下显示的物体上挂了2d uicanvas,真机打出的包节点被删除了

Unity(2)_unity 3d相机下显示的物体上挂了2d uicanvas,真机打出的包节点被删除了


UI物体不显示的原因

Canvas、Canvas Group和Image(这三者都挂到了这个脚本上了)

✅ 检查点列表:

  1. Canvas 设置是否正确

    • Render Mode 设置为 Screen Space - OverlayCamera,确保能显示 UI。

    • Sorting Layer 是否正确?

    • UI Camera(如果使用)是否启用?

  2. Image 组件贴图是否为空?

    • Image 上是否设置了 Sprite?如果为空,当然不可见。

    • Color.a 透明度是否为 0?

  3. CanvasGroup 的 Alpha 是否为 0

    • cg.alpha = 0 会导致所有子物体(包括文字、图片)都透明不可见。

    • ShowTip() 会设置 alpha = 1,然后 3 秒后 DOFade(0)

    • 你查看时如果是在 Scene 视图中编辑,它可能刚好已经透明了。

  4. Image 尺寸与 Anchors 是否正确

    • Image 的 RectTransform 尺寸是否太小,或者 anchor 设置让它被压缩?

  5. 被隐藏了

    • gameObject.SetActive(false) 会直接隐藏整个提示框。

    • 如果 GameTip.Send() 没有被调用过,它会一直是隐藏状态。

疑惑:关于GameTip

为什么他要只做一个这样的预制件,而且我看他这个预制件是放在别的Canvas底下的,所以就没有Render Mode了,为什么?又为什么没有Render Mode了?为什么要加入Canvas和Canvas Group这两个组件

❓ 为什么这个预制件要只做一个?

这个 GameTip 脚本的作用是显示一个提示信息,比如:

“金币不足”、“请先选择一张卡牌”、“网络连接失败”

这种提示内容具有几个特点:

  • 全局唯一(不需要多个实例)

  • 临时出现(自动淡出)

  • 文字不同、样式一致

  • 优先显示在界面上方

所以设计为一个 单一的、全局唯一的预制件 是合适的,方便通过事件广播调用,随时弹出提示。


❓ 为什么放在别的 Canvas 下?

你看到这个预制件是挂在另一个 Canvas 下的(比如 GamePanel 或 UIRoot),这是 Unity UI 的常规做法,因为:

  • Unity 中的 UI 只能显示在 Canvas 之下

  • 如果预制件被动态实例化,通常会挂在某个现有的 Canvas 节点下(即已有的 UI 系统根节点)。

所以:

既然 GameTip 是 UI,它自然要属于某个 Canvas,但不一定要自己创建一个新的 Canvas。


❓ 为什么你看不到 Render Mode?

这是因为:

  • Render Mode 是 Canvas 组件的属性

  • GameTip 预制件本身 没有 Canvas,而是依附于父物体的 Canvas

也就是说:

你看到的 GameTip 预制件,只是一个普通 UI 节点(含 Image/Text/CanvasGroup),它不再是一个独立的“根 UI”,所以没有 Render Mode。

只有最上层的 Canvas 节点才设置 Render Mode: Screen Space - Overlay / Camera / World


❓ 为什么要挂 Canvas 和 CanvasGroup?

这里要区分:

🔹 挂 CanvasGroup 是必须的:

这个组件提供了一些方便控制 UI 淡入淡出的能力(与 DOTween 配合):

  • 控制透明度(alpha

  • 是否可交互(interactable

  • 是否阻止射线(blocksRaycasts

GameTip.ShowTip() 中你就看到它通过:


csharp

复制编辑

cg.DOFade(0, 3f)

来做动画淡出,所以需要 CanvasGroup。


🔸 挂 Canvas 可能是出于“覆盖排序”的需要:

虽然 GameTip 是放在别的 Canvas 下的,有时候还是会在其 GameObject 上 额外加一个 Canvas 组件,并设置 overrideSorting = true,设置 sortingOrder 比别的 UI 高,这样它就可以始终出现在最顶层。

但如果这个 GameTip 只是普通 UI 的一部分,就不一定非得有 Canvas。

总结建议:你可以通过检查这个预制件上的 Canvas 组件是否设置了 overrideSorting = true 来判断它是否是为了“提升显示层级”。

疑惑(跟随上一问):减少重绘?

1.它挂Canvas组件难道没有为了减少重绘的想法吗?难道我想错了?

2.重绘是一种怎样的操作,我可以这样理解吗,这样分离Canvas,虽然次数没变化,但是每次发生变动的时候,从GameTip这里发生的变化就都只会向上传递到Canvas(也就是Game Tip这一层的Canvas,不会传到最上级的Canvas)进行重绘,打包好所有UI信息,然后统一传到GPU进行渲染,之后显示?是这样的流程吗?

❓ 疑问背景

你在分析一个挂有 CanvasCanvasGroupImage 的 UI 预制体(如 GameTip)时提出以下两个关键问题:

❶「它挂 Canvas 是不是为了减少重绘?我这样理解对吗?」

❷「重绘的过程是不是从这个局部 Canvas 开始,一直向上传递数据,最后打包后给 GPU 渲染显示?」


✅ 答案一:Canvas 的确用于隔离重绘,但不是为了减少 次数,而是减少 范围

✔ 正确理解:

Canvas 是 UI 系统的“重建隔离区”
一个 UI 元素变化时,Unity 会标记为 dirty 并触发 Canvas.Rebuild(),这个重建操作 只在所属 Canvas 内部传播,不会影响其它 Canvas。

所以你说的「只传到自己 Canvas,不会传到上层 Canvas」这个逻辑是正确的,这就是我们说的 “局部重建优化”。


✅ 答案二:你描述的渲染流程基本正确,只需要稍加精炼

你的描述大致如下:

「UI 变化后,Canvas 层级内的数据会被打包、整理,最后统一提交到 GPU,之后渲染出来」

没错!这基本就是 Unity 的 UI 渲染管线。我们可以用更技术化的方式表示为:


🧠 Unity UI 渲染流程简化版(Rebuild 到 Render)


plaintext

复制编辑

UI 修改操作(如文本变化) ↓ 标记为 Dirty(Graphic/Layout/Material) ↓ Canvas 收到变动信号 → 执行 Canvas.Rebuild() ↓ CPU 整理数据(位置、尺寸、顶点、纹理索引) ↓ Canvas 打包这些数据 → 生成 Draw Call ↓ 提交给 GPU → GPU 执行图像绘制

✅ 所以你说的「变化打包成一次统一提交给 GPU」这句话是准确的。


🧩 为什么要用 Canvas?

功能 是否必须 解释 分隔 UI 重绘范围 ✅ 推荐 Canvas 是隔离区域,避免局部变动牵连全局 控制渲染顺序 ✅ 推荐 多个 Canvas 可以设置排序层级 管理 UI 批次(Draw Call) ✅ 推荐 同 Canvas 中图像可合批,不同 Canvas 则不能 避免频繁重建整棵树 ✅ 强烈推荐 动态 UI 建议用独立 Canvas 包裹,减轻性能压力

🧩 为什么加入 CanvasGroup?

CanvasGroup 的作用并非减少重绘,但它非常常见,常与弹窗/提示类 UI 绑定使用:

功能 描述 控制透明度(alpha) 可统一设置整个组的透明度 控制是否可交互 interactable = false 可禁用所有子组件交互 控制射线阻挡 blocksRaycasts 决定是否阻挡点击事件

适合 GameTip 这种「渐隐 UI + 无需响应交互」的组件。


📌 总结建议

目的 Canvas 使用建议 避免整个 UI 因一处变动全部重绘 ✔ 为频繁变化部分独立添加 Canvas 管理多个弹窗或过场动画 ✔ 每个弹窗独立 Canvas(可控制遮挡层级) 提示类 UI(如 GameTip) ✔ 用 CanvasGroup 控制渐隐、透明,Canvas 控制更新范围 降低 Draw Call 数量 ✘ 注意过多 Canvas 会阻止合批,可能适得其反,需要平衡

🧪 如何检查是否真的减少了重绘 & 性能分析建议?

Unity 提供了以下工具帮助你检查是否 Canvas 分离有效、重绘是否减少:

🔧 1. UI Profiler(内置)

  • 打开 Profiler(Window → Analysis → Profiler)

  • 选择模块:UI → UI Rebuild

  • 查看 Canvas 的 Layout/Graphic Rebuild 是否频繁

🔍 2. UI Debugger(Unity 2021+ 推荐)

  • Window → UI Toolkit → UI Debugger

  • 可查看每个 UI 节点的重绘状态、Rebuild 频率

🧪 3. 手动测试 DrawCall 数

  • 用 Stats 面板(Game 窗口右上角小按钮)

  • 查看不同 UI 动作下,是否有 Batches (Draw Calls) 明显上升

疑惑:关于DC

分太多 Canvas 会反而增加 Draw Call?我怎么感觉最多也就是与之前不变,怎么还会有增加的情况?

你提得非常好!这个现象确实容易误解。下面我来详细解释 为什么分太多 Canvas 会反而 增加 Draw Call。


✅ 首先:什么情况下 Canvas 会让 Draw Call 变多?

关键点:

不同 Canvas 之间的 UI 元素 不能合批(batching),即使它们使用相同的材质、图集、Shader!


🎯 合批(Batching)回顾一小下:

Unity 合批的核心目的是:

把多个小的渲染请求“打包”成一个大请求,一次性传给 GPU,减少 Draw Call 数。

合批的前提包括:

条件 说明 同一个材质 如使用同一个图集的 Image 或 Text 同一个渲染层 不能跨 Canvas 排序一致 相邻、没有被其它物体插入 非动态变化过频繁 否则无法静态合批

❌ 分太多 Canvas,会怎样?

每个 Canvas 是独立的渲染单元,它自己会:

  • 单独打包一套顶点信息

  • 单独生成一次或多次 Draw Call

  • 无法与其他 Canvas 合并 Draw Call

例子:

情况 Draw Call 数 所有 UI 在一个 Canvas 中,使用同一个图集 1 个 Draw Call(合批成功) 分成 3 个 Canvas,内容完全一样 最少 3 个 Draw Call(不能合批) 动态 UI + 多个 Canvas Draw Call 更可能上升,因为变化区域各自重绘

🔍 那为什么有人说 Canvas 分离还能优化?

因为要看 权衡对象是 CPU 还是 GPU:

目标 分离 Canvas 带来的好处 减少 CPU 消耗 避免整棵 UI 树 Layout Rebuild / Graphic Rebuild 降低 Rebuild 频率 局部变动不会影响其它区域 控制更新粒度 比如:弹窗频繁动,背景静止,就可以隔离渲染成本

所以:

Canvas 分离是为了优化 UI 重建成本(CPU),不是 Draw Call 数(GPU)!


🧠 一句话总结这段话:

分离 Canvas 可以优化 UI 重建(CPU 性能),但可能导致更多 Draw Call(GPU 负担)——需要按需使用,不能滥用!


📌 实际开发建议

使用 Canvas 分离的理由 应该这样做? 弹窗频繁开关 / 渐隐渐现动画 ✔ 是 提示文字闪烁、独立显示 ✔ 是 一堆静态背景图+文本想分开 ✘ 不建议,合在一个更好 所有按钮都包个独立 Canvas ✘ 绝对不要,会暴涨 Draw Call

🧪 如何判断有没有滥用 Canvas?

  1. 在 Game 窗口右上角点击 Stats,看:

    • Batches 是否远大于 UI 元素数

    • 是否 GPU 使用率异常升高(特别在低端设备上)

  2. 使用 Profiler > Rendering,查看:

    • 每帧是否多次 Canvas.Render Overhead

    • 每次 UI 操作是否引发大量 Canvas Rebuild

-----6.17

UnityEditor.EditorUtility.SetDirty(this):为什么需要它?

在 Unity 编辑器中,ScriptableObject 资源(例如配置文件、数据文件等)通常会存储在 *.asset 文件中。当你在 代码中 修改这些资源的内容时,Unity 默认不会自动标记该资源为已修改(dirty)。这意味着 即使你通过代码改变了数据,这些变化也不会被保存,除非你明确告诉 Unity 去保存它们。

为什么 SetDirty(this) 是必要的?

在 Unity 编辑器中,只有被标记为 dirty 的资源才会被保存。但是,修改 ScriptableObject 或其他资源的字段时,Unity 并不会自动认为该资源已被修改,尤其是在 OnValidate() 或代码中修改字段时。因此,如果没有调用 SetDirty(this),即使你修改了资源内容,它也不会自动保存到磁盘上。

什么时候需要使用 SetDirty()

  • ScriptableObject 中修改字段时: 如果你在编辑器中通过代码修改了一个 ScriptableObject,你需要使用 SetDirty() 来通知 Unity 保存这些更改。

  • 自定义编辑器工具中: 如果你在自定义编辑器扩展或脚本中修改了资源内容,并希望这些更改能持久化,需要调用 SetDirty()

  • OnValidate() 中: 当你修改 ScriptableObject 中的数据时,尤其是在 OnValidate() 这样的编辑器生命周期方法中,你应当使用 SetDirty() 来确保数据更改被保存。

示例:修改并保存 ScriptableObject 数据

假设你有一个 ScriptableObject 资源,并希望在编辑器中通过 OnValidate() 来修改它:

using UnityEditor;using UnityEngine;[CreateAssetMenu(fileName = \"NewConfig\", menuName = \"Configs/Config\")]public class MyConfig : ScriptableObject{ public Vector3 position; public bool updatePosition = false; private void OnValidate() { if (updatePosition) { position += Vector3.one; updatePosition = false; // 标记资源为已修改 EditorUtility.SetDirty(this); } }}

在上述代码中,当 updatePositiontrue 时,OnValidate() 方法会修改 position 的值,并调用 SetDirty(this),这会通知 Unity 保存 MyConfig 的修改。

不加 SetDirty() 会发生什么?

如果你 不加 SetDirty(this),即使你修改了 ScriptableObject 的字段,Unity 也不会自动标记该资源为 dirty。因此:

  1. 不会触发自动保存:按下 Ctrl+S(或保存项目)时,*.asset 文件不会保存修改的内容。

  2. 改动丢失:你重新打开项目后,所有通过代码做的修改会丢失,因为修改并未真正写入磁盘。

总结

在 Unity 编辑器中,修改 ScriptableObject 等资源时 需要手动调用 EditorUtility.SetDirty(this) 来确保这些修改被正确标记和保存,否则改动可能无法持久保存。当你通过代码修改资源字段时,调用 SetDirty() 能确保:

  • Unity 知道你修改了资源。

  • 修改的内容会在 Ctrl+S 时保存。

  • 编辑器能够正确跟踪资源变化。

使用 SetDirty() 是确保修改不丢失并能正确保存的好习惯,尤其是在 编辑器扩展自定义资源管理数据编辑器 中非常重要。

---6.27