Skip to main content

🎨 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。

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 变得巨大,遮挡视野。
  • 安全区 (Safe Area):
    • 痛点: 刘海屏、灵动岛、圆角屏幕遮挡 UI。
    • 解决: 编写 SafeArea 脚本。
      • 在运行时获取 Screen.safeArea
      • 将 UI 根节点的 AnchorMinAnchorMax 设置为安全区比例。
      • 注意: 背景图不要挂 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 个)格子。
    • 滑动时,将移出屏幕的格子回收,放到另一端,并更新数据。
    • 插件推荐: EnhancedScrollerLoopScrollRect
    • 核心机制:
      • View Item vs Data Item: 只有 20 个 View Item (GameObject),但有 1000 个 Data Item (C# Class)。
      • Recycle Bin: 移出屏幕的 Item 不会被 Destroy,而是进入回收池。
      • Data Binding: 当 Item 从回收池取出放到另一端时,必须重新绑定数据。
      // 示例: LoopListView 的数据绑定回调
      LoopListViewItem2 OnGetItemByIndex(LoopListView2 listView, int index)
      {
          if (index < 0 || index >= totalDataCount) return null;
      
          // 1. 从池中获取 (GetFromPool)
          LoopListViewItem2 item = listView.NewListViewItem("ItemPrefabName");
          
          // 2. 获取数据 (GetData)
          var data = allInventoryData[index];
          
          // 3. 绑定数据 (BindData)
          var viewScript = item.GetComponent<InventoryItemView>();
          viewScript.SetIcon(data.icon);
          viewScript.SetCount(data.count);
          
          // 4. 重置状态 (Reset State) - 关键!
          // 因为这个 item 可能是复用的,它之前可能处于 "选中" 状态
          viewScript.SetSelected(data.isSelected); 
          
          return item;
      }
      

🤖 3. 阶段三:自动化与工业化 (Automation)

目标: 让机器写代码,让美术直接出界面。

3.1 代码生成 (UI Code Gen)

  • 拒绝 Find: transform.Find("Bg/Button") 是脆弱且低效的。
  • 自动绑定:
    1. 约定命名规范(如 btn_Close, txt_Title)。
    2. 编写编辑器脚本,遍历 Prefab。
    3. 自动生成 XXView.cs,包含 public Button btn_Close;
    4. 自动挂载脚本并序列化引用。

3.2 设计稿直通车 (Figma to Unity)

  • 痛点: 美术出图 -> 拼 UI -> 程序调位置 -> 美术验收不通过 -> 反复调整。
  • 流程:
    1. 美术在 Figma 中按规范作图(使用 Auto Layout)。
    2. 使用插件 (如 Doozy UI, Nova, 或开源的 Figma Converter for Unity) 导出。
    3. 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 粒子系统。

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)
      • Viewport
        • Content (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 高度没有及时刷新,导致模块重叠或无法滑动。
  • 解决方案详解:
    1. Content Size Fitter 链 (The Chain): 必须环环相扣,断一环则全崩。
      • 底层 (Item): 必须有明确高度。
        • Layout Element,设置 Min HeightPreferred 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 能够滚动。
    2. 强制刷新 (The Force Refresh):
      • 为什么需要?: Unity UI 的 Layout 计算通常在帧末尾进行。当你在一帧内 Instantiate -> SetParent -> SetActive,Layout 系统还没来得及计算新高度,ScrollRect 就可能认为 Content 还是 0 高度。
      • 正确代码:
      IEnumerator RefreshShopLayout()
      {
          // 1. 等待当前帧结束,确保所有 Instantiate 和 SetActive 逻辑已执行
          yield return null; 
      
          // 2. 强制立即重建 Layout
          // 注意:LayoutRebuilder 需要 RectTransform 参数
          // 建议从下往上刷,或者直接刷最顶层的 Content (开销略大但最稳)
          LayoutRebuilder.ForceRebuildLayoutImmediate(moduleHotRect);
          LayoutRebuilder.ForceRebuildLayoutImmediate(moduleEquipRect);
          LayoutRebuilder.ForceRebuildLayoutImmediate(rootContentRect);
          
          // 3. (可选) 如果嵌套极深,可能需要再等一帧
          // yield return new WaitForEndOfFrame();
      }
      
    3. 异步加载防抖 (Anti-Jitter):
      • 问题: 图片异步下载回来前,Image 组件可能大小为 0,导致 Item 高度为 0;图片回来后 Item 突然变大,把下面的模块顶下去。
      • 解决: 在 Item 上挂 Layout Element,并设置 Preferred Height 为图片的目标高度(如 200)。这样即使图片没加载出来,位置也已经占好了。

🔗 5. 参考资料