🎨 UI 制作从入门到精通:从手工匠人到工业化生产
📚 1. 阶段一:手工匠人 (Manual Mastery)
目标: 能够拼出适应各种屏幕分辨率、布局整齐的 UI。
1.1 锚点与轴心 (Anchors & Pivots) - 核心基本功
- 锚点 (Anchors): 决定 UI 元素的相对位置和拉伸方式。
- 四叶草 (The Flower): RectTransform 面板上的那个图形。
- 全屏拉伸: Min(0,0) Max(1,1)。适用于背景图、全屏遮罩。
- 居中固定: Min(0.5,0.5) Max(0.5,0.5)。适用于弹窗。
- 靠边停靠: 比如右上角关闭按钮,Anchors 设为右上,Pivot 也设为右上 (1,1)。
- 轴心 (Pivot): 决定 UI 元素的旋转中心和缩放中心。
1.2 自动布局系统 (Layout System)
- Horizontal/Vertical Layout Group: 自动排列子物体。
- Child Controls Size: 勾选后,子物体的大小由父物体控制(如:列表项宽度自动撑满)。
- Child Force Expand: 勾选后,子物体会平分多余空间。
- Content Size Fitter: 根据内容自动调整自身大小。
- 常用组合: Text 组件挂 Content Size Fitter (Vertical Fit: Preferred Size),实现文本框随文字高度自动拉伸。
- Layout Element: 覆盖 Layout Group 的默认设置。
- Min Width: 最小宽度,防止被压扁。
- Preferred Width: 期望宽度。
- Flexible Width: 权重。设为 1 表示参与瓜分剩余空间。
- ⚠️ 性能陷阱 (Performance Pitfalls):
- 嵌套地狱:
Layout Group嵌套越深,重建开销呈指数级增长。 - Dirty Flags: 任何子物体的变化(如
SetActive)都会标记父级 Layout 为 Dirty,导致下一帧强制 Rebuild。 - 最佳实践:
- Build & Disable: 如果列表是静态的(如设置面板),在
Start()排列好后,立即禁用LayoutGroup组件。 - 避免 Animator: 不要用 Animator 驱动 Layout 元素的大小变化,这会每帧触发 Rebuild。
- Build & Disable: 如果列表是静态的(如设置面板),在
- 嵌套地狱:
1.3 九宫格 (9-Slice)
- 原理: 保持四个角不变形,只拉伸中间区域。
- 设置: 在 Sprite Editor 中拖动绿线。
- 应用: 按钮背景、通用弹窗底板。
1.4 屏幕适配 (Screen Adaptation) - 搞定刘海屏与折叠屏
- Canvas Scaler: 屏幕适配的总指挥。
- UI Scale Mode: 永远选
Scale With Screen Size。 - Reference Resolution: 设为美术出图的标准尺寸(如 1920x1080)。
- Match Mode:
- Match Width Or Height: 关键参数。
- 横屏游戏 (Landscape): 推荐 Match = 1 (Height) 或 0.5。
- Match Height (1): 保证 UI 元素在垂直方向上占比不变。宽屏手机(如 iPhone 15)会看到更宽的视野,UI 不会变小。
- Match Width (0): 可能会导致宽屏手机上 UI 变得巨大,遮挡视野。
- UI Scale Mode: 永远选
- 安全区 (Safe Area):
- 痛点: 刘海屏、灵动岛、圆角屏幕遮挡 UI。
- 解决: 编写
SafeArea脚本。- 在运行时获取
Screen.safeArea。 - 将 UI 根节点的
AnchorMin和AnchorMax设置为安全区比例。 - 注意: 背景图不要挂 SafeArea,让它铺满全屏;只有按钮和文字需要挂。
- 在运行时获取
- Aspect Ratio Fitter:
- 用途: 强制保持宽高比。
- 场景: 小地图(必须是正圆)、卡牌立绘(必须是 3:4)、头像框。
🚀 2. 阶段二:性能优化 (Optimization)
目标: 减少 Draw Call,降低 CPU/GPU 消耗,消除卡顿。
2.1 动静分离 (Canvas Splitting)
- 原理: 当 Canvas 下任何一个元素发生变化(位移、颜色、文字改变),整个 Canvas 都会触发 Re-batch(重新构建网格)。
- 实践:
- Static Canvas: 背景图、边框、标题。几乎不动的元素。
- Dynamic Canvas: 血条、伤害数字、倒计时。每帧都在变的元素。
- 不要嵌套: 尽量避免 Canvas 嵌套 Canvas,这会增加层级管理的复杂度。
2.2 射线检测 (Raycast Target)
- 隐形杀手: 每一个勾选了
Raycast Target的 UI 元素(Image, Text)都会参与点击检测运算。 - 优化:
- 默认关闭: 只有按钮、滑动条等需要交互的元素才勾选。
- 纯展示图片/文字: 务必取消勾选。
- Debug 工具: 使用 Unity UI Profiler 或 Scene 视图的 “Overdraw” 模式检查。
2.3 图集 (Sprite Atlas)
- 原理: 将多张小图合并成一张大图,一次性提交给 GPU,实现 1 个 Draw Call 渲染多个元素。
- 策略:
- 按功能分:
CommonAtlas(通用按钮、边框),IconAtlas(技能图标),MainUIAtlas(主界面独有)。 - 避免交叉引用: 如果一个界面同时引用了 5 个图集,就会产生至少 5 个 Draw Call。
- 按功能分:
2.4 列表优化 (LoopListView)
- 痛点: 背包里有 1000 个格子,如果实例化 1000 个 GameObject,打开界面会卡死。
- 解决: 无限循环列表 (Object Pooling)。
- 只实例化屏幕内可见的(例如 20 个)格子。
- 滑动时,将移出屏幕的格子回收,放到另一端,并更新数据。
- 插件推荐:
EnhancedScroller或LoopScrollRect。 - 核心机制:
- View Item vs Data Item: 只有 20 个 View Item (GameObject),但有 1000 个 Data Item (C# Class)。
- Recycle Bin: 移出屏幕的 Item 不会被 Destroy,而是进入回收池。
- Data Binding: 当 Item 从回收池取出放到另一端时,必须重新绑定数据。
🤖 3. 阶段三:自动化与工业化 (Automation)
目标: 让机器写代码,让美术直接出界面。
3.1 代码生成 (UI Code Gen)
- 拒绝 Find:
transform.Find("Bg/Button")是脆弱且低效的。 - 自动绑定:
-
约定命名规范(如
btn_Close,txt_Title)。 - 编写编辑器脚本,遍历 Prefab。
-
自动生成
XXView.cs,包含public Button btn_Close;。 - 自动挂载脚本并序列化引用。
-
约定命名规范(如
3.2 设计稿直通车 (Figma to Unity)
- 痛点: 美术出图 -> 拼 UI -> 程序调位置 -> 美术验收不通过 -> 反复调整。
-
流程:
- 美术在 Figma 中按规范作图(使用 Auto Layout)。
- 使用插件 (如 Doozy UI, Nova, 或开源的 Figma Converter for Unity) 导出。
- Unity 中一键导入,自动生成 RectTransform, Image, Text,甚至自动切图。
- 价值: 还原度 100%,程序只需关注逻辑。
🧛 4. 实战案例:Vampirefall
4.1 战斗 HUD (Heads-Up Display)
- 需求: 屏幕上同时存在 50+ 怪物,每个都有血条;每秒跳出 100+ 伤害数字。
- 优化方案:
- World Space UI vs Screen Space: 怪物血条使用 World Space Canvas 还是 3D Mesh?
- 方案 A: 少量怪物用 World Space Canvas。
- 方案 B (推荐): 大量怪物使用 GPU Instancing 渲染血条面片 (Mesh),不走 UGUI 系统。
- 伤害数字: 必须使用对象池。使用
TextMeshPro的批处理模式,或者自定义 Mesh 粒子系统。
- World Space UI vs Screen Space: 怪物血条使用 World Space Canvas 还是 3D Mesh?
4.2 技能构建界面 (Build UI)
- 需求: 复杂的技能树,连线,节点状态(已解锁/未解锁)。
- 实现:
- ScrollRect: 允许拖拽查看庞大的技能树。
- LineRenderer vs UI Line: 使用
UILineRenderer(UI Extensions) 绘制连线,比 3D LineRenderer 更适合 UI 层级。 - 数据驱动: 技能树的结构完全由配置表决定,UI 代码只负责根据 ID 实例化节点并连线。
4.3 复杂案例:动态异步商城 (Dynamic Async Shop)
- 挑战:
- 结构复杂: 3 个子模块(热销、装备、消耗品),每个模块商品数量不定。
- 异步加载: 商品数据从服务器拉取,图标异步下载。
- 动态显隐: 购买后商品可能消失,甚至整个模块消失。
- 布局结构 (Hierarchy):
ShopScrollView(ScrollRect)ViewportContent(Vertical Layout Group + Content Size Fitter)Module_Hot(Vertical Layout Group + Content Size Fitter)Header(Text)Grid(Grid Layout Group + Content Size Fitter)
Module_Equip(…)Module_Consumable(…)
- 核心痛点:
- 布局错乱 (The Pop): 异步加载图片或实例化 Item 时,Content 高度没有及时刷新,导致模块重叠或无法滑动。
- 解决方案详解:
-
Content Size Fitter 链 (The Chain): 必须环环相扣,断一环则全崩。
- 底层 (Item): 必须有明确高度。
- 挂
Layout Element,设置Min Height或Preferred Height(例如 200)。 - 或者 Item 本身就是固定高度的 Prefab。
- 挂
- 中间层 (Module/Grid): 必须能被子物体撑大。
- 挂
Content Size Fitter。 - 设置
Vertical Fit= Preferred Size。 - 关键: 这会让 Module 的高度自动等于所有 Item 高度之和。
- 挂
- 顶层 (Content): 必须能被 Module 撑大。
- 挂
Content Size Fitter。 - 设置
Vertical Fit= Preferred Size。 - 关键: 这会让 Content 的高度等于所有 Module 高度之和,从而让 ScrollRect 能够滚动。
- 挂
- 底层 (Item): 必须有明确高度。
-
强制刷新 (The Force Refresh):
- 为什么需要?: Unity UI 的 Layout 计算通常在帧末尾进行。当你在一帧内
Instantiate->SetParent->SetActive,Layout 系统还没来得及计算新高度,ScrollRect 就可能认为 Content 还是 0 高度。 - 正确代码:
- 为什么需要?: Unity UI 的 Layout 计算通常在帧末尾进行。当你在一帧内
-
异步加载防抖 (Anti-Jitter):
- 问题: 图片异步下载回来前,Image 组件可能大小为 0,导致 Item 高度为 0;图片回来后 Item 突然变大,把下面的模块顶下去。
- 解决: 在 Item 上挂
Layout Element,并设置Preferred Height为图片的目标高度(如 200)。这样即使图片没加载出来,位置也已经占好了。
-
Content Size Fitter 链 (The Chain): 必须环环相扣,断一环则全崩。
🔗 5. 参考资料
- 📺 Unity: Optimizing Unity UI
- 🛠️ GitHub: LoopScrollRect
- 📄 Blog: UGUI 性能优化总结