> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wanyouxi.online/llms.txt
> Use this file to discover all available pages before exploring further.

# UI 制作从入门到精通：从手工匠人到工业化生产

> **摘要**：本文聚焦「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 根节点的 `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 从回收池取出放到另一端时，必须重新绑定数据。
    ```csharp theme={"theme":{"light":"github-light","dark":"github-dark"}}
    // 示例: 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 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 能够滚动。

  2. **强制刷新 (The Force Refresh)**:

     * **为什么需要?**: Unity UI 的 Layout 计算通常在帧末尾进行。当你在一帧内 `Instantiate` -> `SetParent` -> `SetActive`，Layout 系统还没来得及计算新高度，ScrollRect 就可能认为 Content 还是 0 高度。
     * **正确代码**:

     ```csharp theme={"theme":{"light":"github-light","dark":"github-dark"}}
     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. 参考资料

* 📺 **Unity**: [Optimizing Unity UI](https://learn.unity.com/tutorial/optimizing-unity-ui)
* 🛠️ **GitHub**: [LoopScrollRect](https://github.com/qiankanglai/LoopScrollRect)
* 📄 **Blog**: [UGUI 性能优化总结](https://www.uwa4d.com/)
