PSD转UI自动化:Unity UGUI预制体与代码生成实践

📅 发布时间:2026/9/7 13:20:53
PSD转UI自动化:Unity UGUI预制体与代码生成实践 先说一个很多 Unity 开发都有过的场景UI 设计稿交付之后你在编辑器里对着 PSD 一点点拖 Image、Button、Text调完层级调坐标调完坐标再写绑定代码。一个复杂活动页面几十个节点光“还原设计稿”就能消耗大半天更别说后续 UI 频繁迭代时每次都要重复找节点、改引用、更新坐标。这不是操作熟练度的问题而是流程设计的问题。设计稿到 UI 预制体之间原本就存在一条可以自动化的链路PSD 图层结构天然包含节点层级、名称、位置、大小、可见性这些信息完全可以通过脚本解析出来再映射成 UGUI 预制体。再进一步如果图层命名足够规范Button、Text、Image 这些组件也可以根据命名规则自动挂载连序列化字段和查找绑定都可以一并生成。本文要讲的 ETYIUI正是围绕这条链路设计的一套 PSD 转 UI 自动化方案。先给一个明确判断这类工具真正的核心不在于“自动生成节点”而在于三件事——图层命名规范、组件映射规则、代码生成策略。工具只是把“人和人约定的 UI 结构”翻译成“预制体 可读代码”的翻译器。翻译器不会思考规范不清晰出来的预制体一样是垃圾。所以这篇文章不会只停留在介绍功能。我会从为什么需要讲起把 PSD 导出结构、坐标转换、预制体生成、自动绑定、View/Controller 代码生成的完整链路拆开提供可复制的核心代码。读完你可以自己实现一个最小可用的 PSD 转 UI 工作流也可以判断是否值得在自己的项目里引入 ETYIUI 这类工具。1. 为什么 PSD 转 UI 是 UI 开发流程里的刚需1.1 传统手动搭建的痛点一个中等复杂度的 UI 界面设计稿里通常有几十个图层。手动搭建时前端开发要反复做这几件事对照设计稿创建 GameObject调整大小和位置。根据视觉需求挂 Image、Button、Text 等组件并设置属性。按照功能需求手动拖拽赋值把 UI 组件引用填到 View 脚本里。在代码里监听按钮点击、刷新文本内容。UI 改版后重复执行上述所有步骤。这个流程的问题在于UI 设计工具里的信息没有复用。PSD 里的图层名、层级关系、坐标、大小本来就是一种“结构化的信息”但到了 Unity 里全部要靠人肉重新录入一遍既慢又容易出错。图层一多还会出现更麻烦的情况命名混乱导致找不到节点父子层级和锚点设置不一致导致分辨率适配出问题同一个界面反复改版导致绑定代码大量堆叠。最终 UI 代码质量受影响联调效率也低。1.2 自动化能做到什么程度PSD 转 UI 自动化不是要替代开发者而是把上述流程里“确定性最强、重复度最高”的环节交给程序处理。以 ETYIUI 这类方案来看能做到的包括解析 PSD 图层树得到完整的节点名称、层级、坐标、宽高、可见性。在 Unity 中按图层树结构递归生成 RectTransform 节点。根据命名规则自动挂载 Image、Button、TextMeshProUGUI 等组件。根据命名规则自动生成 C# 字段并完成序列化引用赋值。自动生成 View / Controller 脚本骨架减少重复代码书写。这套能力解决的不是“你会不会写 UI 代码”而是“你每个 UI 界面都要重复付出多少成本”。对以页面数量多、版本迭代快为核心特征的游戏和 App 项目来说节省的成本非常可观。1.3 什么样的团队最适合引入一条经验是PSD 转 UI 自动化更适合以下团队项目 UI 界面数量多且大量是列表页、活动页、商城页等结构相似的页面。团队里有明确的 UI 设计规范或者愿意在自动化引入阶段先统一设计稿规范。前端开发需要同时维护多个版本UI 改动频繁。项目已经沉淀了 UI 框架比如基础 View 基类、按钮扩展、图集管理、多语言组件。反过来如果项目只有一个主界面或者设计稿本身非常混乱、连图层命名都没有统一规则那引入这类工具前要先补设计规范否则工具发挥不了作用。2. ETYIUI 的核心工作流与模块拆解2.1 工作流的四个阶段ETYIUI 这类工具的工作流可以拆成四个阶段阶段输入输出关键动作设计稿准备PSD 文件分层规范的 PSD 结构 JSON统一图层命名标记节点类型结构解析结构 JSON中间数据模型读取图层树、坐标、宽高、可见性预制体生成中间数据模型UGUI Prefab递归创建节点映射 UI 组件设置 RectTransform绑定与代码生成节点树 命名规则View / Controller 脚本生成可绑定字段、赋值逻辑、事件监听骨架这四个阶段里第一阶段最容易被人忽略但往往决定后三个阶段能否跑通。很多团队试水 PSD 转 UI 失败不是程序生成逻辑有问题而是设计稿没有规范生成出来的节点树完全不可用。2.2 两条成败关键第一条是命名规则。工具必须能从图层名称里识别出“这是一个按钮”“这是一个文本”“这是一个需要排除的辅助图层”。如果图层名毫无规律自动化就无从谈起。第二条是“生成代码”和“手写代码”的边界。自动生成的代码必须通过固定标记与手写代码隔离否则下一次重新生成时开发者自己加的字段、事件、业务逻辑会被直接覆盖。理解了这两点再去看后文的核心代码才不会云里雾里。3. PSD 图层命名规范与设计侧准备3.1 为什么设计规范是第一优先级机器不会理解设计意图。看到“图层 2 副本 3”这种名字工具只能创建同名节点无法判断它应该是一个按钮还是一张图片。所以 PSD 转 UI 的第一步是在设计侧建立命名规范。规范越稳定生成结果就越可控。建议在设计还原阶段就导入规范将常见控件类型纳入命名约束图片名称_Img按钮名称_Btn文本名称_Txt面板容器名称_Panel忽略图层#Ignore_名称特殊容器#名称#前缀适合用来标记工具的指令性信息例如忽略某个装饰性图层或者把某个图层组映射为代码里的容器节点。不同团队可以按自己需要扩展。3.2 一套可以直接用的命名约定下面是一套可落地的约定示例LoginUI - 顶层组对应界面根节点 ├── Bg_Img - 全屏背景图片 ├── #Ignore_Guide - 装饰引导层生成时忽略 └── #Panel_Center - 中间内容容器 ├── Title_Txt - 标题文本 ├── Enter_Btn - 登录按钮 ├── User_Img - 用户头像图片 └── Tip_Txt - 提示文本这套约定只看名字就能映射出组件类型。实际项目中还可以加更多后缀比如_Input对应 TMP_InputField、_Toggle对应 Toggle、_Scroll对应 ScrollRect。规则越多自动化能覆盖的场景就越广。3.3 用 Photoshop 脚本导出图层结构 JSONUnity 端无法直接读取 PSD 的工程结构更通用的做法是让 Photoshop 把图层树导出成 JSON。可以交给设计师在出图时执行也可以由工具开发者集成到 CI 中。在 Photoshop 中新建脚本保存为.jsx文件通过菜单“文件 - 脚本 - 浏览”运行// 文件路径Photoshop/ExportUIStructure.jsx if (app.documents.length 0) { alert(请先打开要导出的 PSD 文件); } else { var doc app.activeDocument; var data { name: doc.name.replace(/\.[^\.]$/, ), width: doc.width.value, height: doc.height.value, layers: parseLayer(doc.layers) }; var json JSON.stringify(data, null, 2); var file new File(doc.path / data.name .ui.json); file.open(w); file.write(json); file.close(); alert(导出完成: file.fsName); } function parseLayer(layers) { var result []; for (var i 0; i layers.length; i) { var layer layers[i]; var item { name: layer.name, visible: layer.visible, type: layer.typename }; if (layer.typename ArtLayer) { var b layer.bounds; item.x b[0].value; item.y b[1].value; item.w b[2].value - b[0].value; item.h b[3].value - b[1].value; } if (layer.typename LayerSet layer.layers.length 0) { item.children parseLayer(layer.layers); } result.push(item); } return result; }导出后的 JSON 结构大致如下{ name: LoginUI, width: 1920, height: 1080, layers: [ { name: Bg_Img, visible: true, type: ArtLayer, x: 0, y: 0, w: 1920, h: 1080 }, { name: #Ignore_Guide, visible: true, type: LayerSet, children: [] }, { name: #Panel_Center, visible: true, type: LayerSet, children: [ { name: Title_Txt, visible: true, type: ArtLayer, x: 710, y: 120, w: 500, h: 80 }, { name: Enter_Btn, visible: true, type: ArtLayer, x: 760, y: 360, w: 400, h: 120 } ] } ] }这里有两个容易踩的坑。一是 ExtendScript 对JSON.stringify的支持取决于 Photoshop 版本若脚本报错可以改成手动拼接字符串。二是layer.typename LayerSet对应的是图层组ArtLayer对应普通图层实际项目中有时还会出现文字图层、形状图层等类型需要按需过滤。拿到 JSON 之后Unity 端的工作就开始了。4. Unity 侧环境准备4.1 开发环境本文的代码基于 UGUI 和 TextMeshPro建议环境如下Unity 2021.3 及以上版本版本差异会在文中标注。UI 方案以 UGUI TextMeshPro 为准这也是目前 Unity 项目的常见组合。需要在 Package Manager 中确认已安装TextMeshPro相关资源包。编辑器工具代码放在Assets/ETYIUI/Editor目录下避免打进游戏包。如果你正在使用 UI Toolkit 或 FGUI本文的“预制体生成”阶段需要替换为对应框架的节点创建逻辑但图层解析、命名规范、代码生成的思路仍然适用。4.2 编辑器脚本目录结构建议把工具代码与生成产物分开避免后期资源维护混乱Assets/ETYIUI/ ├── Editor/ │ ├── PsdUI/ │ │ ├── PsdJsonLoader.cs │ │ ├── PsdRectTransformHelper.cs │ │ ├── UiGenerator.cs │ │ ├── UiCodeGenerator.cs │ │ └── PsdUiMenu.cs ├── Generated/ │ ├── Prefabs/ │ └── Scripts/ └── Runtime/Editor下放生成逻辑和菜单入口Generated目录专门存放自动生成的 Prefab 和脚本Runtime放 View 基类等运行时依赖。这样做的好处是自动生成的内容可以被清晰识别Review 和回滚都方便。4.3 用 JsonUtility 读取 PSD 结构 JSONUnity 自带的JsonUtility可以满足基础解析需求只要数据类是可序列化的字段结构。先定义数据模型// 文件路径Assets/ETYIUI/Editor/PsdUI/PsdLayerNode.cs using System; using System.Collections.Generic; [Serializable] public class PsdLayerNode { public string name; public bool visible; public string type; public float x; public float y; public float w; public float h; public ListPsdLayerNode children new ListPsdLayerNode(); } [Serializable] public class PsdUiDoc { public string name; public float width; public float height; public ListPsdLayerNode layers new ListPsdLayerNode(); }再写一个加载器// 文件路径Assets/ETYIUI/Editor/PsdUI/PsdJsonLoader.cs using System.IO; using UnityEngine; public static class PsdJsonLoader { public static PsdUiDoc Load(string jsonPath) { string json File.ReadAllText(jsonPath); return JsonUtility.FromJsonPsdUiDoc(json); } }这段代码有两个注意点。第一JsonUtility不支持顶层数组所以 JSON 根必须是对象我们的PsdUiDoc正好满足。第二如果 JSON 中某个字段缺失JsonUtility会保留 C# 字段的默认值不会直接抛异常调试时要留意字段名是否拼写一致。5. 自动生成 UI 预制体的核心逻辑5.1 PSD 坐标到 UGUI 坐标的转换这是很多初学者会忽略的关键点。PSD 的坐标原点在画布左上角Y 轴向下UGUI 的 RectTransform 在没有锚点偏移时anchoredPosition是以 pivot 为基准、相对锚点中心的偏移。两者不转换生成的节点会整体颠倒或偏移。假定 CanvasScaler 的参考分辨率是 1920x1080子节点锚点统一为屏幕中心那么转换逻辑可以写成// 文件路径Assets/ETYIUI/Editor/PsdUI/PsdRectTransformHelper.cs using UnityEngine; public static class PsdRectTransformHelper { public static Vector2 PsdToAnchored( float psdX, float psdY, float psdW, float psdH, float canvasWidth, float canvasHeight) { float centerX psdX psdW * 0.5f; float centerY psdY psdH * 0.5f; float uiX centerX - canvasWidth * 0.5f; float uiY canvasHeight * 0.5f - centerY; return new Vector2(uiX, uiY); } }这段代码的前提是 pivot 为(0.5, 0.5)锚点为屏幕中心。如果界面里有全屏背景、贴边容器则需要通过命名规则或宽高判断特殊处理。例如当节点宽高等于画布大小时直接设置为 Stretch 锚点保证不同分辨率下背景可以自动撑满。5.2 递归生成 GameObject 与组件映射生成预制体的核心是一个递归函数从根节点开始遍历图层树为每个节点创建 GameObject然后根据命名后缀挂载 UI 组件。下面是核心生成逻辑// 文件路径Assets/ETYIUI/Editor/PsdUI/UiGenerator.cs using UnityEngine; using UnityEngine.UI; using TMPro; using UnityEditor; public static class UiGenerator { public static GameObject Build(PsdUiDoc doc) { GameObject root new GameObject(doc.name, typeof(RectTransform)); RectTransform rootRect root.GetComponentRectTransform(); rootRect.sizeDelta new Vector2(doc.width, doc.height); // 生成到场景中的第一个 Canvas 下实际使用时可改为挂到当前选中节点 #if UNITY_2023_1_OR_NEWER Canvas canvas Object.FindFirstObjectByTypeCanvas(); #else Canvas canvas Object.FindObjectOfTypeCanvas(); #endif if (canvas ! null) { root.transform.SetParent(canvas.transform, false); } foreach (var layer in doc.layers) { BuildNode(layer, root.transform, doc.width, doc.height); } return root; } private static void BuildNode(PsdLayerNode node, Transform parent, float canvasWidth, float canvasHeight) { string finalName ProcessLayerName(node.name); if (finalName null) { return; } GameObject go new GameObject(finalName, typeof(RectTransform)); go.transform.SetParent(parent, false); RectTransform rect go.GetComponentRectTransform(); rect.sizeDelta new Vector2(node.w, node.h); Vector2 pos PsdRectTransformHelper.PsdToAnchored( node.x, node.y, node.w, node.h, canvasWidth, canvasHeight); rect.anchoredPosition pos; AddUiComponent(go, node.name); if (node.children ! null) { foreach (var child in node.children) { if (!child.visible) { continue; } BuildNode(child, go.transform, canvasWidth, canvasHeight); } } } private static string ProcessLayerName(string rawName) { if (rawName.StartsWith(#Ignore)) { return null; } if (rawName.StartsWith(#)) { return rawName.Substring(1); } return rawName; } private static void AddUiComponent(GameObject go, string rawName) { if (rawName.EndsWith(_Btn)) { if (go.GetComponentImage() null) { go.AddComponentImage(); } if (go.GetComponentButton() null) { go.AddComponentButton(); } } else if (rawName.EndsWith(_Txt)) { var tmp go.GetComponentTextMeshProUGUI(); if (tmp null) { tmp go.AddComponentTextMeshProUGUI(); } tmp.text rawName.Replace(_Txt, ); tmp.fontSize 36; tmp.alignment TextAlignmentOptions.Center; tmp.color Color.white; } else if (rawName.EndsWith(_Img)) { if (go.GetComponentImage() null) { go.AddComponentImage(); } } else if (rawName.EndsWith(_Panel)) { if (go.GetComponentImage() null) { go.AddComponentImage(); } } } }这里有一个值得说明的设计AddUiComponent里先判断组件是否已存在避免同一个节点被重复挂载。这在手动多次点击“生成”按钮时非常有用保证生成操作是幂等的。5.3 保存为 Prefab 并刷新资源生成了场景中的 GameObject 还不够需要保存成 Prefab 才能复用。Unity 2018.3 之后推荐使用PrefabUtility.SaveAsPrefabAsset// 文件路径Assets/ETYIUI/Editor/PsdUI/PsdUiMenu.cs using UnityEngine; using UnityEditor; public static class PsdUiMenu { [MenuItem(Assets/ETYIUI/生成 UI 预制体, false)] public static void GeneratePrefab() { string assetPath AssetDatabase.GetAssetPath(Selection.activeObject); if (string.IsNullOrEmpty(assetPath) || !assetPath.EndsWith(.json)) { Debug.LogWarning(请先选中由 Photoshop 导出的 .ui.json 文件); return; } PsdUiDoc doc PsdJsonLoader.Load(assetPath); GameObject root UiGenerator.Build(doc); string saveDir Assets/ETYIUI/Generated/Prefabs; if (!AssetDatabase.IsValidFolder(saveDir)) { AssetDatabase.CreateFolder(Assets/ETYIUI/Generated, Prefabs); } string savePath AssetDatabase.GenerateUniqueAssetPath( saveDir / doc.name .prefab); PrefabUtility.SaveAsPrefabAsset(root, savePath); Object.DestroyImmediate(root); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($生成完成{savePath}); } }这段代码实现了从“选中 JSON”到“生成 Prefab”的完整闭环。AssetDatabase.GenerateUniqueAssetPath会处理重名问题避免覆盖已有预制体。6. 自动绑定与 View/Controller 代码生成6.1 绑定规则命名后缀如何映射字段预制体生成后还不能直接用代码控制。传统做法是手动在 View 脚本里声明public Button btnEnter再回到编辑器里拖拽赋值。PSD 转 UI 自动化要做的就是把这步也省掉。思路是因为节点名已经带了后缀比如Enter_Btn那就可以在生成代码时直接生成字段btnEnter并在OnValidate里通过transform.Find自动查找并赋值。这样不需要任何手动拖拽。下面是字段命名的匹配规则PSD 图层名生成字段名组件类型自动绑定语句Bg_ImgimgBgImagetransform.Find(Bg_Img)?.GetComponentImage()Title_TxttxtTitleTextMeshProUGUItransform.Find(Title_Txt)?.GetComponentTextMeshProUGUI()Enter_BtnbtnEnterButtontransform.Find(Enter_Btn)?.GetComponentButton()User_ImgimgUserImagetransform.Find(User_Img)?.GetComponentImage()6.2 生成 View 脚本可以根据 JSON 节点树直接拼接生成一个 View 脚本。为了安全自动生成字段时做三件事去掉#前缀。去除_Btn、_Txt、_Img等后缀。首字母小写变成合法 C# 字段名。下面是一个简化的代码生成器// 文件路径Assets/ETYIUI/Editor/PsdUI/UiCodeGenerator.cs using System.Text; using UnityEngine; public static class UiCodeGenerator { public static string GenerateViewCode(PsdUiDoc doc) { var sb new StringBuilder(); sb.AppendLine(// 本文件由 ETYIUI 工具生成请勿手动修改 Auto Bind 区域); sb.AppendLine(using UnityEngine;); sb.AppendLine(using UnityEngine.UI;); sb.AppendLine(using TMPro;); sb.AppendLine(); sb.AppendLine($public class {doc.name}View : MonoBehaviour); sb.AppendLine({); CollectFields(sb, doc.layers); sb.AppendLine( // User Code Begin ); sb.AppendLine( // User Code End ); sb.AppendLine(); sb.AppendLine(#if UNITY_EDITOR); sb.AppendLine( private void OnValidate()); sb.AppendLine( {); sb.AppendLine( AutoBind();); sb.AppendLine( }); sb.AppendLine(); sb.AppendLine( private void AutoBind()); sb.AppendLine( {); GenerateBindLines(sb, doc.layers, ); sb.AppendLine( }); sb.AppendLine(#endif); sb.AppendLine(}); return sb.ToString(); } private static void CollectFields(StringBuilder sb, System.Collections.Generic.ListPsdLayerNode nodes) { foreach (var node in nodes) { string fieldName ToFieldName(node.name); if (fieldName null) { continue; } if (node.name.EndsWith(_Btn)) { sb.AppendLine($ [SerializeField] private Button {fieldName};); } else if (node.name.EndsWith(_Txt)) { sb.AppendLine($ [SerializeField] private TextMeshProUGUI {fieldName};); } else if (node.name.EndsWith(_Img)) { sb.AppendLine($ [SerializeField] private Image {fieldName};); } if (node.children ! null) { CollectFields(sb, node.children); } } } private static void GenerateBindLines(StringBuilder sb, System.Collections.Generic.ListPsdLayerNode nodes, string parentPath) { foreach (var node in nodes) { string path string.IsNullOrEmpty(parentPath) ? node.name : parentPath / node.name; string fieldName ToFieldName(node.name); if (fieldName null) { continue; } if (node.name.EndsWith(_Btn)) { sb.AppendLine($ {fieldName} transform.Find(\{path}\)?.GetComponentButton();); } else if (node.name.EndsWith(_Txt)) { sb.AppendLine($ {fieldName} transform.Find(\{path}\)?.GetComponentTextMeshProUGUI();); } else if (node.name.EndsWith(_Img)) { sb.AppendLine($ {fieldName} transform.Find(\{path}\)?.GetComponentImage();); } if (node.children ! null) { GenerateBindLines(sb, node.children, path); } } } private static string ToFieldName(string rawName) { if (string.IsNullOrEmpty(rawName)) { return null; } string processed rawName; if (processed.StartsWith(#)) { processed processed.Substring(1); } string fieldName processed .Replace(_Btn, ) .Replace(_Txt, ) .Replace(_Img, ) .Replace(_Panel, ); if (string.IsNullOrEmpty(fieldName)) { return null; } return char.ToLowerInvariant(fieldName[0]) fieldName.Substring(1); } }生成的LoginUIView.cs大致长这样// 本文件由 ETYIUI 工具生成请勿手动修改 Auto Bind 区域 using UnityEngine; using UnityEngine.UI; using TMPro; public class LoginUIView : MonoBehaviour { [SerializeField] private Button btnEnter; [SerializeField] private TextMeshProUGUI txtTitle; [SerializeField] private Image imgBg; // User Code Begin // User Code End #if UNITY_EDITOR private void OnValidate() { AutoBind(); } private void AutoBind() { btnEnter transform.Find(Bg_Img/Enter_Btn)?.GetComponentButton(); txtTitle transform.Find(Bg_Img/Title_Txt)?.GetComponentTextMeshProUGUI(); imgBg transform.Find(Bg_Img)?.GetComponentImage(); } #endif }注意示例中的 Find 路径取决于 PSD 图层树的实际层级。真实生成器里需要按照节点层级拼接路径而上面生成器里的parentPath参数就是在做这件事。6.3 生成 Controller 脚本View 负责持有 UI 引用Controller 负责把组件事件映射到业务逻辑。自动生成的 Controller 不需要太复杂能起到“标准入口”的作用即可// 文件路径Assets/ETYIUI/Generated/Scripts/LoginUIController.cs using UnityEngine; public class LoginUIController : MonoBehaviour { [SerializeField] private LoginUIView view; private void Awake() { if (view null) { view GetComponentLoginUIView(); } view.BtnEnter.onClick.AddListener(OnEnterClick); view.TxtTitle.text Login; } private void OnEnterClick() { Debug.Log(点击了登录按钮); } }因此 View 中的字段需要提供外部访问。你可以把自动生成的字段改为public或者在 View 中追加公开属性。自动生成工具建议统一采用public字段Controller 读取起来更直接。6.4 使用生成的 UI 并进行验证当 Prefab 生成完成后把它拖入场景给它挂上对应的 View 脚本和 Controller 脚本进入 Play 模式即可验证。验证重点有四个节点层级是否和 PSD 图层树一致。按钮、文本、图片是否自动找到并赋值。点击按钮是否能触发 Controller 中的监听方法。不同分辨率下位置是否有明显偏移。如果一切正常说明这条链路已经跑通。接下来是工程化层面的问题。7. 工程化扩展图集、多分辨率与增量生成7.1 Sprite 切片与图集归集PSD 转 UI 生成的节点最终还要有 Sprite 才能显示。通常每个图层切片会单独导出 PNG然后进入 Unity 后设置纹理的 Sprite 模式。这里建议从一开始就规划图集策略。一种常见做法是PSD 设计稿按照功能模块拆成多个 UI 图集每个图集导出时保持图层命名一致。Unity 中通过 SpriteAtlas 统一管理生成 Prefab 后再把对应 Sprite 挂到 Image 的sprite字段上。如果自动生成时图片还没有导入可以先留空等图集生成后再批量赋值。7.2 锚点与多分辨率适配严格的 PSD 转 UI 还需要考虑锚点。不同设计意图对应不同锚点全屏背景用 Stretch四个方向都贴边。顶部栏用 Top Centerx 方向随屏幕缩放。中间弹窗用 Center尺寸固定。底部按钮用 Bottom Center。锚点无法从图层名直接猜出来但可以通过命名规则约定。比如_Full后缀表示全屏_Top表示顶部。在设计规范阶段约定好生成器里做映射比将来逐个人工调整锚点要可靠得多。7.3 增量生成与 User Code 保护区自动生成代码最怕覆盖手写代码。推荐两个策略User Code Begin / End 标记生成器只覆盖标记区域外的内容标记区域之间的内容原样保留。partial classView 分为自动生成的XxxView.cs和手写的XxxView.Handler.cs手写文件不参与重新生成。第一种实现更简单直接在生成模板里保留固定标记即可。第二种结构更清晰适合 View 逻辑较多的项目。不管哪一种都要在生成前检查目标代码是否被外部修改避免无提示覆盖。8. 常见问题与排查方法问题现象可能原因排查方式解决方案生成的节点位置整体偏移或上下颠倒PSD 坐标到 UGUI 的 Y 轴没有翻转打印单个节点的转换前后坐标对比核对PsdToAnchored确认uiY canvasHeight / 2 - centerY字段名包含中文或空格导致 C# 编译失败图层命名没有清洗查看 Console 编译错误在ToFieldName中过滤非法字符必要时用拼音或英文映射表同一节点重复生成多个组件生成逻辑里没有判断组件是否已存在查看节点的 Inspector 组件列表在AddUiComponent中先GetComponent再AddComponent文本生成了但显示为方块或字体丢失TextMeshPro 缺少对应字体资源查看 TMP 组件字体设置在设计规范中约束字体名称生成后统一替换为项目字体资源手写代码被重新生成覆盖没有使用 User Code 保护区查看生成器的写入策略在生成模板中加入保护区标记或改为 partial classPrefab 保存后引用丢失DestroyImmediate(root)过早执行检查保存顺序和日志先在场景中完成赋值和绑定再SaveAsPrefabAsset多层嵌套的 Find 路径找不到节点parentPath 拼接与实际层级不一致打印生成的 Find 路径递归时统一使用处理后的节点名拼接路径全屏背景在不同分辨率下有黑边锚点仍是中心点而非 Stretch查看 Bg 节点的 RectTransform根据命名或宽高判断将全屏节点设置为 Stretch 锚点这些问题是 PSD 转 UI 工具落地时常遇到的直接照表排查效率最高。9. 最佳实践与工程建议9.1 把设计规范变成团队约定自动化工具是放大器。规范好效率放大规范差混乱也放大。项目中应当把图层命名规则、切片要求、文本规范写进设计规范文档并给到设计团队和外包团队。引入阶段建议先做小范围试点选一个中等复杂度的界面跑通全部流程收集问题后再推广到整个项目。9.2 生成代码与手写代码严格分离这是最容易在后期爆雷的点。一定要确保自动生成的文件有固定文件头比如“本文件由 ETYIUI 工具生成”并且在 Code Review 时提醒团队不要直接修改自动生成区域。推荐开启生成目录的只读提示或者在生成器中加入哈希校验若检测到手写改动则先备份再覆盖。9.3 把生成流程接入团队 CI如果团队人力充足可以把 Photoshop 脚本和 Unity 编辑器生成脚本串成一条命令在 CI 上自动完成“设计稿 → JSON → Prefab → 代码”的流水线。这样设计稿有改动时只需触发重新生成提交内容可以通过 Git Diff 查看。9.4 配合 SpriteAtlas 和资源大小管理自动生成预制体后要检查资源的导入设置包括纹理压缩格式、Sprite 图集归类、是否生成 Mipmap 等。UI 资源管理不是 PSD 转 UI 工具的核心但没有资源规范生成出来的界面表现和包体大小都会失控。9.5 保留手工微调入口自动化生成的界面不一定是最终效果。开发者往往还需要微调文本间距、按钮回调、特殊动效。设计工具时不要试图消灭一切手动操作而是把“机械还原”和“业务微调”分开。机械还原交给工具业务微调留在手写区域。10. 总结与下一步建议PSD 转 UI 自动化的核心价值是把设计稿到预制体之间的重复劳动变成可复用规则。ETYIUI 的工作流看起来模块很多但真正要紧的是四个环节图层命名规范、JSON 结构解析、预制体递归生成、代码绑定生成。把这四条链路打通哪怕是一个只有几百行代码的编辑器工具也能在实际项目中节省大量人力。接下来可以按这个顺序实践先整理自己项目的 UI 命名规范至少覆盖_Btn、_Txt、_Img、_Panel、#Ignore这几种。用 Photoshop 脚本导出一个测试界面的结构 JSON检查图层树是否符合预期。把文中的UiGenerator和UiCodeGenerator集成到自己的 Editor 工程跑通一个最小样例。在样例通过后补充自己的组件类型、锚点规则、图集归集逻辑。最后再考虑接入 CI、做增量生成、支持更多设计工具格式。一个值得进一步研究的方向是如何从设计稿自动判断布局意图。比如根据多个节点的对齐关系自动设置锚点或者根据按钮和文本的包含关系自动生成组合控件。这是 PSD 转 UI 工具从“能用”走向“好用”的关键也是在自己实现时可以持续投入精力的方向。