工具效果:
第一步,把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);
- }
- }
- }
- // 计算最优的borderSize
- int borderSizeX = (maxX - minX) / 3;
- int borderSizeY = (maxY - minY) / 3;
- int borderSize = Mathf.Min(borderSizeX, borderSizeY);
- // 根据边界和Border Size计算Nine Slice Border
- int 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 objects
- else 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;
- }
- #region
-
- const 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);
- }
- }
- }
- // 计算最优的borderSize
- int borderSizeX = (maxX - minX) / 3;
- int borderSizeY = (maxY - minY) / 3;
- int borderSize = Mathf.Min(borderSizeX, borderSizeY);
- // 根据边界和Border Size计算Nine Slice Border
- int 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原图(右):

工具开源地点:https://github.com/sunsvip/PSD2UGUI_X
来源:https://blog.csdn.net/final5788/article/details/131406527
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |