尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PSD自动转游戏UI:从图层解析到引擎还原的完整实践

PSD自动转游戏UI:从图层解析到引擎还原的完整实践 美术同学丢过来一份PSD客户端这边要在一个晚上之内把界面搭出来——做过游戏UI的人对这套流程应该都不陌生。传统做法是拿到PSD之后人工分层、手动导出、逐个定位、拼进图集、再拖到引擎里排坐标光一个战斗结算界面就能耗掉大半天。这篇文章要聊的是“PSD自动转游戏UI”的一种新思路从解析图层结构、自动切图、记录坐标到生成图集和UI描述文件把整条流水线串起来。适合Unity、Cocos、Godot或者自研引擎的客户端程序、技术美术和UI设计师参考核心思路可以直接改造进自己项目。先说一下这个方案解决的核心痛点。游戏UI和普通网页UI不一样设计稿里一个按钮可能拆了七八个图层底色、描边、内阴影、高光、选中态、禁用态每个状态又可能单独做了图标。如果让程序手工拼很容易漏掉某个状态而且美术改了字体大小、挪了几个像素之后程序又要对着PSD重新量一遍坐标。自动转换的思路本质上就是让PSD本身变成“可解析的数据源”而不是一张只能人工看的图。只要图层组织够规范程序完全可以替代人眼完成切图和定位。1. 思路拆解为什么“自动转换”比“手动切图”靠谱1.1 传统流程的瓶颈到底在哪先看传统流程里最花时间的几个环节。切图阶段程序或者技术美术需要在PSD里逐个勾选可见图层导出PNG命名尽量贴合代码里的引用名定位阶段要靠标尺或者PS的“拷贝CSS”类似功能把每个控件的x、y、宽高记下来九宫格识别要靠肉眼判断哪条边可以拉伸哪条边不能动最后还要把这些碎片拼成一张图集再手工在引擎里创建对应的Image控件、填写坐标和锚点。这里面最容易出错的其实是坐标。PSD设计稿是顶左原点坐标系游戏引擎坐标系通常也是顶左或中心原点看起来差不多但设计稿里有大量的透明留白一个图层的位置不是它视觉内容的位置。手动量的时候只要把一个图层的透明边距算错整个按钮的点击区域就和显示内容错位。版本迭代更麻烦美术把某个图标往右挪了5px程序在引擎里就要改一个数字然后重新打包牵一发动全身。1.2 新思路的核心把“图层名”变成配置文件我踩过上面的坑之后一直在想怎么把流程机器化。后来总结出来的核心思路很简单让图层命名变成一套“机器可读的规则”程序通过解析图层树就能知道哪些图层该导出、哪些图层是容器、哪些图层要做九宫格、哪些图层要生成文本占位。听起来有点像给PSD加XML注释但实际做起来以后效果非常明显。美术只要按规范命名比如按钮根图层叫btn_hero_1内部的高光和阴影图层用下划线或者--前缀标识为“合并到父级”解析程序就能自动把子图层拍平到按钮上导出一张干净的按钮图同时把按钮的中心点和大小记进JSON。这一步做完切图和定位的误差就和人没关系了PSD里是什么坐标引擎里就是什么坐标。1.3 技术选型对比JSX脚本还是独立解析器实现这个思路主要有两条路一条是在Photoshop内部跑JSX脚本直接控制PS的动作优点是能拿到文字图层渲染、混合模式、智能对象等最终合成效果另一条是用psd-tools这类独立库在外部解析PSD文件优点是不依赖PS环境可以批量处理而且能在CI/CD流水线里自动化。我在项目里实际对比过这两种方式。JSX脚本更贴近“所见即所得”因为它本质上是让PS自己去执行菜单里的那些功能比如“导出为PNG”“复制CSS”但缺点也很明显每个美术的电脑都要装PS且版本得匹配跑大规模的批量任务特别费力而且PSD里如果用了太多调整图层脚本需要先把所有图层合并成像素图层再导出流程一复杂速度就慢下来了。psd-tools则完全在内存里解析二进制格式快但拿不到PS里智能对象没栅格化之前的路径和文字图层的字体信息这两个能力差异决定了选型倾向。我的建议是双轨并行。编辑器里的临时预览用JSX脚本双击就能把当前打开的PSD快速导出一版给客户端自测正式批量转换走Python结合psd-tools的离线流程将解析结果输出成标准JSON和PNG图集最后引擎加载器再读取这些JSON。这样美术日常迭代可以在PS里做客户端打包和提测流水线又能跑全量自动化两边不耽误。顺带提一句市面上一些“图片转psd分层”的插件思路是把一张扁平图拆解成多层PSD方便美术二次编辑而我们游戏UI自动化要解决的其实是反向问题把PSD的层级和位置信息提取出来变成引擎可以直接消费的资源描述。这两个方向听起来都在搞PSD但目标不同别被工具功能带偏。2. 核心细节与实操要点让PSD图层变成能用的UI树2.1 图层树解析与分组语义PSD文件在结构上是一个嵌套的树根节点通常对应画布画布下面有多个图层组图层组里可以继续嵌套子组或者普通图层。做自动转换的第一步就是把这棵树完整遍历出来然后按照你定义的语义去映射。我用的映射规则大概是这样一个图层组对应UI上的一个“容器节点”或“复合控件”普通像素图层对应一张贴图或一个Sprite如果某个普通图层名称以特殊前缀开头比如nine_那它同时需要被识别成九宫格控件。图层组里的子图层只要没有标记为单独导出默认全部拍平合并到组里输出成一张图。这样设计的好处是不需要程序去管“哪个图层是背景、哪个是阴影”只要美术把同类控件放到一个组里输出就自然收敛到一个控件节点上。2.2 命名规范与匹配规则命名规范是整个方案里最重要的一环没有之一。我最初吃过亏让美术“尽量按规则命名”结果每个人风格不一样有的用中文有的用小驼峰有的在名字后面加了数字序号解析脚本写了不少兼容逻辑还是漏洞百出。后来我干脆做成强约束不符合规则的图层不导出并给出警告日志逼着美术按规范来。下面是我打磨过的一版命名规则可以直接抄前缀/关键词含义导出处理bg_背景层导出为整图生成根节点背景btn_按钮根图层整个图层组导出为一张按钮图icon_图标层单独导出Sprite不参与父子合并nine_九宫格图层导出并检测边缘生成insets参数txt_文本占位层不导图只记录矩形框作为文字区域pop_弹窗根节点生成弹窗容器记录缩放锚点--前缀合并到父级该图层不单独导出拍平进父组LL等后缀锚点控制节点在父容器内的对齐位置美术只要在图层名里带上这些关键词程序就能自动决定处理方式。比如有个图层叫btn_skill_1解析器看到btn_前缀就会把这个图层组里的所有子图层名字带--的那些拍平成一张按钮图并把这个按钮的宽高、中心点位置记录为控件数据。对美术来说命名习惯调整一次之后后续工作流完全不用变但对程序来说解析逻辑大幅简化。2.3 坐标换算与透明裁切PSD里每个图层都有四个关键字段left、top、right、bottom表示这个图层在画布里的完整包围盒。这个包围盒是包含透明像素的直接拿来当UI坐标会出大问题——一个图标在PSD里左侧留了30px透明边距视觉中心点其实在包围盒偏右的位置。因此自动转换时要做一次“透明裁切”把图层内容中真正不透明的区域切出来同时记录裁切后的偏移量。具体换算过程是这样的假设PSD设计稿分辨率是1280x720某个图层在原始坐标系里的left100, top120透明裁切后发现不透明区域左上角相对原始包围盒偏移了offset_x8, offset_y10裁切后的宽高是w120, h80。那么在UI坐标系里这个控件的实际位置就是x1008108y12010130实际尺寸就是120x80。这套换算逻辑必须严格保留因为之后拼图集和引擎加载都要依赖这套坐标。2.4 九宫格识别要自动也要有兜底九宫格是游戏UI里特别常见的需求尤其是对话框底板、按钮底框这类需要跟着内容长度拉伸的素材。手动做九宫格通常是在PS里手动量四条边的可拉伸区域再填进引擎的slice参数既慢又容易记错。自动识别的基本思路是裁切后的图片四周各取一条中心线检测边缘像素是否连续透明或不透明从而推断上下左右四条边哪些区域是可安全拉伸的。这套逻辑在纯色或者规则渐变背景上识别得比较准但遇到带描边、投影、花纹的素材就容易误判。所以我的做法是分两档自动检测出的insets作为初始值如果美术在图层名里手动写了四边距离比如nine_6_12_6_12表示左6右12上6下12解析器就直接用人手填的数值不再自动检测。自动检测只作为没有手动标记时的兜底方案。这样既有自动化效率又保留人工精确控制的口子。3. 实操过程写一个可落地的PSD转UI小工具3.1 先用psd-tools把图层树捞出来前半段要落地先得把PSD文件读明白。我在离线转换工具里用的是Python的psd-tools库读取图层树非常直观。看下核心代码from psd_tools import PSDImage psd PSDImage.open(hero_battle.psd) print(f画布尺寸: {psd.width} x {psd.height}) def walk(layer, depth0): if layer.is_group(): print( * depth f[组] {layer.name} | 位置({layer.left}, {layer.top})) for child in layer: walk(child, depth 1) else: print( * depth f[层] {layer.name} | {layer.kind} | f包围盒({layer.left}, {layer.top}, {layer.right}, {layer.bottom})) walk(psd)这段代码会把整个图层树打出来让我快速确认命名规范有没有被遵守。实际项目里我一般还会统计不合规图层的数量超过一定比例就中断流程提醒美术同学先改命名。3.2 自动切图并记录坐标偏移拿到图层树之后下一步就是遍历所有需要导出的图层把每个图层的内容裁出来存成PNG同时把坐标和偏移量记录到字典里。推荐用layer.composite()先把图层最终渲染成图像再用PIL的getbbox()获取不透明区域的包围盒from psd_tools import PSDImage from PIL import Image import json, os psd PSDImage.open(hero_battle.psd) os.makedirs(out, exist_okTrue) ui_nodes {} for layer in psd.descendants(): # 只看像素类或智能对象层文字层单独处理 if layer.kind not in (pixel, smartobject, type): continue # 名字里带 -- 的合并层不单独导出 if layer.name.startswith(--): continue img layer.composite() bbox img.getbbox() if not bbox: continue left, top, right, bottom bbox crop img.crop(bbox) filename fout/{layer.name}.png crop.save(filename) ui_nodes[layer.name] { x: layer.left left, y: layer.top top, w: right - left, h: bottom - top, src: filename, } with open(out/ui_nodes.json, w, encodingutf-8) as fp: json.dump(ui_nodes, fp, ensure_asciiFalse, indent2)这个脚本把“透明裁切”和“坐标换算”一次做完输出结果就是干净的切图加一张坐标表。关键点是layer.composite()拿到的图像坐标系和图层包围盒是同一个参考系所以裁切后的偏移量可以直接叠加在layer.left和layer.top上。3.3 生成UI描述文件把结构写进JSON切图只是第一步真正让游戏引擎能自动搭界面的是描述文件。我会为每个界面生成一份JSON里面包含图集引用、控件树、控件类型、锚点、九宫格参数等信息。示例大致长这样{ screen: HeroBattleScreen, canvas: { width: 1280, height: 720 }, atlas: ui_atlas, nodes: [ { name: bg_main, type: Image, x: 0, y: 0, w: 1280, h: 720, sprite: bg_main, anchor: top-left }, { name: btn_skill_1, type: Button, x: 1100, y: 540, w: 120, h: 120, sprite: btn_skill_1, anchor: bottom-right }, { name: nine_dialog_bg, type: Image, x: 300, y: 200, w: 640, h: 360, sprite: nine_dialog_bg, insets: [24, 24, 24, 24], type: Sliced } ] }程序拿到这份JSON之后完全可以自动在场景里创建对应的UI节点设置锚点、Sprite、RectTransform。我甚至会在JSON里直接附带type字段来区分普通图片、按钮、文本占位和九宫格这样引擎侧加载器不需要靠猜测来决定初始化方式。3.4 图集打包与引擎侧还原切出来的PNG数量通常很多需要打包成一张或多张图集。如果你们项目用的是TexturePacker命令行一条命令就能搞定TexturePacker --sheet ui_atlas.png --data ui_atlas.json --trim-mode None --padding 0 out/注意这里我特意写了--trim-mode None。为什么因为前面的切图脚本已经做完透明裁切并记录了偏移量图集工具如果再对每张图做一次裁剪坐标就需要再维护一层变换把这个二次裁切关掉图集里每个Sprite的rect就是JSON里对应的原始尺寸加载器计算坐标时就不用做反向补偿。这是很多人会踩的坑图集里每个子图一trimUnity的Sprite就自动带上border和offset信息看起来没事但一旦你自己写加载逻辑坐标就会莫名偏移。引擎侧还原的逻辑也很简单。以Unity为例加载器读取JSON后遍历节点列表解析出sprite引用用Resources.LoadAllSprite(ui_atlas)把图集里所有Sprite一次性加载进来再根据节点数据创建GameObject和Image组件。核心代码思路如下public class UIScreenLoader : MonoBehaviour { public string jsonPath UIJson/hero_battle_screen; void Start() { var textAsset Resources.LoadTextAsset(jsonPath); var screen JsonUtility.FromJsonUIScreenData(textAsset.text); var sprites Resources.LoadAllSprite(screen.atlas); foreach (var node in screen.nodes) { var go new GameObject(node.name); go.transform.SetParent(transform, false); var img go.AddComponentUnityEngine.UI.Image(); img.sprite System.Array.Find(sprites, s s.name node.sprite); if (node.type Sliced) { img.type UnityEngine.UI.Image.Type.Sliced; img.pixelsPerUnitMultiplier 1f; } var rt go.GetComponentRectTransform(); rt.sizeDelta new Vector2(node.w, node.h); rt.anchoredPosition new Vector2(node.x, -node.y); } } }这里有一个坐标系转换细节需要特别注意PSD的y轴向下为正Unity的UI坐标如果不做任何处理都是以中心点为原点且y轴向上为正。为了简单我选择把所有节点锚点设置在左上角并把y轴翻转这样PSD坐标就能直接映射到Unity的anchoredPosition。如果界面用了九宫格拉伸还需要额外从描述文件里读取insets并设置Image的sprite border这个字段可以在运行时通过Sprite.Create重新生成带border的Sprite或者提前在导入管线里注入。4. 常见问题与排查技巧实录4.1 智能对象和文字图层的渲染结果不对psd-tools在解析智能对象时拿到的composite()结果有时候是图层缩略图而不是高清原图尤其是PSD里包含外部链接的智能对象时解析出来的尺寸和清晰度可能跟PS里看到的不一致。文字图层也类似如果系统里没有安装对应字体解析出来的文字是缺字的位图坐标和大小都会跑偏。我的处理方式是在转换工具里增加“图层类型检测”遇到智能对象和文字图层就输出警告提示先用PS脚本把这类图层栅格化为像素图层再走自动化流程。美术侧养成一个习惯交付的PSD尽量不保留可编辑文字输出前“转为智能对象后再栅格化”一次。4.2 混合模式导出后效果完全不对PSD里常用的正片叠底、滤色、叠加这类混合模式导出时layer.composite()有可能会直接忽略混合关系导致输出的图标颜色不对尤其是阴影层用了正片叠底的时候。这个问题在独立解析器里很难完美解决因为混合模式涉及它和下方所有可见图层的叠代计算。最简单可靠的办法是混合模式相关的图层用--前缀标记为合并到父级在PS端预先拍平成像素结果不给独立解析器做计算的机会。换句话说自动化流程里只接受“最终合成好的像素层”不接受依赖混合关系的半成品层。4.3 图集边缘出现白边和压缩噪点UI素材打包成图集后常见的问题是拉伸之后边缘出现半透明白边。原因通常有两个一是图集打包时没有预留1像素内边距相邻Sprite在Mipmap采样时互相污染二是纹理压缩格式对透明通道有损透明度边缘被压出杂色。解决方法是在图集打包时把--padding设为2甚至4同时在引擎导入设置里把Mipmap关掉除非UI需要动态缩放的超大图压缩格式换成ASTC或ETC2而不是RGBA32硬压缩。UI图集的绘制距离一般不变Mipmap本来就是白白浪费内存还添乱的东西。4.4 九宫格识别总是把边角当成可拉伸区域自动九宫格检测的误判大多来自带投影或描边的素材。比如一个对话框底板四周有一圈渐变阴影阴影往外延伸了十几个像素自动检测就会误认为整条边缘都是可拉伸区域导致拉伸后在边角出现模糊色块。我的改进方式是不做逐像素二值判断而是取边缘外侧的透明度曲线找到“连续不透明段”的起止位置再结合左右对称性做一次平滑最后把自动检测结果和人工标记对比学习。更务实的方案还是回到命名兜底只要设计师在图层名里写了nine_10_10_20_20程序就直接用人手数值自动检测只是没有标记时的辅助。4.5 批量转换速度太慢怎么办如果PSD做得特别大图层几百上千个descendants()遍历加上每层composite()是一个很重的操作。我在实际批处理中观察过一个400层的PSD在普通办公电脑上可能要跑一两分钟其中大部分时间花在图层合成上。优化思路有两个方向第一提前在转换脚本里跳过不可见图层和纯装饰图层只要图层名以--开头就不执行composite()第二做增量转换——只在PSD文件哈希变化时重新处理整个文件否则直接复用上一次生成的JSON和切图这样美术小改单个图层时整个流水线可以秒级完成。如果项目对时效要求特别高还可以把“遍历图层树记录结构”和“像素级合成”拆成两个阶段先快速生成结构JSON给客户端预览占位后台慢慢补高清切图。5. 我兜了一圈之后留下的心得这套PSD自动转游戏UI的方案我在两个项目里实践过一个Unity项目一个自研引擎项目。最开始我也想着做一个“万能转换器”自动分析所有图层结构后来被现实教育了最终解决效率瓶颈的不是解析代码而是命名规范和执行力度。美术同学一旦理解了“图层名就是程序眼里的数据”他们其实很愿意配合因为省掉的是他们自己反复跟程序对坐标、对状态的沟通成本。我个人的建议是不要一上来就追求全自动生成完整可玩的界面先让工具自动导出“九宫格背景按钮图标”这类最机械的部分文本、动效和复杂交互仍然留给客户端手工装配。这就像给团队配了个历练好的实习生能扛下80%的基础体力活剩下20%需要人做判断的部分再交给人。后续想扩展的方向也很明确一是把JSON描述文件接进Unity的UI Toolkit或者UGUI自研编辑器做到双击一条记录就能在编辑器里预览还原结果二是把命名规范通过PS插件做成一键自检美术交付前点一下就能看到所有不合规的图层不需要等客户端跑完流程再返工三是把生成的UI节点数据接到自动化截图对比流程里改版之后跑一遍界面差异直接出图回归测试都能省不少人。这套思路的关键不在于某个库或者某个脚本有多神奇而在于把“PSD里的层级和位置”当成数据来对待让数据和命名成为美术和程序之间的接口协议。只要接口协议定好了工具怎么做都只是实现细节。
返回列表