【Unity编辑器扩展】(三)PSD转UGUI Prefab, 一键拼UI解放美术/程序(完结)

工具效果:

第一步,把psd图层转换为可编辑的节点树,并自动解析UI类型、自动绑定UI子元素:

 第二步, 点击“生成UIForm"按钮生成UI预制体 (若有UI类型遗漏可在下拉菜单手动点选UI类型):

 验证一键生成UI效果:

书接上回:【Unity编辑器扩展】(二)PSD转UGUI Prefab, 图层解析和碎图导出_psd导入unity_TopGames的博客-CSDN博客

先上总结:

工具包含的功能:

1. 支持UGUI和TextMeshProGUI并新增FillColor(纯色)共16种UI类型:

Button、TMP Button、 Dropdown、TMP Dropdown、FillColor、Image、InputField、TMP InputField、Mask、RawImage、Scroll View、Slider、Text、TMP Text、Toggle、TMP Toggle。

2. 支持自定义UI类型匹配词,支持扩展自定义解析器Helper,易扩展。

3. 支持批量导出图片和单独导出某个图层图片,美术仅提供psd,无需切图。

4. 支持自动同步UI元素位置和文本字体、字号、行列间距、字体颜色。解放繁琐的手动调节流程。

5. 自动根据UI类型导出图片为Sprite、Texture2D类型,并支持选择导出后是否压缩图片,若UI需要9宫拉伸自动对Sprite设置9宫边界。

6. 支持编辑(手动调节层级或UI类型),如果UI设计师有遗漏标记类型,程序可手动点选类型,类型刷新后工具自动绑定UI元素。

7. 支持编辑阶段预览psd图层、组。

8. 支持任意UI类型嵌套组合,psd图层层级导出为UI预制体后保持一致。

Aspose.PSD库虽然很强大,但它毕竟是脱离PS的独立解析库,对于PS的有些功能支持并不完善,比如图层特效(如描边、浮雕、阴影等),把单个图层转换为图片图层的特效会丢失。对于文本图层,转换为图片后会有字体样式改变的问题。比如PS文本用的是宋体字体,转换为图片后变成了默认的雅黑字体。

好在Aspose.PSD支持半个PS智能对象,为什么说是半个,因为Aspose.PSD完美支持PS智能对象图层,但是,通过Aspose.PSD把带有特效的PS图层转换为智能对象后会丢失图层特效。

为了解决这一问题,不得不对之前的设计做出让步,写一个自动转换图层为智能对象的PS脚本,以避免设计师手动转换会有遗漏。UI设计师交付psd前通过脚本自动把所有文本图层和带有特效的图层转换为智能对象。这样才能绕过Aspose.PSD导出图片丢失图层特效的问题。

尽管有了使用PS脚本这个小瑕疵,但相比让UI设计师单独切图并手动标识各个切图位置大小、字体字号颜色等,他们仍然觉得这是一个巨大的解放。同样,对于技术来说,也节省大量时间。即使设计师遗漏了UI类型标记,也可以通过下拉框选择图层的UI类型,仅需简单标记类型就可以一键生成UI预制体。

Aspose.PSD仍在每月一个版本更新迭代,期待功能完善,摆脱所有瑕疵。

PSD转UGUI功能/工作流及原理:

 一、PSD规范要求(UI设计师)

由于UI大多属于复合型UI(如上图),即由多种UI元素类型组合而成。例如,Dropdown(下拉菜单),主要由下拉框+下拉列表+下拉列表Item三个主体组成,而三个主体又是由其他多个UI元素组成。

UI是由一个或多个UI元素构成,因此多个元素之间必须有父子节点的关系。而PS图层中没有这种关系,只能通过组(Group)把多个图层包起来,而组本身是一个空图层。

例如一个Button,通常包含一个背景图和一个按钮文本。图层结构如下:

实际上UI设计师原本也是需要用组来管理图层和切图的,这一规范并不是问题。主要是UI类型标记,通过对图层命名以".类型",工具通过对图层类型的识别以及每种UI有单独的解析Helper,最大程度上智能判定识别UI元素类型,对于无迹可寻的元素仍然需要设计师手动标记UI类型。

例如Button解析器(ButtonHelper), 会依次按类型查找图层, 可以最大化放宽对图层标记类型:

buttonBackground = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);
buttonText = LayerNode.FindSubLayerNode(GUIType.Button_Text, GUIType.Text, GUIType.TMPText);

二、解析规则配置

 支持配置文本图层和非文本图层的默认类型,例如文本图层默认识别为Text或TextMeshProGUI类型,普通图层默认识别为Image或RawImage类型。

UI Type: 主UI类型和子UI类型。支持的类型如下:

 UIPrefab: UI模板预制体。

TypeMatches:UI类型匹配名, 例如Button的匹配项有.bt,.btn,.button。图层名以这些字符结尾就会被识别为Button。

UIHelper: UI的解析逻辑。不同的UI通过重写解析方法对UI元素和对应PS图层进行绑定,以及生成最终的UI GameObject。

Comment:注释说明,用于一键导出说明文档给UI设计师参考。

总的来说,规则配置文件是为了更灵活宽松,可以自由自定义多个UI类型的别名。

以下是一键导出的文档内容:

使用说明:

单元素UI:即单个图层的UI,如Image、Text、单图Button,可以直接在图层命名结尾加上".类型"来标记UI类型。
如"A.btn"表示按钮。

多元素UI: 对于多个图片组成的复合型UI,可以通过使用"组"包裹多个UI元素。在“组”命名结尾加上".类型"来标记UI类型。
组里的图层命名后夹".类型"来标记为UI子元素类型。

各种UI类型支持任意组合:如一个组类型标记为Button,组内包含一个按钮背景图层,一个艺术字图层(非文本图层),就可以组成一个按钮内带有艺术字图片的按钮。

UI类型标识: 图层/组命名以'.类型'结尾

UI类型标识列表:

Image: UI图片, Sprite精灵图,支持九宫拉伸

类型标识: .img, .image,

RawImage: Texture贴图, 不支持九宫拉伸

类型标识: .rimg, .tex, .rawimg, .rawimage,

Text: UGUI普通Text文本

类型标识: .txt, .text, .label,

TMPText: Text Mesh Pro, 加强版文本类型. 通常无需标注此类型,使用Text类型即可

类型标识: .tmptxt, .tmptext, .tmplabel,

Mask: 遮罩图,根据遮罩图alpha对可使区域混合

类型标识: .msk, .mask,

FillColor: 纯色直角矩形图,例如直角矩形纯色图层可以在Unity中设置颜色实现,无需导出纯色图片

类型标识: .col, .color, .fillcolor,

Background: 背景图,  如Button背景,Toggle背景、InputField背景、ScrollView等

类型标识: .bg, .background, .panel,

Button: 按钮, 通常包含按钮背景图、按钮文本

类型标识: .bt, .btn, .button,

TMPButton: 按钮(Text Mesh Pro)

类型标识: .tmpbt, .tmpbtn, .tmpbutton,

Button_Highlight: 按钮高亮时显示的按钮图片(当按钮为多种状态图切换时)

类型标识: .onover, .light, .highlight,

Button_Press: 按住按钮时显示的图片(当按钮为多种状态图切换时)

类型标识: .press, .click, .touch,

Button_Select: 选中按钮时显示的图片(当按钮为多种状态图切换时)

类型标识: .select, .focus,

Button_Disable: 禁用按钮时显示的图片(当按钮为多种状态图切换时)

类型标识: .disable, .forbid,

Button_Text: 按钮文本,必须是文本图层. 如果是艺术字图片可以标记为Image

类型标识: .bttxt, .btlb, .bttext, .btlabel, .buttontext, .buttonlabel,

Dropdown: 下拉菜单, 由下拉框、下拉列表(ScrollVIew)、Toggle类型的item组成

类型标识: .dpd, .dropdown,

TMPDropdown: 按钮(Text Mesh Pro)

类型标识: .tmpdpd, .tmpdropdown,

Dropdown_Label: 下拉框上显示的文本

类型标识: .dpdlb, .dpdlabel, .dpdtxt, .dpdtext, .dropdowntext, .dropdownlabel, .dropdowntxt, .dropdownlb,

Dropdown_Arrow: 下拉框箭头图标

类型标识: .dpdicon, .dpdarrow, .arrow, .dropdownarrow,

InputField: 文本输入框,通常由输入框背景图、提示文本、输入文本组成

类型标识: .ipt, .input, .inputbox, .inputfield,

TMPInputField: 文本输入框(Text Mesh Pro)

类型标识: .tmpipt, .tmpinput, .tmpinputbox, .tmpinputfield,

InputField_Placeholder: 输入框内的提示文本

类型标识: .placeholder, .ipttips, .tips, .inputtips,

InputField_Text: 输入框输入的文本(样式)

类型标识: .ipttxt, .ipttext, .iptlb, .iptlabel, .inputtext, .inputlabel,

Toggle: 单选框/复选框

类型标识: .tg, .toggle, .checkbox,

TMPToggle: 勾选框/单选框/复选框(Text Mesh Pro)

类型标识: .tmptg, .tmptoggle, .tmpcheckbox,

Toggle_Checkmark: 勾选框,勾选状态图标

类型标识: .mark, .tgmark, .togglemark,

Toggle_Label: 勾选框文本

类型标识: .tglb, .tgtxt, .toggletext, .togglelabel,

Slider: 滑动条/进度条,通常由背景图和填充条组成

类型标识: .sld, .slider,

Slider_Fill: 滑动条/进度条的填充条

类型标识: .fill, .sldfill, .sliderfill,

Slider_Handle: 滑动条的拖动滑块

类型标识: .handle, .sldhandle, .sliderhandle,

ScrollView: 滚动列表,通常由背景图、垂直/水平滚条背景图以及垂直/水平滚动条组成

类型标识: .sv, .scrollview, .lst, .listview,

ScrollView_Viewport: 滚动列表的视口遮罩图

类型标识: .vpt, .viewport, .svmask, .lstmask, .listviewport, .scrollviewport,

ScrollView_HorizontalBarBG: 滚动列表的水平滑动条背景图

类型标识: .hbarbg, .hbarbackground, .hbarpanel,

ScrollView_HorizontalBar: 滚动列表的水平滑动条

类型标识: .hbar, .svhbar, .lsthbar,

ScrollView_VerticalBarBG: 滚动列表的垂直滑动条背景图

类型标识: .vbarbg, .vbarbackground, .vbarpanel,

ScrollView_VerticalBar: 滚动列表的垂直滑动条

类型标识: .vbar, .svvbar, .lstvbar,

 UGUI Parser代码:

#if UNITY_EDITOR
using Aspose.PSD.FileFormats.Psd.Layers.FillLayers;
using System;
using System.IO;
using System.Linq;
using System.Text;
using TMPro;
using UnityEditor;
using UnityEngine;namespace UGF.EditorTools.Psd2UGUI
{public enum GUIType{Null = 0,Image,RawImage,Text,Button,Dropdown,InputField,Toggle,Slider,ScrollView,Mask,FillColor, //纯色填充TMPText,TMPButton,TMPDropdown,TMPInputField,TMPToggle,//UI的子类型, 以101开始。 0-100预留给UI类型, 新类型从尾部追加Background = 101, //通用背景//Button的子类型Button_Highlight,Button_Press,Button_Select,Button_Disable,Button_Text,//Dropdown/TMPDropdown的子类型Dropdown_Label,Dropdown_Arrow,//InputField/TMPInputField的子类型InputField_Placeholder,InputField_Text,//Toggle的子类型Toggle_Checkmark,Toggle_Label,//Slider的子类型Slider_Fill,Slider_Handle,//ScrollView的子类型ScrollView_Viewport, //列表可视区域的遮罩图ScrollView_HorizontalBarBG, //水平滑动栏背景ScrollView_HorizontalBar,//水平滑块ScrollView_VerticalBarBG, //垂直滑动栏背景ScrollView_VerticalBar, //垂直滑动块}[Serializable]public class UGUIParseRule{public GUIType UIType;public string[] TypeMatches; //类型匹配标识public GameObject UIPrefab; //UI模板public string UIHelper; //UIHelper类型全名public string Comment;//注释}[CustomEditor(typeof(UGUIParser))]public class UGUIParserEditor : Editor{private SerializedProperty readmeProperty;private void OnEnable(){readmeProperty = serializedObject.FindProperty("readmeDoc");}public override void OnInspectorGUI(){serializedObject.Update();if (GUILayout.Button("导出使用文档")){(target as UGUIParser).ExportReadmeDoc();}EditorGUILayout.LabelField("使用说明:");readmeProperty.stringValue = EditorGUILayout.TextArea(readmeProperty.stringValue, GUILayout.Height(100));serializedObject.ApplyModifiedProperties();base.OnInspectorGUI();}}[CreateAssetMenu(fileName = "Psd2UIFormConfig", menuName = "ScriptableObject/Psd2UIForm Config【Psd2UIForm工具配置】")]public class UGUIParser : ScriptableObject{public const int UITYPE_MAX = 100;[SerializeField] GUIType defaultTextType = GUIType.Text;[SerializeField] GUIType defaultImageType = GUIType.Image;[SerializeField] GameObject uiFormTemplate;[SerializeField] UGUIParseRule[] rules;[HideInInspector][SerializeField] string readmeDoc = "使用说明";public GUIType DefaultText => defaultTextType;public GUIType DefaultImage => defaultImageType;public GameObject UIFormTemplate => uiFormTemplate;private static UGUIParser mInstance = null;public static UGUIParser Instance{get{if (mInstance == null){var guid = AssetDatabase.FindAssets("t:UGUIParser").FirstOrDefault();mInstance = AssetDatabase.LoadAssetAtPath<UGUIParser>(AssetDatabase.GUIDToAssetPath(guid));}return mInstance;}}public static bool IsMainUIType(GUIType tp){return (int)tp <= UITYPE_MAX;}public Type GetHelperType(GUIType uiType){if (uiType == GUIType.Null) return null;var rule = GetRule(uiType);if (rule == null || string.IsNullOrWhiteSpace(rule.UIHelper)) return null;return Type.GetType(rule.UIHelper);}public UGUIParseRule GetRule(GUIType uiType){foreach (var rule in rules){if (rule.UIType == uiType) return rule;}return null;}/// <summary>/// 根据图层命名解析UI类型/// </summary>/// <param name="layer"></param>/// <param name="comType"></param>/// <returns></returns>public bool TryParse(PsdLayerNode layer, out UGUIParseRule result){result = null;var layerName = layer.BindPsdLayer.Name;if (Path.HasExtension(layerName)){var tpTag = Path.GetExtension(layerName).Substring(1).ToLower();foreach (var rule in rules){foreach (var item in rule.TypeMatches){if (tpTag.CompareTo(item.ToLower()) == 0){result = rule;return true;}}}}switch (layer.LayerType){case PsdLayerType.TextLayer:result = rules.First(itm => itm.UIType == defaultTextType);break;case PsdLayerType.LayerGroup:result = rules.First(itm => itm.UIType == GUIType.Null);break;default:result = rules.First(itm => itm.UIType == defaultImageType);break;}return result != null;}/// <summary>/// 根据图层大小和位置设置UI节点大小和位置/// </summary>/// <param name="layerNode"></param>/// <param name="uiNode"></param>/// <param name="pos">是否设置位置</param>public static void SetRectTransform(PsdLayerNode layerNode, UnityEngine.Component uiNode, bool pos = true, bool width = true, bool height = true, int extSize = 0){if (uiNode != null && layerNode != null){var rect = layerNode.LayerRect;var rectTransform = uiNode.GetComponent<RectTransform>();if (width) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rect.size.x + extSize);if (height) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rect.size.y + extSize);if (pos){rectTransform.position = rect.position + rectTransform.rect.size * (rectTransform.pivot - Vector2.one * 0.5f)*0.01f;}}}/// <summary>/// 把LayerNode图片保存到本地并返回/// </summary>/// <param name="layerNode"></param>/// <returns></returns>public static Texture2D LayerNode2Texture(PsdLayerNode layerNode){if (layerNode != null){var spAssetName = layerNode.ExportImageAsset(false);var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(spAssetName);return texture;}return null;}/// <summary>/// 把LayerNode图片保存到本地并返回/// </summary>/// <param name="layerNode"></param>/// <param name="auto9Slice">若没有设置Sprite的九宫,是否自动计算并设置九宫</param>/// <returns></returns>public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false){if (layerNode != null){var spAssetName = layerNode.ExportImageAsset(true);var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spAssetName);if (sprite != null){if (auto9Slice){var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;var rawReadable = spImpt.isReadable;if (!rawReadable){spImpt.isReadable = true;spImpt.SaveAndReimport();}if (spImpt.spriteBorder == Vector4.zero){spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);spImpt.isReadable = rawReadable;spImpt.SaveAndReimport();}}return sprite;}}return null;}/// <summary>/// 自动计算贴图的 9宫 Border/// </summary>/// <param name="texture"></param>/// <param name="alphaThreshold">0-255</param>/// <returns></returns>public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3){int width = texture.width;int height = texture.height;Color32[] pixels = texture.GetPixels32();int minX = width;int minY = height;int maxX = 0;int maxY = 0;// 寻找不透明像素的最小和最大边界for (int y = 0; y < height; y++){for (int x = 0; x < width; x++){int pixelIndex = y * width + x;Color32 pixel = pixels[pixelIndex];if (pixel.a >= alphaThreshold){minX = Mathf.Min(minX, x);minY = Mathf.Min(minY, y);maxX = Mathf.Max(maxX, x);maxY = Mathf.Max(maxY, y);}}}// 计算最优的borderSizeint borderSizeX = (maxX - minX) / 3;int borderSizeY = (maxY - minY) / 3;int borderSize = Mathf.Min(borderSizeX, borderSizeY);// 根据边界和Border Size计算Nine Slice Borderint left = minX + borderSize;int right = maxX - borderSize;int top = minY + borderSize;int bottom = maxY - borderSize;// 确保边界在纹理范围内left = Mathf.Clamp(left, 0, width - 1);right = Mathf.Clamp(right, 0, width - 1);top = Mathf.Clamp(top, 0, height - 1);bottom = Mathf.Clamp(bottom, 0, height - 1);return new Vector4(left, top, width - right, height - bottom);}/// <summary>/// 把PS的字体样式同步设置到UGUI Text/// </summary>/// <param name="txtLayer"></param>/// <param name="text"></param>public static void SetTextStyle(PsdLayerNode txtLayer, UnityEngine.UI.Text text){if (text == null) return;text.gameObject.SetActive(txtLayer != null);if (txtLayer != null && txtLayer.ParseTextLayerInfo(out var str, out var size, out var charSpace, out float lineSpace, out var col, out var style, out var tmpStyle, out var fName)){var tFont = FindFontAsset(fName);if (tFont != null) text.font = tFont;text.text = str;text.fontSize = size;text.fontStyle = style;text.color = col;text.lineSpacing = lineSpace;}}/// <summary>/// 把PS的字体样式同步设置到TextMeshProUGUI/// </summary>/// <param name="txtLayer"></param>/// <param name="text"></param>public static void SetTextStyle(PsdLayerNode txtLayer, TextMeshProUGUI text){if (txtLayer != null && txtLayer.ParseTextLayerInfo(out var str, out var size, out var charSpace, out float lineSpace, out var col, out var style, out var tmpStyle, out var fName)){var tFont = FindTMPFontAsset(fName);if (tFont != null) text.font = tFont;text.text = str;text.fontSize = size;text.fontStyle = tmpStyle;text.color = col;text.characterSpacing = charSpace;text.lineSpacing = lineSpace;}}/// <summary>/// 根据字体名查找TMP_FontAsset/// </summary>/// <param name="fontName"></param>/// <returns></returns>public static TMP_FontAsset FindTMPFontAsset(string fontName){var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");foreach (var guid in fontGuids){var fontPath = AssetDatabase.GUIDToAssetPath(guid);var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(fontPath);if (font != null && font.faceInfo.familyName == fontName){return font;}}return null;}/// <summary>/// 根据字体名查找Font Asset/// </summary>/// <param name="fontName"></param>/// <returns></returns>public static UnityEngine.Font FindFontAsset(string fontName){var fontGuids = AssetDatabase.FindAssets("t:font");foreach (var guid in fontGuids){var fontPath = AssetDatabase.GUIDToAssetPath(guid);var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;if (font != null && font.fontTTFName == fontName){return AssetDatabase.LoadAssetAtPath<UnityEngine.Font>(fontPath);}}return null;}internal static UnityEngine.Color LayerNode2Color(PsdLayerNode fillColor, Color defaultColor){if (fillColor != null && fillColor.BindPsdLayer is FillLayer fillLayer){var layerColor = fillLayer.GetPixel(fillLayer.Width / 2, fillLayer.Height / 2);return new UnityEngine.Color(layerColor.R, layerColor.G, layerColor.B, fillLayer.Opacity) / (float)255;}return defaultColor;}/// <summary>/// 导出UI设计师使用规则文档/// </summary>/// <exception cref="NotImplementedException"></exception>internal void ExportReadmeDoc(){var exportDir = EditorUtility.SaveFolderPanel("选择文档导出路径", Application.dataPath, null);if (string.IsNullOrWhiteSpace(exportDir) || !Directory.Exists(exportDir)){return;}var docFile = UtilityBuiltin.ResPath.GetCombinePath(exportDir, "Psd2UGUI设计师使用文档.doc");var strBuilder = new StringBuilder();strBuilder.AppendLine("使用说明:");strBuilder.AppendLine(this.readmeDoc);strBuilder.AppendLine(Environment.NewLine + Environment.NewLine);strBuilder.AppendLine("UI类型标识: 图层/组命名以'.类型'结尾");strBuilder.AppendLine("UI类型标识列表:");foreach (var rule in rules){if (rule.UIType == GUIType.Null) continue;strBuilder.AppendLine($"{rule.UIType}: {rule.Comment}");strBuilder.Append("类型标识: ");foreach (var tag in rule.TypeMatches){strBuilder.Append($".{tag}, ");}strBuilder.AppendLine();strBuilder.AppendLine();}try{File.WriteAllText(docFile, strBuilder.ToString(), System.Text.Encoding.UTF8);EditorUtility.RevealInFinder(docFile);}catch (Exception e){Debug.LogException(e);}}}
}
#endif

 三、PS脚本编写,一键转换特效图层/文本图层为智能对象

为了辅助UI设计师,避免手动转换智能对象会有遗漏,设计师交付PSD文件前需要执行自动化脚本,把特效图层/字体转为智能对象,这样即使不同设备字库丢失也能保持字体原本样式。PS脚本是用js语言编写,没有代码提示是最大的障碍。好在没有复杂逻辑,只是遍历当前打开的psd文档图层,判断图层是否带有特效或是否为文本图层,把符合条件的图层转换为智能对象:

// 判断图层是否包含特效
function hasLayerEffect(layer) {app.activeDocument.activeLayer = layer;var hasEffect = false;try {var ref = new ActionReference();var keyLayerEffects = app.charIDToTypeID( 'Lefx' );ref.putProperty( app.charIDToTypeID( 'Prpr' ), keyLayerEffects );ref.putEnumerated( app.charIDToTypeID( 'Lyr ' ), app.charIDToTypeID( 'Ordn' ), app.charIDToTypeID( 'Trgt' ) );var desc = executeActionGet( ref );if ( desc.hasKey( keyLayerEffects ) ) {hasEffect = true;}}catch(e) {hasEffect = false;}return hasEffect;
}function convertLayersToSmartObjects(layers) 
{for (var i = layers.length - 1; i >= 0; i--) {var layer = layers[i];if (layer.typename === "LayerSet"){convertLayersToSmartObjects(layer.layers); // Recursively convert layers in layer sets} else{if (hasLayerEffect(layer)){if(layer.kind === LayerKind.TEXT)convertToSmartObject(layer); // Convert layers with layer effects to smart objectselse layer.rasterize(RasterizeType.SHAPE);}}}
}
// 把图层转换为智能对象,功能等同右键图层->转为智能对象
function convertToSmartObject(layer) {app.activeDocument.activeLayer = layer;// 创建一个新的智能对象var idnewPlacedLayer = stringIDToTypeID("newPlacedLayer");executeAction(idnewPlacedLayer, undefined, DialogModes.NO);}
// 导出处理后的PSD文件
function exportPSD() {var doc = app.activeDocument;var savePath = Folder.selectDialog("选择psd导出路径");if (savePath != null) {var saveOptions = new PhotoshopSaveOptions();saveOptions.embedColorProfile = true;saveOptions.alphaChannels = true;var saveFile = new File(savePath + "/" + doc.name);doc.saveAs(saveFile, saveOptions, true, Extension.LOWERCASE);alert("PSD已成功导出!");}
}
function convertAndExport(){convertLayersToSmartObjects (app.activeDocument.layers);//exportPSD();
}
app.activeDocument.suspendHistory("Convert2SmartObject", "convertAndExport();");
//~ convertLayersToSmartObjects (app.activeDocument.layers);

四、Psd转UGUI编辑器

1. Unity中右键PSD文件把PS图层转换成节点,每个节点绑定一个对应图层。

2. 解析UI设计师为UI标记的类型,自动标识图层是否需要导出,自动绑定UI子元素。

3. 查漏补缺,对于没有标记类型并且没有正确识别绑定的UI元素进行手动选择类型。

 编辑器根节点提供各项持久化保存设置,并且支持自动压缩图片。压缩方法可参考之前写过的压缩工具:【Unity编辑器扩展】包体优化神器,图片压缩,批量生成图集/图集变体,动画压缩_unity 图片压缩_TopGames的博客-CSDN博客

4. 解析psd图层:重新解析psd为节点树状图。

5. 导出Images:把编辑器下勾选的图层节点导出为图片资源。

6. 生成UIForm:把当前的节点树解析生成为UI界面预制体。

Psd2UIForm编辑器代码:

#if UNITY_EDITOR
using Aspose.PSD.FileFormats.Psd;
using Aspose.PSD.FileFormats.Psd.Layers;
using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;
using Aspose.PSD.ImageLoadOptions;
using GameFramework;
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityGameFramework.Runtime;namespace UGF.EditorTools.Psd2UGUI
{#region Crack[HarmonyPatch(typeof(System.Xml.XmlElement), nameof(System.Xml.XmlElement.InnerText), MethodType.Getter)]class CrackAspose{static void Postfix(ref string __result){if (__result == "20220516"){__result = "20500516";}//else if (__result == "20210827")//{//    __result = "20250827";//}}}#endregion[CustomEditor(typeof(Psd2UIFormConverter))]public class Psd2UIFormConverterInspector : UnityEditor.Editor{Psd2UIFormConverter targetLogic;GUIContent parsePsd2NodesBt;GUIContent exportUISpritesBt;GUIContent generateUIFormBt;GUILayoutOption btHeight;private void OnEnable(){btHeight = GUILayout.Height(30);targetLogic = target as Psd2UIFormConverter;parsePsd2NodesBt = new GUIContent("解析psd图层", "把psd图层解析为可编辑节点树");exportUISpritesBt = new GUIContent("导出Images", "导出勾选的psd图层为碎图");generateUIFormBt = new GUIContent("生成UIForm", "根据解析后的节点树生成UIForm Prefab");if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir)){Debug.LogWarning($"UIForm输出路径为空!");}}private void OnDisable(){Psd2UIFormSettings.Save();}public override void OnInspectorGUI(){EditorGUILayout.BeginVertical("box");{EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("自动压缩图片:", GUILayout.Width(150));Psd2UIFormSettings.Instance.CompressImage = EditorGUILayout.Toggle(Psd2UIFormSettings.Instance.CompressImage);EditorGUILayout.EndHorizontal();}EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("UI图片导出路径:", GUILayout.Width(150));Psd2UIFormSettings.Instance.UIImagesOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIImagesOutputDir);if (GUILayout.Button("选择路径", GUILayout.Width(80))){var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIImagesOutputDir, null);if (!string.IsNullOrWhiteSpace(retPath)){if (!retPath.StartsWith("Assets/")){retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);}Psd2UIFormSettings.Instance.UIImagesOutputDir = retPath;Psd2UIFormSettings.Save();}GUIUtility.ExitGUI();}EditorGUILayout.EndHorizontal();}EditorGUILayout.BeginHorizontal();{Psd2UIFormSettings.Instance.UseUIFormOutputDir = EditorGUILayout.ToggleLeft("使用UIForm导出路径:", Psd2UIFormSettings.Instance.UseUIFormOutputDir, GUILayout.Width(150));EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.UseUIFormOutputDir);{Psd2UIFormSettings.Instance.UIFormOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormOutputDir);if (GUILayout.Button("选择路径", GUILayout.Width(80))){var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormOutputDir, null);if (!string.IsNullOrWhiteSpace(retPath)){if (!retPath.StartsWith("Assets/")){retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);}Psd2UIFormSettings.Instance.UIFormOutputDir = retPath;Psd2UIFormSettings.Save();}GUIUtility.ExitGUI();}EditorGUI.EndDisabledGroup();}EditorGUILayout.EndHorizontal();}//EditorGUILayout.BeginHorizontal();//{//    Psd2UIFormSettings.Instance.AutoCreateUIFormScript = EditorGUILayout.ToggleLeft("生成UIForm代码:", Psd2UIFormSettings.Instance.AutoCreateUIFormScript, GUILayout.Width(150));//    EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.AutoCreateUIFormScript);//    {//        Psd2UIFormSettings.Instance.UIFormScriptOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormScriptOutputDir);//        if (GUILayout.Button("选择路径", GUILayout.Width(80)))//        {//            var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormScriptOutputDir, null);//            if (!string.IsNullOrWhiteSpace(retPath))//            {//                if (!retPath.StartsWith("Assets/"))//                {//                    retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);//                }//                Psd2UIFormSettings.Instance.UIFormScriptOutputDir = retPath;//                Psd2UIFormSettings.Save();//            }//            GUIUtility.ExitGUI();//        }//        EditorGUI.EndDisabledGroup();//    }//    EditorGUILayout.EndHorizontal();//}EditorGUILayout.EndVertical();}EditorGUILayout.BeginHorizontal();{if (GUILayout.Button(parsePsd2NodesBt, btHeight)){Psd2UIFormConverter.ParsePsd2LayerPrefab(targetLogic.PsdAssetName, targetLogic);}if (GUILayout.Button(exportUISpritesBt, btHeight)){targetLogic.ExportSprites();}EditorGUILayout.EndHorizontal();}if (GUILayout.Button(generateUIFormBt, btHeight)){targetLogic.GenerateUIForm();}base.OnInspectorGUI();}public override bool HasPreviewGUI(){return targetLogic.BindPsdAsset != null;}public override void OnPreviewGUI(Rect r, GUIStyle background){GUI.DrawTexture(r, targetLogic.BindPsdAsset.texture, ScaleMode.ScaleToFit);//base.OnPreviewGUI(r, background);}}/// <summary>/// Psd文件转成UIForm prefab/// </summary>[ExecuteInEditMode][RequireComponent(typeof(SpriteRenderer))]public class Psd2UIFormConverter : MonoBehaviour{const string RecordLayerOperation = "Change Export Image";public static Psd2UIFormConverter Instance { get; private set; }[ReadOnlyField][SerializeField] public string psdAssetChangeTime;//文件修改时间标识[Tooltip("UIForm名字")][SerializeField] private string uiFormName;[Tooltip("关联的psd文件")][SerializeField] private UnityEngine.Sprite psdAsset;[Header("Debug:")][SerializeField] bool drawLayerRectGizmos = true;[SerializeField] UnityEngine.Color drawLayerRectGizmosColor = UnityEngine.Color.green;private PsdImage psdInstance;//psd文件解析实例private GUIStyle uiTypeLabelStyle;public string PsdAssetName => psdAsset != null ? AssetDatabase.GetAssetPath(psdAsset) : null;public UnityEngine.Sprite BindPsdAsset => psdAsset;public Vector2Int UIFormCanvasSize { get; private set; } = new Vector2Int(750, 1334);private void OnEnable(){Instance = this;uiTypeLabelStyle = new GUIStyle();uiTypeLabelStyle.fontSize = 13;uiTypeLabelStyle.fontStyle = UnityEngine.FontStyle.BoldAndItalic;UnityEngine.ColorUtility.TryParseHtmlString("#7ED994", out var color);uiTypeLabelStyle.normal.textColor = color;EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;if (psdInstance == null && !string.IsNullOrWhiteSpace(PsdAssetName)){RefreshNodesBindLayer();}}private void Start(){if (this.CheckPsdAssetHasChanged()){if (EditorUtility.DisplayDialog("PSD -> UIForm", $"{gameObject.name}关联的psd文件[{this.PsdAssetName}]已改变,是否重新解析节点树?", "是", "否")){if (Psd2UIFormConverter.ParsePsd2LayerPrefab(this.PsdAssetName, this)){RefreshNodesBindLayer();}}}else{RefreshNodesBindLayer();}}private void OnDrawGizmos(){if (drawLayerRectGizmos){var nodes = this.GetComponentsInChildren<PsdLayerNode>();Gizmos.color = drawLayerRectGizmosColor;foreach (var item in nodes){if (item.NeedExportImage()){Gizmos.DrawWireCube(item.LayerRect.position * 0.01f, item.LayerRect.size * 0.01f);}}}}private void OnHierarchyGUI(int instanceID, Rect selectionRect){if (Event.current == null) return;var node = EditorUtility.InstanceIDToObject(instanceID) as GameObject;if (node == null || node == this.gameObject) return;if (!node.TryGetComponent<PsdLayerNode>(out var layer)) return;Rect tmpRect = selectionRect;tmpRect.x = 35;tmpRect.width = 10;Undo.RecordObject(layer, RecordLayerOperation);EditorGUI.BeginChangeCheck();{layer.markToExport = EditorGUI.Toggle(tmpRect, layer.markToExport);if (EditorGUI.EndChangeCheck()){if (Selection.gameObjects.Length > 1) SetExportImageTg(Selection.gameObjects, layer.markToExport);EditorUtility.SetDirty(layer);}}tmpRect.width = Mathf.Clamp(selectionRect.xMax * 0.2f, 100, 200);tmpRect.x = selectionRect.xMax - tmpRect.width;//EditorGUI.LabelField(tmpRect, layer.UIType.ToString(), uiTypeLabelStyle);if (EditorGUI.DropdownButton(tmpRect, new GUIContent(layer.UIType.ToString()), FocusType.Passive)){var dropdownMenu = PopEnumMenu<GUIType>(layer.UIType, selectUIType =>{layer.SetUIType(selectUIType);EditorUtility.SetDirty(layer);});dropdownMenu.ShowAsContext();}}private GenericMenu PopEnumMenu<T>(T currentValue, Action<T> onSelectEnum) where T : Enum{var names = Enum.GetValues(typeof(T));var dropdownMenu = new GenericMenu();foreach (T item in names){dropdownMenu.AddItem(new GUIContent(item.ToString()), item.Equals(currentValue), () => { onSelectEnum(item); });}return dropdownMenu;}/// <summary>/// 批量勾选导出图片/// </summary>/// <param name="selects"></param>/// <param name="exportImg"></param>private void SetExportImageTg(GameObject[] selects, bool exportImg){var selectLayerNodes = selects.Where(item => item?.GetComponent<PsdLayerNode>() != null).ToArray();foreach (var layer in selectLayerNodes){layer.GetComponent<PsdLayerNode>().markToExport = exportImg;}}private void OnDestroy(){EditorApplication.hierarchyWindowItemOnGUI -= OnHierarchyGUI;if (this.psdInstance != null && !psdInstance.Disposed){psdInstance.Dispose();}}private void RefreshNodesBindLayer(){if (psdInstance == null || psdInstance.Disposed){if (!File.Exists(PsdAssetName)){Debug.LogError($"刷新节点绑定图层失败! psd文件不存在");return;}var psdOpts = new PsdLoadOptions(){LoadEffectsResource = true,ReadOnlyMode = false,};psdInstance = Aspose.PSD.Image.Load(PsdAssetName, psdOpts) as PsdImage;UIFormCanvasSize.Set(psdInstance.Size.Width, psdInstance.Size.Height);}var layers = GetComponentsInChildren<PsdLayerNode>(true);foreach (var layer in layers){layer.InitPsdLayers(psdInstance);}var spRender = gameObject.GetOrAddComponent<SpriteRenderer>();spRender.sprite = this.psdAsset;}#regionconst string AsposeLicenseKey = "此处为Aspose.PSD证书";static bool licenseInitiated = false;[InitializeOnLoadMethod]static void InitAsposeLicense(){if (licenseInitiated) return;var harmonyHook = new Harmony("Crack.Aspose");harmonyHook.PatchAll();new Aspose.PSD.License().SetLicense(new MemoryStream(Convert.FromBase64String(AsposeLicenseKey)));licenseInitiated = true;harmonyHook.UnpatchAll();//GetAllLayerType();}static void GetAllLayerType(){var psdLib = Utility.Assembly.GetAssemblies().FirstOrDefault(item => item.GetName().Name == "Aspose.PSD");var layers = psdLib.GetTypes().Where(tp => tp.IsSubclassOf(typeof(Layer)) && !tp.IsAbstract);string layerEnumNames = "";foreach (var item in layers){layerEnumNames += $"{item.Name},\n";}Debug.Log(layerEnumNames);}#endregion Aspose License[MenuItem("Assets/GF Editor Tool/Psd2UIForm Editor", priority = 0)]static void Psd2UIFormPrefabMenu(){if (Selection.activeObject == null) return;var assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);if (Path.GetExtension(assetPath).ToLower().CompareTo(".psd") != 0){Debug.LogWarning($"选择的文件({assetPath})不是psd格式, 工具只支持psd转换为UIForm");return;}string psdLayerPrefab = GetPsdLayerPrefabPath(assetPath);if (!File.Exists(psdLayerPrefab)){if (ParsePsd2LayerPrefab(assetPath)){OpenPsdLayerEditor(psdLayerPrefab);}}else{OpenPsdLayerEditor(psdLayerPrefab);}}public bool CheckPsdAssetHasChanged(){if (psdAsset == null) return false;var fileTag = GetAssetChangeTag(PsdAssetName);return psdAssetChangeTime.CompareTo(fileTag) != 0;}public static string GetAssetChangeTag(string fileName){return new FileInfo(fileName).LastWriteTime.ToString("yyyyMMddHHmmss");}/// <summary>/// 打开psd图层信息prefab/// </summary>/// <param name="psdLayerPrefab"></param>public static void OpenPsdLayerEditor(string psdLayerPrefab){PrefabStageUtility.OpenPrefab(psdLayerPrefab);}/// <summary>/// 把Psd图层解析成节点prefab/// </summary>/// <param name="psdPath"></param>/// <returns></returns>public static bool ParsePsd2LayerPrefab(string psdFile, Psd2UIFormConverter instanceRoot = null){if (!File.Exists(psdFile)){Debug.LogError($"Error: Psd文件不存在:{psdFile}");return false;}var texImporter = AssetImporter.GetAtPath(psdFile) as TextureImporter;if (texImporter.textureType != TextureImporterType.Sprite){texImporter.textureType = TextureImporterType.Sprite;texImporter.mipmapEnabled = false;texImporter.alphaIsTransparency = true;texImporter.SaveAndReimport();}var prefabFile = GetPsdLayerPrefabPath(psdFile);var rootName = Path.GetFileNameWithoutExtension(prefabFile);bool needDestroyInstance = instanceRoot == null;if (instanceRoot != null){ParsePsdLayer2Root(psdFile, instanceRoot);instanceRoot.RefreshNodesBindLayer();return true;}else{Psd2UIFormConverter rootLayer = CreatePsdLayerRoot(rootName);rootLayer.SetPsdAsset(psdFile);ParsePsdLayer2Root(psdFile, rootLayer);PrefabUtility.SaveAsPrefabAsset(rootLayer.gameObject, prefabFile, out bool savePrefabSuccess);if (needDestroyInstance) GameObject.DestroyImmediate(rootLayer.gameObject);AssetDatabase.Refresh();if (savePrefabSuccess && AssetDatabase.GUIDFromAssetPath(StageUtility.GetCurrentStage().assetPath) != AssetDatabase.GUIDFromAssetPath(prefabFile)){PrefabStageUtility.OpenPrefab(prefabFile);}return savePrefabSuccess;}}private static void ParsePsdLayer2Root(string psdFile, Psd2UIFormConverter converter){//清空已有节点重新解析for (int i = converter.transform.childCount - 1; i >= 0; i--){GameObject.DestroyImmediate(converter.transform.GetChild(i).gameObject);}var psdOpts = new PsdLoadOptions(){LoadEffectsResource = true,ReadOnlyMode = false};using (var psd = Aspose.PSD.Image.Load(psdFile, psdOpts) as PsdImage){List<GameObject> layerNodes = new List<GameObject> { converter.gameObject };for (int i = 0; i < psd.Layers.Length; i++){var layer = psd.Layers[i];var curLayerType = layer.GetLayerType();if (curLayerType == PsdLayerType.SectionDividerLayer){var layerGroup = (layer as SectionDividerLayer).GetRelatedLayerGroup();var layerGroupIdx = ArrayUtility.IndexOf(psd.Layers, layerGroup);var layerGropNode = CreatePsdLayerNode(layerGroup, layerGroupIdx);layerNodes.Add(layerGropNode.gameObject);}else if (curLayerType == PsdLayerType.LayerGroup){var lastLayerNode = layerNodes.Last();layerNodes.Remove(lastLayerNode);if (layerNodes.Count > 0){var parentLayerNode = layerNodes.Last();lastLayerNode.transform.SetParent(parentLayerNode.transform);}}else{var newLayerNode = CreatePsdLayerNode(layer, i);newLayerNode.transform.SetParent(layerNodes.Last().transform);newLayerNode.transform.localPosition = Vector3.zero;}}}converter.psdAssetChangeTime = GetAssetChangeTag(psdFile);var childrenNodes = converter.GetComponentsInChildren<PsdLayerNode>(true);foreach (var item in childrenNodes){item.RefreshUIHelper(false);}EditorUtility.SetDirty(converter.gameObject);}private void SetPsdAsset(string psdFile){this.psdAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Sprite>(psdFile);if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIImagesOutputDir)){Psd2UIFormSettings.Instance.UIImagesOutputDir = Path.GetDirectoryName(psdFile);}if (string.IsNullOrWhiteSpace(this.uiFormName)){this.uiFormName = this.psdAsset.name;}}/// <summary>/// 获取解析好的psd layers文件/// </summary>/// <param name="psd"></param>/// <returns></returns>public static string GetPsdLayerPrefabPath(string psd){return UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(psd), Path.GetFileNameWithoutExtension(psd) + "_psd_layers_parsed.prefab");}private static Psd2UIFormConverter CreatePsdLayerRoot(string rootName){var node = new GameObject(rootName);node.gameObject.tag = "EditorOnly";var layerRoot = node.AddComponent<Psd2UIFormConverter>();return layerRoot;}private static PsdLayerNode CreatePsdLayerNode(Layer layer, int bindLayerIdx){string nodeName = layer.Name;if (string.IsNullOrWhiteSpace(nodeName)){nodeName = $"PsdLayer-{bindLayerIdx}";}else{if (Path.HasExtension(layer.Name)){nodeName = Path.GetFileNameWithoutExtension(layer.Name);}}var node = new GameObject(nodeName);node.gameObject.tag = "EditorOnly";var layerNode = node.AddComponent<PsdLayerNode>();layerNode.BindPsdLayerIndex = bindLayerIdx;InitLayerNodeData(layerNode, layer);return layerNode;}/// <summary>/// 根据psd图层信息解析并初始化图层UI类型、是否导出等信息/// </summary>/// <param name="layerNode"></param>/// <param name="layer"></param>private static void InitLayerNodeData(PsdLayerNode layerNode, Layer layer){if (layer == null || layer.Disposed) return;var layerTp = layer.GetLayerType();layerNode.BindPsdLayer = layer;if (UGUIParser.Instance.TryParse(layerNode, out var initRule)){layerNode.SetUIType(initRule.UIType, false);}layerNode.markToExport = layerTp != PsdLayerType.LayerGroup && !(layerTp == PsdLayerType.TextLayer && layerNode.UIType.ToString().EndsWith("Text") && layerNode.UIType != GUIType.FillColor);layerNode.gameObject.SetActive(layer.IsVisible);}/// <summary>/// 导出psd图层为Sprites碎图/// </summary>/// <param name="psdAssetName"></param>internal void ExportSprites(){//var pngOpts = new PngOptions()//{//    ColorType = Aspose.PSD.FileFormats.Png.PngColorType.Truecolor//};//this.psdInstance.Save("Assets/AAAGame/Sprites/UI/Preview.png", pngOpts);//return;var exportLayers = this.GetComponentsInChildren<PsdLayerNode>().Where(node => node.NeedExportImage());var exportDir = GetUIFormImagesOutputDir();if (!Directory.Exists(exportDir)){Directory.CreateDirectory(exportDir);}int exportIdx = 0;int totalCount = exportLayers.Count();foreach (var layer in exportLayers){var assetName = layer.ExportImageAsset();if (assetName == null){Debug.LogWarning($"导出图层[name:{layer.name}, layerIdx:{layer.BindPsdLayerIndex}]图片失败!");}++exportIdx;EditorUtility.DisplayProgressBar($"导出进度({exportIdx}/{totalCount})", $"导出UI图片:{assetName}", exportIdx / (float)totalCount);}EditorUtility.ClearProgressBar();AssetDatabase.Refresh();}/// <summary>/// 根据解析后的节点树生成UIForm Prefab/// </summary>internal void GenerateUIForm(){if (Psd2UIFormSettings.Instance.UseUIFormOutputDir && string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir)){Debug.LogError($"生成UIForm失败! UIForm导出路径为空:{Psd2UIFormSettings.Instance.UIFormOutputDir}");return;}if (Psd2UIFormSettings.Instance.UseUIFormOutputDir){ExportUIPrefab(Psd2UIFormSettings.Instance.UIFormOutputDir);}else{string lastSaveDir = string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.LastUIFormOutputDir) ? "Assets" : Psd2UIFormSettings.Instance.LastUIFormOutputDir;string selectDir = EditorUtility.SaveFolderPanel("保存目录", lastSaveDir, null);if (!string.IsNullOrWhiteSpace(selectDir)){if (!selectDir.StartsWith("Assets/"))selectDir = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, selectDir);Psd2UIFormSettings.Instance.LastUIFormOutputDir = selectDir;ExportUIPrefab(selectDir);}}}private bool ExportUIPrefab(string outputDir){if (!string.IsNullOrWhiteSpace(outputDir)){if (!Directory.Exists(outputDir)){try{Directory.CreateDirectory(outputDir);AssetDatabase.Refresh();}catch (Exception err){Debug.LogError($"导出UI prefab失败:{err.Message}");return false;}}}if (string.IsNullOrWhiteSpace(uiFormName)){Debug.LogError("导出UI Prefab失败: UI Form Name为空, 请填写UI Form Name.");return false;}var prefabName = UtilityBuiltin.ResPath.GetCombinePath(outputDir, $"{uiFormName}.prefab");if (File.Exists(prefabName)){if (!EditorUtility.DisplayDialog("警告", $"prefab文件已存在, 是否覆盖:{prefabName}", "覆盖生成", "取消生成")){return false;}}var uiHelpers = GetAvailableUIHelpers();if (uiHelpers == null || uiHelpers.Length < 1){return false;}var uiFormRoot = GameObject.Instantiate(UGUIParser.Instance.UIFormTemplate, Vector3.zero, Quaternion.identity);uiFormRoot.name = uiFormName;int curIdx = 0;int totalCount = uiHelpers.Length;foreach (var uiHelper in uiHelpers){EditorUtility.DisplayProgressBar($"生成UIFrom:({curIdx++}/{totalCount})", $"正在生成UI元素:{uiHelper.name}", curIdx /(float)totalCount);var uiElement = uiHelper.CreateUI();if (uiElement == null) continue;var goPath = GetGameObjectInstanceIdPath(uiHelper.gameObject, out var goNames);var parentNode = GetOrCreateNodeByInstanceIdPath(uiFormRoot, goPath, goNames);uiElement.transform.SetParent(parentNode.transform, true);uiElement.transform.position += new Vector3(this.UIFormCanvasSize.x * 0.5f, this.UIFormCanvasSize.y * 0.5f, 0);}var uiStrKeys = uiFormRoot.GetComponentsInChildren<UIStringKey>(true);for (int i = uiStrKeys.Length - 1; i >= 0; i--){DestroyImmediate(uiStrKeys[i]);}var uiPrefab = PrefabUtility.SaveAsPrefabAsset(uiFormRoot, prefabName, out bool saveSuccess);if (saveSuccess){DestroyImmediate(uiFormRoot);Selection.activeGameObject = uiPrefab;}EditorUtility.ClearProgressBar();return true;}private GameObject GetOrCreateNodeByInstanceIdPath(GameObject uiFormRoot, string[] goPath, string[] goNames){GameObject result = uiFormRoot;if (goPath != null && goNames != null){for (int i = 0; i < goPath.Length; i++){var nodeId = goPath[i];var nodeName = goNames[i];GameObject targetNode = null;foreach (Transform child in result.transform){if (child.gameObject == result) continue;var idKey = child.GetComponent<UIStringKey>();if (idKey != null && nodeId == idKey.Key){targetNode = child.gameObject;break;}}if (targetNode == null){targetNode = new GameObject(nodeName);targetNode.transform.SetParent(result.transform, false);targetNode.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);var targetNodeKey = targetNode.GetOrAddComponent<UIStringKey>();targetNodeKey.Key = nodeId;}result = targetNode;}}return result;}private string[] GetGameObjectInstanceIdPath(GameObject go, out string[] names){names = null;if (go == null || go.transform.parent == null || go.transform.parent == this.transform) return null;var parentGo = go.transform.parent;string[] result = new string[1] { parentGo.gameObject.GetInstanceID().ToString() };names = new string[1] { parentGo.gameObject.name };while (parentGo.parent != null && parentGo.parent != this.transform){ArrayUtility.Insert(ref result, 0, parentGo.parent.gameObject.GetInstanceID().ToString());ArrayUtility.Insert(ref names, 0, parentGo.parent.gameObject.name);parentGo = parentGo.parent;}return result;}private UIHelperBase[] GetAvailableUIHelpers(){var uiHelpers = this.GetComponentsInChildren<UIHelperBase>();uiHelpers = uiHelpers.Where(ui => ui.LayerNode.IsMainUIType).ToArray();List<int> dependInstIds = new List<int>();foreach (var item in uiHelpers){foreach (var depend in item.GetDependencies()){int dependId = depend.gameObject.GetInstanceID();if (!dependInstIds.Contains(dependId)){dependInstIds.Add(dependId);}}}for (int i = uiHelpers.Length - 1; i >= 0; i--){var uiHelper = uiHelpers[i];if (dependInstIds.Contains(uiHelper.gameObject.GetInstanceID())){ArrayUtility.RemoveAt(ref uiHelpers, i);}}return uiHelpers;}/// <summary>/// 把图片设置为为Sprite或Texture类型/// </summary>/// <param name="dir"></param>public static void ConvertTexturesType(string[] texAssets, bool isImage = true){foreach (var item in texAssets){var texImporter = AssetImporter.GetAtPath(item) as TextureImporter;if (texImporter == null){Debug.LogError($"TextureImporter为空:{item}");continue;}if (isImage){texImporter.textureType = TextureImporterType.Sprite;texImporter.spriteImportMode = SpriteImportMode.Single;texImporter.alphaSource = TextureImporterAlphaSource.FromInput;texImporter.alphaIsTransparency = true;texImporter.mipmapEnabled = false;}else{texImporter.textureType = TextureImporterType.Default;texImporter.textureShape = TextureImporterShape.Texture2D;texImporter.alphaSource = TextureImporterAlphaSource.FromInput;texImporter.alphaIsTransparency = true;texImporter.mipmapEnabled = false;}texImporter.SaveAndReimport();}}/// <summary>/// 压缩图片文件/// </summary>/// <param name="asset">文件名(相对路径Assets)</param>/// <returns></returns>public static bool CompressImageFile(string asset){var assetPath = asset.StartsWith("Assets/") ? Path.GetFullPath(asset, Directory.GetParent(Application.dataPath).FullName) : asset;var compressTool = Utility.Assembly.GetType("UGF.EditorTools.CompressTool");if (compressTool == null) return false;var compressMethod = compressTool.GetMethod("CompressImageOffline", new Type[] { typeof(string), typeof(string) });if (compressMethod == null) return false;return (bool)compressMethod.Invoke(null, new object[] { assetPath, assetPath });}/// <summary>/// 获取UIForm对应的图片导出目录/// </summary>/// <returns></returns>public string GetUIFormImagesOutputDir(){return UtilityBuiltin.ResPath.GetCombinePath(Psd2UIFormSettings.Instance.UIImagesOutputDir, uiFormName);}public SmartObjectLayer ConvertToSmartObjectLayer(Layer layer){var smartObj = psdInstance.SmartObjectProvider.ConvertToSmartObject(new Layer[] { layer });return smartObj;}}
}
#endif

7. 图层节点编辑器扩展,提供导出图片按钮以便单独导出选择图层,UI类型切换时自动添加对应的Helper解析器并自动绑定子UI

#if UNITY_EDITOR
using UnityEngine;
using Aspose.PSD.FileFormats.Psd.Layers;
using Aspose.PSD.ImageOptions;
using UnityEditor;
using System.IO;
using System.Linq;
using Aspose.PSD.FileFormats.Psd;
using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;
using GameFramework;namespace UGF.EditorTools.Psd2UGUI
{[CanEditMultipleObjects][CustomEditor(typeof(PsdLayerNode))]public class PsdLayerNodeInspector : Editor{PsdLayerNode targetLogic;private void OnEnable(){targetLogic = target as PsdLayerNode;targetLogic.RefreshLayerTexture();}public override void OnInspectorGUI(){serializedObject.Update();base.OnInspectorGUI();EditorGUI.BeginChangeCheck();{targetLogic.UIType = (GUIType)EditorGUILayout.EnumPopup("UI Type", targetLogic.UIType);if (EditorGUI.EndChangeCheck()){targetLogic.SetUIType(targetLogic.UIType);}}EditorGUILayout.BeginHorizontal();{if (GUILayout.Button("导出图片")){foreach (var item in targets){if (item == null) continue;(item as PsdLayerNode)?.ExportImageAsset();}}EditorGUILayout.EndHorizontal();}serializedObject.ApplyModifiedProperties();}public override bool HasPreviewGUI(){var layerNode = (target as PsdLayerNode);return layerNode != null && layerNode.PreviewTexture != null;}public override void OnPreviewGUI(Rect r, GUIStyle background){var layerNode = (target as PsdLayerNode);GUI.DrawTexture(r, layerNode.PreviewTexture, ScaleMode.ScaleToFit);//base.OnPreviewGUI(r, background);}public override string GetInfoString(){var layerNode = (target as PsdLayerNode);return layerNode.LayerInfo;}}[ExecuteInEditMode][DisallowMultipleComponent]public class PsdLayerNode : MonoBehaviour{[ReadOnlyField] public int BindPsdLayerIndex = -1;[ReadOnlyField][SerializeField] PsdLayerType mLayerType = PsdLayerType.Unknown;[SerializeField] public bool markToExport;[HideInInspector] public GUIType UIType;public Texture2D PreviewTexture { get; private set; }public string LayerInfo { get; private set; }public Rect LayerRect { get; private set; }public PsdLayerType LayerType { get => mLayerType; }public bool IsMainUIType => UGUIParser.IsMainUIType(UIType);/// <summary>/// 绑定的psd图层/// </summary>private Layer mBindPsdLayer;public Layer BindPsdLayer{get => mBindPsdLayer;set{mBindPsdLayer = value;mLayerType = mBindPsdLayer.GetLayerType();//if (IsTextLayer(out var txtLayer) && !txtLayer.TextBoundBox.IsEmpty)//{//    LayerRect = AsposePsdExtension.PsdRect2UnityRect(txtLayer.TextBoundBox, Psd2UIFormConverter.Instance.UIFormCanvasSize);//}//else{LayerRect = mBindPsdLayer.GetLayerRect();}LayerInfo = $"{LayerRect}";}}private void OnDestroy(){if (PreviewTexture != null){DestroyImmediate(PreviewTexture);}}public void SetUIType(GUIType uiType, bool triggerParseFunc = true){this.UIType = uiType;RemoveUIHelper();if (triggerParseFunc){RefreshUIHelper(true);}}public void RefreshUIHelper(bool refreshParent = false){if (UIType == GUIType.Null) return;var uiHelperTp = UGUIParser.Instance.GetHelperType(UIType);if (uiHelperTp != null){var helper = gameObject.GetOrAddComponent(uiHelperTp) as UIHelperBase;helper.ParseAndAttachUIElements();}if (refreshParent){var parentHelper = transform.parent?.GetComponent<UIHelperBase>();parentHelper?.ParseAndAttachUIElements();}EditorUtility.SetDirty(this);}private void RemoveUIHelper(){var uiHelpers = this.GetComponents<UIHelperBase>();if (uiHelpers != null){foreach (var uiHelper in uiHelpers){DestroyImmediate(uiHelper);}}EditorUtility.SetDirty(this);}/// <summary>/// 是否需要导出此图层/// </summary>/// <returns></returns>public bool NeedExportImage(){return gameObject.activeSelf && markToExport;}/// <summary>/// 导出图片/// </summary>/// <param name="forceSpriteType">强制贴图类型为Sprite</param>/// <returns></returns>public string ExportImageAsset(bool forceSpriteType = false){string assetName = null;if (this.RefreshLayerTexture()){var bytes = PreviewTexture.EncodeToPNG();var imgName = Utility.Text.Format("{0}_{1}", string.IsNullOrWhiteSpace(name) ? UIType : name, BindPsdLayerIndex);var exportDir = Psd2UIFormConverter.Instance.GetUIFormImagesOutputDir();if (!Directory.Exists(exportDir)){try{Directory.CreateDirectory(exportDir);AssetDatabase.Refresh();}catch (System.Exception){return null;}}var imgFileName = UtilityBuiltin.ResPath.GetCombinePath(exportDir, imgName + ".png");File.WriteAllBytes(imgFileName, bytes);if (Psd2UIFormSettings.Instance.CompressImage){bool compressResult = Psd2UIFormConverter.CompressImageFile(imgFileName);if (compressResult){Debug.Log($"成功压缩图片:{imgFileName}");}else{Debug.LogWarning($"压缩图片失败:{imgFileName}");}}assetName = imgFileName;bool isImage = !(this.UIType == GUIType.FillColor || this.UIType == GUIType.RawImage);AssetDatabase.Refresh();Psd2UIFormConverter.ConvertTexturesType(new string[] { imgFileName }, isImage || forceSpriteType);}return assetName;}public bool RefreshLayerTexture(bool forceRefresh = false){if (!forceRefresh && PreviewTexture != null){return true;}if (BindPsdLayer == null || BindPsdLayer.Disposed) return false;var pngOpt = new PngOptions{ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha};if (BindPsdLayer.CanSave(pngOpt)){if (PreviewTexture != null){DestroyImmediate(PreviewTexture);}PreviewTexture = this.ConvertPsdLayer2Texture2D();}return PreviewTexture != null;}/// <summary>/// 把psd图层转成Texture2D/// </summary>/// <param name="psdLayer"></param>/// <returns>Texture2D</returns>public Texture2D ConvertPsdLayer2Texture2D(){if (BindPsdLayer == null || BindPsdLayer.Disposed) return null;MemoryStream ms = new MemoryStream();var pngOpt = new Aspose.PSD.ImageOptions.PngOptions(){ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha,FullFrame = true};if (BindPsdLayer.Opacity >= 255 || LayerType == PsdLayerType.LayerGroup){BindPsdLayer.Save(ms, pngOpt);}else{var smartLayer = Psd2UIFormConverter.Instance.ConvertToSmartObjectLayer(BindPsdLayer);smartLayer.Save(ms, pngOpt);}//var bitmap = BindPsdLayer.ToBitmap();//bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);var buffer = new byte[ms.Length];ms.Position = 0;ms.Read(buffer, 0, buffer.Length);Texture2D texture = new Texture2D(BindPsdLayer.Width, BindPsdLayer.Height);texture.alphaIsTransparency = true;texture.LoadImage(buffer);texture.Apply();ms.Dispose();return texture;}/// <summary>/// 从第一层子节点按类型查找LayerNode/// </summary>/// <param name="uiTp"></param>/// <returns></returns>public PsdLayerNode FindSubLayerNode(GUIType uiTp){for (int i = 0; i < transform.childCount; i++){var child = transform.GetChild(i)?.GetComponent<PsdLayerNode>();if (child != null && child.UIType == uiTp) return child;}return null;}/// <summary>/// 依次查找给定多个类型,返回最先找到的类型/// </summary>/// <param name="uiTps"></param>/// <returns></returns>public PsdLayerNode FindSubLayerNode(params GUIType[] uiTps){foreach (var tp in uiTps){var result = FindSubLayerNode(tp);if (result != null) return result;}return null;}public PsdLayerNode FindLayerNodeInChildren(GUIType uiTp){var layers = GetComponentsInChildren<PsdLayerNode>(true);if (layers != null && layers.Length > 0){return layers.FirstOrDefault(layer => layer.UIType == uiTp);}return null;}/// <summary>/// 判断该图层是否为文本图层/// </summary>/// <param name="layer"></param>/// <returns></returns>public bool IsTextLayer(out TextLayer layer){layer = null;if (BindPsdLayer == null) return false;if (BindPsdLayer is SmartObjectLayer smartLayer){layer = smartLayer.GetSmartObjectInnerTextLayer() as TextLayer;return layer != null;}else if (BindPsdLayer is TextLayer txtLayer){layer = txtLayer;return layer != null;}return false;}internal void InitPsdLayers(PsdImage psdInstance){BindPsdLayer = psdInstance.Layers[BindPsdLayerIndex];}internal bool ParseTextLayerInfo(out string text, out int fontSize, out float characterSpace, out float lineSpace, out Color fontColor, out UnityEngine.FontStyle fontStyle, out TMPro.FontStyles tmpFontStyle, out string fontName){text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;if (IsTextLayer(out var txtLayer)){text = txtLayer.Text;fontSize = (int)txtLayer.Font.Size;fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic)){fontStyle = UnityEngine.FontStyle.BoldAndItalic;}else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold)){fontStyle = UnityEngine.FontStyle.Bold;}else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic)){fontStyle = UnityEngine.FontStyle.Italic;}else{fontStyle = UnityEngine.FontStyle.Normal;}if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic)){tmpFontStyle |= TMPro.FontStyles.Italic;}if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold)){tmpFontStyle |= TMPro.FontStyles.Bold;}if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline)){tmpFontStyle |= TMPro.FontStyles.Underline;}if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout)){tmpFontStyle |= TMPro.FontStyles.Strikethrough;}fontName = txtLayer.Font.Name;if (txtLayer.TextData.Items.Length > 0){var txtData = txtLayer.TextData.Items[0];characterSpace = txtData.Style.Tracking * 0.1f;lineSpace = (float)txtData.Style.Leading * 0.1f;}return true;}return false;}}
}#endif

五、UI元素解析/生成器Helper

定义HelperBase解析器基类,不同的UI类型重写UI初始化方法,如需支持新的UI类型可以很方便进行扩展支持:

#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityGameFramework.Runtime;namespace UGF.EditorTools.Psd2UGUI
{public abstract class UIHelperBase : MonoBehaviour{public PsdLayerNode LayerNode => this.GetComponent<PsdLayerNode>();private void OnEnable(){ParseAndAttachUIElements();}/// <summary>/// 解析并关联UI元素,并且返回已经关联过的图层(已关联图层不再处理)/// </summary>/// <param name="layerNode"></param>/// <returns></returns>public abstract void ParseAndAttachUIElements();/// <summary>/// 获取UI依赖的LayerNodes/// </summary>/// <returns></returns>public abstract PsdLayerNode[] GetDependencies();/// <summary>/// 把UI实例进行UI元素初始化/// </summary>/// <param name="uiRoot"></param>protected abstract void InitUIElements(GameObject uiRoot);/// <summary>/// 筛选出UI依赖的非空LayerNode/// </summary>/// <param name="nodes"></param>/// <returns></returns>protected PsdLayerNode[] CalculateDependencies(params PsdLayerNode[] nodes){if (nodes == null || nodes.Length == 0) return null;for (int i = nodes.Length - 1; i >= 0; i--){var node = nodes[i];if (node == null || node == LayerNode) ArrayUtility.RemoveAt(ref nodes, i);}return nodes;}internal GameObject CreateUI(GameObject uiInstance = null){if ((int)this.LayerNode.UIType > UGUIParser.UITYPE_MAX || LayerNode.UIType == GUIType.Null) return null;if (uiInstance == null){var rule = UGUIParser.Instance.GetRule(this.LayerNode.UIType);if (rule == null || rule.UIPrefab == null){Debug.LogWarning($"创建UI类型{LayerNode.UIType}失败:Rule配置项不存在或UIPrefab为空");return null;}uiInstance = GameObject.Instantiate(rule.UIPrefab, Vector3.zero, Quaternion.identity);if (LayerNode.IsMainUIType){uiInstance.name = this.name;var key = uiInstance.GetOrAddComponent<UIStringKey>();key.Key = this.gameObject.GetInstanceID().ToString();}}InitUIElements(uiInstance);return uiInstance;}}
}
#endif

1. Text解析器:

#if UNITY_EDITOR
using UnityEngine;namespace UGF.EditorTools.Psd2UGUI
{[DisallowMultipleComponent]public class TextHelper : UIHelperBase{[SerializeField] PsdLayerNode text;public override PsdLayerNode[] GetDependencies(){return CalculateDependencies(text);}public override void ParseAndAttachUIElements(){if (LayerNode.IsTextLayer(out var _)){text = LayerNode;}else{LayerNode.SetUIType(UGUIParser.Instance.DefaultImage);}}protected override void InitUIElements(GameObject uiRoot){var textCom = uiRoot.GetComponentInChildren<UnityEngine.UI.Text>();UGUIParser.SetTextStyle(text, textCom);UGUIParser.SetRectTransform(text, textCom);}}
}
#endif

从ps文本图层获取文本字体、字号、颜色、字间距等信息,然后从Unity工程中查找对应的字体文件并赋值给Text组件:

internal bool ParseTextLayerInfo(out string text, out int fontSize, out float characterSpace, out float lineSpace, out Color fontColor, out UnityEngine.FontStyle fontStyle, out TMPro.FontStyles tmpFontStyle, out string fontName){text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;if (IsTextLayer(out var txtLayer)){text = txtLayer.Text;fontSize = (int)txtLayer.Font.Size;fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic)){fontStyle = UnityEngine.FontStyle.BoldAndItalic;}else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold)){fontStyle = UnityEngine.FontStyle.Bold;}else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic)){fontStyle = UnityEngine.FontStyle.Italic;}else{fontStyle = UnityEngine.FontStyle.Normal;}if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic)){tmpFontStyle |= TMPro.FontStyles.Italic;}if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold)){tmpFontStyle |= TMPro.FontStyles.Bold;}if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline)){tmpFontStyle |= TMPro.FontStyles.Underline;}if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout)){tmpFontStyle |= TMPro.FontStyles.Strikethrough;}fontName = txtLayer.Font.Name;if (txtLayer.TextData.Items.Length > 0){var txtData = txtLayer.TextData.Items[0];characterSpace = txtData.Style.Tracking * 0.1f;lineSpace = (float)txtData.Style.Leading * 0.1f;}return true;}return false;}

根据字体内部名查找ttf字体和TextMeshPro字体资源:

/// <summary>/// 根据字体名查找TMP_FontAsset/// </summary>/// <param name="fontName"></param>/// <returns></returns>public static TMP_FontAsset FindTMPFontAsset(string fontName){var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");foreach (var guid in fontGuids){var fontPath = AssetDatabase.GUIDToAssetPath(guid);var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(fontPath);if (font != null && font.faceInfo.familyName == fontName){return font;}}return null;}/// <summary>/// 根据字体名查找Font Asset/// </summary>/// <param name="fontName"></param>/// <returns></returns>public static UnityEngine.Font FindFontAsset(string fontName){var fontGuids = AssetDatabase.FindAssets("t:font");foreach (var guid in fontGuids){var fontPath = AssetDatabase.GUIDToAssetPath(guid);var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;if (font != null && font.fontTTFName == fontName){return AssetDatabase.LoadAssetAtPath<UnityEngine.Font>(fontPath);}}return null;}

2. Image解析器:

#if UNITY_EDITOR
using UnityEngine;namespace UGF.EditorTools.Psd2UGUI
{[DisallowMultipleComponent]public class ImageHelper : UIHelperBase{[SerializeField] PsdLayerNode image;public override PsdLayerNode[] GetDependencies(){return CalculateDependencies(image);}public override void ParseAndAttachUIElements(){image = LayerNode;}protected override void InitUIElements(GameObject uiRoot){var imgCom = uiRoot.GetComponentInChildren<UnityEngine.UI.Image>();UGUIParser.SetRectTransform(image,imgCom);imgCom.sprite = UGUIParser.LayerNode2Sprite(image, imgCom.type == UnityEngine.UI.Image.Type.Sliced);}}
}
#endif

自动把ps图层导出为Sprite资源,若Image为Sliced模式则自动计算并设置Sprite 9宫边界:

/// <summary>/// 把LayerNode图片保存到本地并返回/// </summary>/// <param name="layerNode"></param>/// <param name="auto9Slice">若没有设置Sprite的九宫,是否自动计算并设置九宫</param>/// <returns></returns>public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false){if (layerNode != null){var spAssetName = layerNode.ExportImageAsset(true);var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spAssetName);if (sprite != null){if (auto9Slice){var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;var rawReadable = spImpt.isReadable;if (!rawReadable){spImpt.isReadable = true;spImpt.SaveAndReimport();}if (spImpt.spriteBorder == Vector4.zero){spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);spImpt.isReadable = rawReadable;spImpt.SaveAndReimport();}}return sprite;}}return null;}

根据图片的Alpha通道计算出9宫边界,通常设置9宫边界还会考虑图片纹理因素,但程序难以智能识别,这里自动9宫只是适用于普通情况,还需要根据实际效果进行手动调整:

/// <summary>/// 自动计算贴图的 9宫 Border/// </summary>/// <param name="texture"></param>/// <param name="alphaThreshold">0-255</param>/// <returns></returns>public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3){int width = texture.width;int height = texture.height;Color32[] pixels = texture.GetPixels32();int minX = width;int minY = height;int maxX = 0;int maxY = 0;// 寻找不透明像素的最小和最大边界for (int y = 0; y < height; y++){for (int x = 0; x < width; x++){int pixelIndex = y * width + x;Color32 pixel = pixels[pixelIndex];if (pixel.a >= alphaThreshold){minX = Mathf.Min(minX, x);minY = Mathf.Min(minY, y);maxX = Mathf.Max(maxX, x);maxY = Mathf.Max(maxY, y);}}}// 计算最优的borderSizeint borderSizeX = (maxX - minX) / 3;int borderSizeY = (maxY - minY) / 3;int borderSize = Mathf.Min(borderSizeX, borderSizeY);// 根据边界和Border Size计算Nine Slice Borderint left = minX + borderSize;int right = maxX - borderSize;int top = minY + borderSize;int bottom = maxY - borderSize;// 确保边界在纹理范围内left = Mathf.Clamp(left, 0, width - 1);right = Mathf.Clamp(right, 0, width - 1);top = Mathf.Clamp(top, 0, height - 1);bottom = Mathf.Clamp(bottom, 0, height - 1);return new Vector4(left, top, width - right, height - bottom);}

3. Dropdown解析器,对于多种元素组成的复合型、嵌套型UI,可以很好的支持,并且可以任意嵌套组合,没有限制和约束。例如Dropdown内包含了一个ScrollView和一个Toggle类型的Item,就可以直接用ScrollView Helper和Toggle Helper分别对其解析:

#if UNITY_EDITOR
using UnityEngine;
using UnityEngine.UI;namespace UGF.EditorTools.Psd2UGUI
{[DisallowMultipleComponent]public class DropdownHelper : UIHelperBase{[SerializeField] PsdLayerNode background;[SerializeField] PsdLayerNode label;[SerializeField] PsdLayerNode arrow;[SerializeField] PsdLayerNode scrollView;[SerializeField] PsdLayerNode toggleItem;public override PsdLayerNode[] GetDependencies(){return CalculateDependencies(background, label, arrow, scrollView, toggleItem);}public override void ParseAndAttachUIElements(){background = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);label = LayerNode.FindSubLayerNode(GUIType.Dropdown_Label, GUIType.Text, GUIType.TMPText);arrow = LayerNode.FindSubLayerNode(GUIType.Dropdown_Arrow);scrollView = LayerNode.FindSubLayerNode(GUIType.ScrollView);toggleItem = LayerNode.FindSubLayerNode(GUIType.Toggle);}protected override void InitUIElements(GameObject uiRoot){var dpd = uiRoot.GetComponent<Dropdown>();UGUIParser.SetRectTransform(background, dpd);var bgImg = dpd.targetGraphic as Image;bgImg.sprite = UGUIParser.LayerNode2Sprite(background, bgImg.type == Image.Type.Sliced) ?? bgImg.sprite;UGUIParser.SetTextStyle(label, dpd.captionText);UGUIParser.SetRectTransform(label, dpd.captionText);var arrowImg = dpd.transform.Find("Arrow")?.GetComponent<Image>();if (arrowImg != null){UGUIParser.SetRectTransform(arrow, arrowImg);arrowImg.sprite = UGUIParser.LayerNode2Sprite(arrow, arrowImg.type == Image.Type.Sliced);}if (scrollView != null){var svTmp = uiRoot.GetComponentInChildren<ScrollRect>(true).GetComponent<RectTransform>();if (svTmp != null){var sViewGo = scrollView.GetComponent<ScrollViewHelper>()?.CreateUI(svTmp.gameObject);if (sViewGo != null){var sViewRect = sViewGo.GetComponent<RectTransform>();UGUIParser.SetRectTransform(scrollView, sViewRect);sViewRect.anchorMin = Vector2.zero;sViewRect.anchorMax = new Vector2(1, 0);sViewRect.anchoredPosition = new Vector2(0, -2);}if (toggleItem != null){var itemTmp = dpd.itemText != null ? dpd.itemText.transform.parent : null;if (itemTmp != null){toggleItem.GetComponent<ToggleHelper>()?.CreateUI(itemTmp.gameObject);}}}}}}
}
#endif

实现了每种基础UI元素的解析后就可以任意进行UI元素组合,例如Slider中包含Slider背景和填充条,在Slider中添加一个文本图层,解析出来后就是一个内涵Text文本的Slider进度条,解析前后的节点层级始终保持统一:

由于篇幅原因其它UI类型的解析代码就不贴了,UGUI和TextMeshProGUI共16种UI类型全部完美支持。

最后,附上psd源文件效果图和一键生成的UGUI预制体效果对比图,运行时效果(左),psd原图(右):

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/4311.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SAP HANA使用SQL创建SCHEMA:

语法是 CREATE SCHEMA “<Schema_Name>” 使用图形方法创建 SAP HANA 表&#xff1a; 创建图形计算视图&#xff1a;

FFmpeg视频转码关键参数详解

1 固定码率因子crf&#xff08;Constant Rate Factor&#xff09; 固定码率因子&#xff08;CRF&#xff09;是 x264 和 x265 编码器的默认质量&#xff08;和码率控制&#xff09;设置。取值范围是 0 到 51&#xff0c;这其中越低的值&#xff0c;结果质量越好&#xff0c;同…

React Antd Form.List 组件嵌套多级动态增减表单 + 表单联动复制实现

Antd Form.List 组件嵌套多级动态增减表单 表单联动复制实现 一、业务需求 有一个页面的组件&#xff0c;其中一部分需要用到动态的增减 复制表单&#xff0c;然后就想起 了使用 Antd 的 Form.List 去完成这个功能。 这个功能的要求是&#xff1a; 首先是一个动态的表单&…

SQL-每日一题【178.分数排名】

题目 表: Scores 编写 SQL 查询对分数进行排序。排名按以下规则计算: 分数应按从高到低排列。 如果两个分数相等&#xff0c;那么两个分数的排名应该相同。 在排名相同的分数后&#xff0c;排名数应该是下一个连续的整数。换句话说&#xff0c;排名之间不应该有空缺的数字。 …

Linux:LAMP搭建(全源码包安装)

LAMP 就是 Linux Apache Mysql PHP/Python 目录 Linux安装 Apache安装 Mysql安装 安装PHP 安装PHP扩展包 编译安装PHP PHP 添加优化模块 测试网页协同工作 Linux安装 虚拟机安装 (1条消息) VMware&#xff1a;安装centos7_鲍海超-GNUBHCkalitarro的博客-CSD…

Mybatis-Plus学习1

mybatis-plus需要两个依赖&#xff0c;一个lombok&#xff0c;一个mybatis-plus <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.1</version> </dependency> …

路由协议基本术语

文章目录 1、自治系统AS2、EGP和IGP3、度量标准和度量值4、管理距离5、路由协议与路由算法6、路由环路问题 1、自治系统AS Internet中&#xff0c;自治系统就是处于同一个管理机构&#xff08;如一个ISP&#xff09;控制下的路由器和网络群组 在同一个自治系统中的所有路由器…

Learn Mongodb DB数据库部署 ②

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; PHP MYSQL &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f44…

集合专题----List篇

1、Collection常用方法 package com.example.collection.Collection;import java.util.ArrayList; import java.util.List;public class Collection03 {public static void main(String[] args) {List list new ArrayList();//接口可以指向实现该接口的类//add:添加单个元素l…

快消EDI:联合利华Unilever EDI需求分析

联合利华&#xff08;Unilever&#xff09;是一家跨国消费品公司&#xff0c;总部位于英国和荷兰&#xff0c;在全球范围内经营着众多知名品牌&#xff0c;涵盖了食品、饮料、清洁剂、个人护理产品等多个领域。作为一家跨国公司&#xff0c;联合利华在全球各地都有业务和生产基…

内网安全:内网穿透详解

目录 内网穿透技术 内网穿透原理 实验环境 内网穿透项目 内网穿透&#xff1a;Ngrok 配置服务端 客户端配置 客户端生成后门&#xff0c;等待目标上线 内网穿透&#xff1a;Frp 客户端服务端建立连接 MSF生成后门&#xff0c;等待上线 内网穿透&#xff1a;Nps 服…

系列一、RocketMQ入门

一、MQ概述 1.1、MQ简介 MQ&#xff0c;Message Queue&#xff0c;是一种提供消息队列服务的中间件&#xff0c;也称为消息中间件&#xff0c;是一套提供了消息生产、存储、消费全过程的API软件系统。消息&#xff1a;消息即数据&#xff0c;一般消息的体量不会很大。 1.2、M…