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

[复制链接]
查看1032 | 回复0 | 2023-8-23 11:45:51 | 显示全部楼层 |阅读模式 来自 中国北京
工具效果:

第一步,把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), 会依次按范例查找图层, 可以最大化放宽对图层标志范例:
  1. buttonBackground = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);
  2. 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代码:
  1. #if UNITY_EDITOR
  2. using Aspose.PSD.FileFormats.Psd.Layers.FillLayers;
  3. using System;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using TMPro;
  8. using UnityEditor;
  9. using UnityEngine;
  10. namespace UGF.EditorTools.Psd2UGUI
  11. {
  12.     public enum GUIType
  13.     {
  14.         Null = 0,
  15.         Image,
  16.         RawImage,
  17.         Text,
  18.         Button,
  19.         Dropdown,
  20.         InputField,
  21.         Toggle,
  22.         Slider,
  23.         ScrollView,
  24.         Mask,
  25.         FillColor, //纯色填充
  26.         TMPText,
  27.         TMPButton,
  28.         TMPDropdown,
  29.         TMPInputField,
  30.         TMPToggle,
  31.         //UI的子类型, 以101开始。 0-100预留给UI类型, 新类型从尾部追加
  32.         Background = 101, //通用背景
  33.         //Button的子类型
  34.         Button_Highlight,
  35.         Button_Press,
  36.         Button_Select,
  37.         Button_Disable,
  38.         Button_Text,
  39.         //Dropdown/TMPDropdown的子类型
  40.         Dropdown_Label,
  41.         Dropdown_Arrow,
  42.         //InputField/TMPInputField的子类型
  43.         InputField_Placeholder,
  44.         InputField_Text,
  45.         //Toggle的子类型
  46.         Toggle_Checkmark,
  47.         Toggle_Label,
  48.         //Slider的子类型
  49.         Slider_Fill,
  50.         Slider_Handle,
  51.         //ScrollView的子类型
  52.         ScrollView_Viewport, //列表可视区域的遮罩图
  53.         ScrollView_HorizontalBarBG, //水平滑动栏背景
  54.         ScrollView_HorizontalBar,//水平滑块
  55.         ScrollView_VerticalBarBG, //垂直滑动栏背景
  56.         ScrollView_VerticalBar, //垂直滑动块
  57.     }
  58.     [Serializable]
  59.     public class UGUIParseRule
  60.     {
  61.         public GUIType UIType;
  62.         public string[] TypeMatches; //类型匹配标识
  63.         public GameObject UIPrefab; //UI模板
  64.         public string UIHelper; //UIHelper类型全名
  65.         public string Comment;//注释
  66.     }
  67.     [CustomEditor(typeof(UGUIParser))]
  68.     public class UGUIParserEditor : Editor
  69.     {
  70.         private SerializedProperty readmeProperty;
  71.         private void OnEnable()
  72.         {
  73.             readmeProperty = serializedObject.FindProperty("readmeDoc");
  74.         }
  75.         public override void OnInspectorGUI()
  76.         {
  77.             serializedObject.Update();
  78.             if (GUILayout.Button("导出使用文档"))
  79.             {
  80.                 (target as UGUIParser).ExportReadmeDoc();
  81.             }
  82.             EditorGUILayout.LabelField("使用说明:");
  83.             readmeProperty.stringValue = EditorGUILayout.TextArea(readmeProperty.stringValue, GUILayout.Height(100));
  84.             serializedObject.ApplyModifiedProperties();
  85.             base.OnInspectorGUI();
  86.         }
  87.     }
  88.     [CreateAssetMenu(fileName = "Psd2UIFormConfig", menuName = "ScriptableObject/Psd2UIForm Config【Psd2UIForm工具配置】")]
  89.     public class UGUIParser : ScriptableObject
  90.     {
  91.         public const int UITYPE_MAX = 100;
  92.         [SerializeField] GUIType defaultTextType = GUIType.Text;
  93.         [SerializeField] GUIType defaultImageType = GUIType.Image;
  94.         [SerializeField] GameObject uiFormTemplate;
  95.         [SerializeField] UGUIParseRule[] rules;
  96.         [HideInInspector][SerializeField] string readmeDoc = "使用说明";
  97.         public GUIType DefaultText => defaultTextType;
  98.         public GUIType DefaultImage => defaultImageType;
  99.         public GameObject UIFormTemplate => uiFormTemplate;
  100.         private static UGUIParser mInstance = null;
  101.         public static UGUIParser Instance
  102.         {
  103.             get
  104.             {
  105.                 if (mInstance == null)
  106.                 {
  107.                     var guid = AssetDatabase.FindAssets("t:UGUIParser").FirstOrDefault();
  108.                     mInstance = AssetDatabase.LoadAssetAtPath<UGUIParser>(AssetDatabase.GUIDToAssetPath(guid));
  109.                 }
  110.                 return mInstance;
  111.             }
  112.         }
  113.         public static bool IsMainUIType(GUIType tp)
  114.         {
  115.             return (int)tp <= UITYPE_MAX;
  116.         }
  117.         public Type GetHelperType(GUIType uiType)
  118.         {
  119.             if (uiType == GUIType.Null) return null;
  120.             var rule = GetRule(uiType);
  121.             if (rule == null || string.IsNullOrWhiteSpace(rule.UIHelper)) return null;
  122.             return Type.GetType(rule.UIHelper);
  123.         }
  124.         public UGUIParseRule GetRule(GUIType uiType)
  125.         {
  126.             foreach (var rule in rules)
  127.             {
  128.                 if (rule.UIType == uiType) return rule;
  129.             }
  130.             return null;
  131.         }
  132.         /// <summary>
  133.         /// 根据图层命名解析UI类型
  134.         /// </summary>
  135.         /// <param name="layer"></param>
  136.         /// <param name="comType"></param>
  137.         /// <returns></returns>
  138.         public bool TryParse(PsdLayerNode layer, out UGUIParseRule result)
  139.         {
  140.             result = null;
  141.             var layerName = layer.BindPsdLayer.Name;
  142.             if (Path.HasExtension(layerName))
  143.             {
  144.                 var tpTag = Path.GetExtension(layerName).Substring(1).ToLower();
  145.                 foreach (var rule in rules)
  146.                 {
  147.                     foreach (var item in rule.TypeMatches)
  148.                     {
  149.                         if (tpTag.CompareTo(item.ToLower()) == 0)
  150.                         {
  151.                             result = rule;
  152.                             return true;
  153.                         }
  154.                     }
  155.                 }
  156.             }
  157.             switch (layer.LayerType)
  158.             {
  159.                 case PsdLayerType.TextLayer:
  160.                     result = rules.First(itm => itm.UIType == defaultTextType);
  161.                     break;
  162.                 case PsdLayerType.LayerGroup:
  163.                     result = rules.First(itm => itm.UIType == GUIType.Null);
  164.                     break;
  165.                 default:
  166.                     result = rules.First(itm => itm.UIType == defaultImageType);
  167.                     break;
  168.             }
  169.             return result != null;
  170.         }
  171.         /// <summary>
  172.         /// 根据图层大小和位置设置UI节点大小和位置
  173.         /// </summary>
  174.         /// <param name="layerNode"></param>
  175.         /// <param name="uiNode"></param>
  176.         /// <param name="pos">是否设置位置</param>
  177.         public static void SetRectTransform(PsdLayerNode layerNode, UnityEngine.Component uiNode, bool pos = true, bool width = true, bool height = true, int extSize = 0)
  178.         {
  179.             if (uiNode != null && layerNode != null)
  180.             {
  181.                 var rect = layerNode.LayerRect;
  182.                 var rectTransform = uiNode.GetComponent<RectTransform>();
  183.                 if (width) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rect.size.x + extSize);
  184.                 if (height) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rect.size.y + extSize);
  185.                 if (pos)
  186.                 {
  187.                     rectTransform.position = rect.position + rectTransform.rect.size * (rectTransform.pivot - Vector2.one * 0.5f)*0.01f;
  188.                 }
  189.             }
  190.         }
  191.         /// <summary>
  192.         /// 把LayerNode图片保存到本地并返回
  193.         /// </summary>
  194.         /// <param name="layerNode"></param>
  195.         /// <returns></returns>
  196.         public static Texture2D LayerNode2Texture(PsdLayerNode layerNode)
  197.         {
  198.             if (layerNode != null)
  199.             {
  200.                 var spAssetName = layerNode.ExportImageAsset(false);
  201.                 var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(spAssetName);
  202.                 return texture;
  203.             }
  204.             return null;
  205.         }
  206.         /// <summary>
  207.         /// 把LayerNode图片保存到本地并返回
  208.         /// </summary>
  209.         /// <param name="layerNode"></param>
  210.         /// <param name="auto9Slice">若没有设置Sprite的九宫,是否自动计算并设置九宫</param>
  211.         /// <returns></returns>
  212.         public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false)
  213.         {
  214.             if (layerNode != null)
  215.             {
  216.                 var spAssetName = layerNode.ExportImageAsset(true);
  217.                 var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spAssetName);
  218.                 if (sprite != null)
  219.                 {
  220.                     if (auto9Slice)
  221.                     {
  222.                         var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;
  223.                         var rawReadable = spImpt.isReadable;
  224.                         if (!rawReadable)
  225.                         {
  226.                             spImpt.isReadable = true;
  227.                             spImpt.SaveAndReimport();
  228.                         }
  229.                         if (spImpt.spriteBorder == Vector4.zero)
  230.                         {
  231.                             spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);
  232.                             spImpt.isReadable = rawReadable;
  233.                             spImpt.SaveAndReimport();
  234.                         }
  235.                     }
  236.                     return sprite;
  237.                 }
  238.             }
  239.             return null;
  240.         }
  241.         /// <summary>
  242.         /// 自动计算贴图的 9宫 Border
  243.         /// </summary>
  244.         /// <param name="texture"></param>
  245.         /// <param name="alphaThreshold">0-255</param>
  246.         /// <returns></returns>
  247.         public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3)
  248.         {
  249.             int width = texture.width;
  250.             int height = texture.height;
  251.             Color32[] pixels = texture.GetPixels32();
  252.             int minX = width;
  253.             int minY = height;
  254.             int maxX = 0;
  255.             int maxY = 0;
  256.             // 寻找不透明像素的最小和最大边界
  257.             for (int y = 0; y < height; y++)
  258.             {
  259.                 for (int x = 0; x < width; x++)
  260.                 {
  261.                     int pixelIndex = y * width + x;
  262.                     Color32 pixel = pixels[pixelIndex];
  263.                     if (pixel.a >= alphaThreshold)
  264.                     {
  265.                         minX = Mathf.Min(minX, x);
  266.                         minY = Mathf.Min(minY, y);
  267.                         maxX = Mathf.Max(maxX, x);
  268.                         maxY = Mathf.Max(maxY, y);
  269.                     }
  270.                 }
  271.             }
  272.             // 计算最优的borderSize
  273.             int borderSizeX = (maxX - minX) / 3;
  274.             int borderSizeY = (maxY - minY) / 3;
  275.             int borderSize = Mathf.Min(borderSizeX, borderSizeY);
  276.             // 根据边界和Border Size计算Nine Slice Border
  277.             int left = minX + borderSize;
  278.             int right = maxX - borderSize;
  279.             int top = minY + borderSize;
  280.             int bottom = maxY - borderSize;
  281.             // 确保边界在纹理范围内
  282.             left = Mathf.Clamp(left, 0, width - 1);
  283.             right = Mathf.Clamp(right, 0, width - 1);
  284.             top = Mathf.Clamp(top, 0, height - 1);
  285.             bottom = Mathf.Clamp(bottom, 0, height - 1);
  286.             return new Vector4(left, top, width - right, height - bottom);
  287.         }
  288.         /// <summary>
  289.         /// 把PS的字体样式同步设置到UGUI Text
  290.         /// </summary>
  291.         /// <param name="txtLayer"></param>
  292.         /// <param name="text"></param>
  293.         public static void SetTextStyle(PsdLayerNode txtLayer, UnityEngine.UI.Text text)
  294.         {
  295.             if (text == null) return;
  296.             text.gameObject.SetActive(txtLayer != null);
  297.             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))
  298.             {
  299.                 var tFont = FindFontAsset(fName);
  300.                 if (tFont != null) text.font = tFont;
  301.                 text.text = str;
  302.                 text.fontSize = size;
  303.                 text.fontStyle = style;
  304.                 text.color = col;
  305.                 text.lineSpacing = lineSpace;
  306.             }
  307.         }
  308.         /// <summary>
  309.         /// 把PS的字体样式同步设置到TextMeshProUGUI
  310.         /// </summary>
  311.         /// <param name="txtLayer"></param>
  312.         /// <param name="text"></param>
  313.         public static void SetTextStyle(PsdLayerNode txtLayer, TextMeshProUGUI text)
  314.         {
  315.             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))
  316.             {
  317.                 var tFont = FindTMPFontAsset(fName);
  318.                 if (tFont != null) text.font = tFont;
  319.                 text.text = str;
  320.                 text.fontSize = size;
  321.                 text.fontStyle = tmpStyle;
  322.                 text.color = col;
  323.                 text.characterSpacing = charSpace;
  324.                 text.lineSpacing = lineSpace;
  325.             }
  326.         }
  327.         /// <summary>
  328.         /// 根据字体名查找TMP_FontAsset
  329.         /// </summary>
  330.         /// <param name="fontName"></param>
  331.         /// <returns></returns>
  332.         public static TMP_FontAsset FindTMPFontAsset(string fontName)
  333.         {
  334.             var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");
  335.             foreach (var guid in fontGuids)
  336.             {
  337.                 var fontPath = AssetDatabase.GUIDToAssetPath(guid);
  338.                 var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(fontPath);
  339.                 if (font != null && font.faceInfo.familyName == fontName)
  340.                 {
  341.                     return font;
  342.                 }
  343.             }
  344.             return null;
  345.         }
  346.         /// <summary>
  347.         /// 根据字体名查找Font Asset
  348.         /// </summary>
  349.         /// <param name="fontName"></param>
  350.         /// <returns></returns>
  351.         public static UnityEngine.Font FindFontAsset(string fontName)
  352.         {
  353.             var fontGuids = AssetDatabase.FindAssets("t:font");
  354.             foreach (var guid in fontGuids)
  355.             {
  356.                 var fontPath = AssetDatabase.GUIDToAssetPath(guid);
  357.                 var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;
  358.                 if (font != null && font.fontTTFName == fontName)
  359.                 {
  360.                     return AssetDatabase.LoadAssetAtPath<UnityEngine.Font>(fontPath);
  361.                 }
  362.             }
  363.             return null;
  364.         }
  365.         internal static UnityEngine.Color LayerNode2Color(PsdLayerNode fillColor, Color defaultColor)
  366.         {
  367.             if (fillColor != null && fillColor.BindPsdLayer is FillLayer fillLayer)
  368.             {
  369.                 var layerColor = fillLayer.GetPixel(fillLayer.Width / 2, fillLayer.Height / 2);
  370.                 return new UnityEngine.Color(layerColor.R, layerColor.G, layerColor.B, fillLayer.Opacity) / (float)255;
  371.             }
  372.             return defaultColor;
  373.         }
  374.         /// <summary>
  375.         /// 导出UI设计师使用规则文档
  376.         /// </summary>
  377.         /// <exception cref="NotImplementedException"></exception>
  378.         internal void ExportReadmeDoc()
  379.         {
  380.             var exportDir = EditorUtility.SaveFolderPanel("选择文档导出路径", Application.dataPath, null);
  381.             if (string.IsNullOrWhiteSpace(exportDir) || !Directory.Exists(exportDir))
  382.             {
  383.                 return;
  384.             }
  385.             var docFile = UtilityBuiltin.ResPath.GetCombinePath(exportDir, "Psd2UGUI设计师使用文档.doc");
  386.             var strBuilder = new StringBuilder();
  387.             strBuilder.AppendLine("使用说明:");
  388.             strBuilder.AppendLine(this.readmeDoc);
  389.             strBuilder.AppendLine(Environment.NewLine + Environment.NewLine);
  390.             strBuilder.AppendLine("UI类型标识: 图层/组命名以'.类型'结尾");
  391.             strBuilder.AppendLine("UI类型标识列表:");
  392.             foreach (var rule in rules)
  393.             {
  394.                 if (rule.UIType == GUIType.Null) continue;
  395.                 strBuilder.AppendLine($"{rule.UIType}: {rule.Comment}");
  396.                 strBuilder.Append("类型标识: ");
  397.                 foreach (var tag in rule.TypeMatches)
  398.                 {
  399.                     strBuilder.Append($".{tag}, ");
  400.                 }
  401.                 strBuilder.AppendLine();
  402.                 strBuilder.AppendLine();
  403.             }
  404.             try
  405.             {
  406.                 File.WriteAllText(docFile, strBuilder.ToString(), System.Text.Encoding.UTF8);
  407.                 EditorUtility.RevealInFinder(docFile);
  408.             }
  409.             catch (Exception e)
  410.             {
  411.                 Debug.LogException(e);
  412.             }
  413.         }
  414.     }
  415. }
  416. #endif
复制代码
 三、PS脚本编写,一键转换殊效图层/文本图层为智能对象

为了辅助UI筹划师,制止手动转换智能对象会有遗漏,筹划师交付PSD文件前需要实行自动化脚本,把殊效图层/字体转为智能对象,这样纵然差别设备字库丢失也能保持字体原本样式。PS脚本是用js语言编写,没有代码提示是最大的停滞。幸亏没有复杂逻辑,只是遍历当前打开的psd文档图层,判断图层是否带有殊效或是否为文本图层,把符合条件的图层转换为智能对象:
  1. // 判断图层是否包含特效
  2. function hasLayerEffect(layer) {
  3.     app.activeDocument.activeLayer = layer;
  4.         var hasEffect = false;
  5.         try {
  6.                 var ref = new ActionReference();
  7.                 var keyLayerEffects = app.charIDToTypeID( 'Lefx' );
  8.                 ref.putProperty( app.charIDToTypeID( 'Prpr' ), keyLayerEffects );
  9.                 ref.putEnumerated( app.charIDToTypeID( 'Lyr ' ), app.charIDToTypeID( 'Ordn' ), app.charIDToTypeID( 'Trgt' ) );
  10.                 var desc = executeActionGet( ref );
  11.                 if ( desc.hasKey( keyLayerEffects ) ) {
  12.                         hasEffect = true;
  13.                 }
  14.         }catch(e) {
  15.                 hasEffect = false;
  16.         }
  17.         return hasEffect;
  18. }
  19. function convertLayersToSmartObjects(layers)
  20. {
  21.     for (var i = layers.length - 1; i >= 0; i--)
  22.     {
  23.         var layer = layers[i];
  24.         if (layer.typename === "LayerSet")
  25.         {
  26.                convertLayersToSmartObjects(layer.layers); // Recursively convert layers in layer sets
  27.         }
  28.         else
  29.         {
  30.              if (hasLayerEffect(layer)){
  31.                 if(layer.kind === LayerKind.TEXT)convertToSmartObject(layer); // Convert layers with layer effects to smart objects
  32.                 else layer.rasterize(RasterizeType.SHAPE);
  33.              }
  34.         }
  35.     }
  36. }
  37. // 把图层转换为智能对象,功能等同右键图层->转为智能对象
  38. function convertToSmartObject(layer) {
  39.     app.activeDocument.activeLayer = layer;
  40.     // 创建一个新的智能对象
  41.     var idnewPlacedLayer = stringIDToTypeID("newPlacedLayer");
  42.     executeAction(idnewPlacedLayer, undefined, DialogModes.NO);
  43.    
  44. }
  45. // 导出处理后的PSD文件
  46. function exportPSD() {
  47.   var doc = app.activeDocument;
  48.   var savePath = Folder.selectDialog("选择psd导出路径");
  49.   if (savePath != null) {
  50.     var saveOptions = new PhotoshopSaveOptions();
  51.     saveOptions.embedColorProfile = true;
  52.     saveOptions.alphaChannels = true;
  53.     var saveFile = new File(savePath + "/" + doc.name);
  54.     doc.saveAs(saveFile, saveOptions, true, Extension.LOWERCASE);
  55.     alert("PSD已成功导出!");
  56.   }
  57. }
  58. function convertAndExport(){
  59.     convertLayersToSmartObjects (app.activeDocument.layers);
  60.     //exportPSD();
  61. }
  62. app.activeDocument.suspendHistory("Convert2SmartObject", "convertAndExport();");
  63. //~ 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编辑器代码:
  1. #if UNITY_EDITOR
  2. using Aspose.PSD.FileFormats.Psd;
  3. using Aspose.PSD.FileFormats.Psd.Layers;
  4. using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;
  5. using Aspose.PSD.ImageLoadOptions;
  6. using GameFramework;
  7. using HarmonyLib;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Text;
  13. using UnityEditor;
  14. using UnityEditor.SceneManagement;
  15. using UnityEngine;
  16. using UnityGameFramework.Runtime;
  17. namespace UGF.EditorTools.Psd2UGUI
  18. {
  19.     #region Crack
  20.     [HarmonyPatch(typeof(System.Xml.XmlElement), nameof(System.Xml.XmlElement.InnerText), MethodType.Getter)]
  21.     class CrackAspose
  22.     {
  23.         static void Postfix(ref string __result)
  24.         {
  25.             if (__result == "20220516")
  26.             {
  27.                 __result = "20500516";
  28.             }
  29.             //else if (__result == "20210827")
  30.             //{
  31.             //    __result = "20250827";
  32.             //}
  33.         }
  34.     }
  35.     #endregion
  36.     [CustomEditor(typeof(Psd2UIFormConverter))]
  37.     public class Psd2UIFormConverterInspector : UnityEditor.Editor
  38.     {
  39.         Psd2UIFormConverter targetLogic;
  40.         GUIContent parsePsd2NodesBt;
  41.         GUIContent exportUISpritesBt;
  42.         GUIContent generateUIFormBt;
  43.         GUILayoutOption btHeight;
  44.         private void OnEnable()
  45.         {
  46.             btHeight = GUILayout.Height(30);
  47.             targetLogic = target as Psd2UIFormConverter;
  48.             parsePsd2NodesBt = new GUIContent("解析psd图层", "把psd图层解析为可编辑节点树");
  49.             exportUISpritesBt = new GUIContent("导出Images", "导出勾选的psd图层为碎图");
  50.             generateUIFormBt = new GUIContent("生成UIForm", "根据解析后的节点树生成UIForm Prefab");
  51.             if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir))
  52.             {
  53.                 Debug.LogWarning($"UIForm输出路径为空!");
  54.             }
  55.         }
  56.         private void OnDisable()
  57.         {
  58.             Psd2UIFormSettings.Save();
  59.         }
  60.         public override void OnInspectorGUI()
  61.         {
  62.             EditorGUILayout.BeginVertical("box");
  63.             {
  64.                 EditorGUILayout.BeginHorizontal();
  65.                 {
  66.                     EditorGUILayout.LabelField("自动压缩图片:", GUILayout.Width(150));
  67.                     Psd2UIFormSettings.Instance.CompressImage = EditorGUILayout.Toggle(Psd2UIFormSettings.Instance.CompressImage);
  68.                     EditorGUILayout.EndHorizontal();
  69.                 }
  70.                 EditorGUILayout.BeginHorizontal();
  71.                 {
  72.                     EditorGUILayout.LabelField("UI图片导出路径:", GUILayout.Width(150));
  73.                     Psd2UIFormSettings.Instance.UIImagesOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIImagesOutputDir);
  74.                     if (GUILayout.Button("选择路径", GUILayout.Width(80)))
  75.                     {
  76.                         var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIImagesOutputDir, null);
  77.                         if (!string.IsNullOrWhiteSpace(retPath))
  78.                         {
  79.                             if (!retPath.StartsWith("Assets/"))
  80.                             {
  81.                                 retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);
  82.                             }
  83.                             Psd2UIFormSettings.Instance.UIImagesOutputDir = retPath;
  84.                             Psd2UIFormSettings.Save();
  85.                         }
  86.                         GUIUtility.ExitGUI();
  87.                     }
  88.                     EditorGUILayout.EndHorizontal();
  89.                 }
  90.                 EditorGUILayout.BeginHorizontal();
  91.                 {
  92.                     Psd2UIFormSettings.Instance.UseUIFormOutputDir = EditorGUILayout.ToggleLeft("使用UIForm导出路径:", Psd2UIFormSettings.Instance.UseUIFormOutputDir, GUILayout.Width(150));
  93.                     EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.UseUIFormOutputDir);
  94.                     {
  95.                         Psd2UIFormSettings.Instance.UIFormOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormOutputDir);
  96.                         if (GUILayout.Button("选择路径", GUILayout.Width(80)))
  97.                         {
  98.                             var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormOutputDir, null);
  99.                             if (!string.IsNullOrWhiteSpace(retPath))
  100.                             {
  101.                                 if (!retPath.StartsWith("Assets/"))
  102.                                 {
  103.                                     retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);
  104.                                 }
  105.                                 Psd2UIFormSettings.Instance.UIFormOutputDir = retPath;
  106.                                 Psd2UIFormSettings.Save();
  107.                             }
  108.                             GUIUtility.ExitGUI();
  109.                         }
  110.                         EditorGUI.EndDisabledGroup();
  111.                     }
  112.                     EditorGUILayout.EndHorizontal();
  113.                 }
  114.                 //EditorGUILayout.BeginHorizontal();
  115.                 //{
  116.                 //    Psd2UIFormSettings.Instance.AutoCreateUIFormScript = EditorGUILayout.ToggleLeft("生成UIForm代码:", Psd2UIFormSettings.Instance.AutoCreateUIFormScript, GUILayout.Width(150));
  117.                 //    EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.AutoCreateUIFormScript);
  118.                 //    {
  119.                 //        Psd2UIFormSettings.Instance.UIFormScriptOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormScriptOutputDir);
  120.                 //        if (GUILayout.Button("选择路径", GUILayout.Width(80)))
  121.                 //        {
  122.                 //            var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormScriptOutputDir, null);
  123.                 //            if (!string.IsNullOrWhiteSpace(retPath))
  124.                 //            {
  125.                 //                if (!retPath.StartsWith("Assets/"))
  126.                 //                {
  127.                 //                    retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);
  128.                 //                }
  129.                 //                Psd2UIFormSettings.Instance.UIFormScriptOutputDir = retPath;
  130.                 //                Psd2UIFormSettings.Save();
  131.                 //            }
  132.                 //            GUIUtility.ExitGUI();
  133.                 //        }
  134.                 //        EditorGUI.EndDisabledGroup();
  135.                 //    }
  136.                 //    EditorGUILayout.EndHorizontal();
  137.                 //}
  138.                 EditorGUILayout.EndVertical();
  139.             }
  140.             EditorGUILayout.BeginHorizontal();
  141.             {
  142.                 if (GUILayout.Button(parsePsd2NodesBt, btHeight))
  143.                 {
  144.                     Psd2UIFormConverter.ParsePsd2LayerPrefab(targetLogic.PsdAssetName, targetLogic);
  145.                 }
  146.                 if (GUILayout.Button(exportUISpritesBt, btHeight))
  147.                 {
  148.                     targetLogic.ExportSprites();
  149.                 }
  150.                 EditorGUILayout.EndHorizontal();
  151.             }
  152.             if (GUILayout.Button(generateUIFormBt, btHeight))
  153.             {
  154.                 targetLogic.GenerateUIForm();
  155.             }
  156.             base.OnInspectorGUI();
  157.         }
  158.         public override bool HasPreviewGUI()
  159.         {
  160.             return targetLogic.BindPsdAsset != null;
  161.         }
  162.         public override void OnPreviewGUI(Rect r, GUIStyle background)
  163.         {
  164.             GUI.DrawTexture(r, targetLogic.BindPsdAsset.texture, ScaleMode.ScaleToFit);
  165.             //base.OnPreviewGUI(r, background);
  166.         }
  167.     }
  168.     /// <summary>
  169.     /// Psd文件转成UIForm prefab
  170.     /// </summary>
  171.     [ExecuteInEditMode]
  172.     [RequireComponent(typeof(SpriteRenderer))]
  173.     public class Psd2UIFormConverter : MonoBehaviour
  174.     {
  175.         const string RecordLayerOperation = "Change Export Image";
  176.         public static Psd2UIFormConverter Instance { get; private set; }
  177.         [ReadOnlyField][SerializeField] public string psdAssetChangeTime;//文件修改时间标识
  178.         [Tooltip("UIForm名字")][SerializeField] private string uiFormName;
  179.         [Tooltip("关联的psd文件")][SerializeField] private UnityEngine.Sprite psdAsset;
  180.         [Header("Debug:")][SerializeField] bool drawLayerRectGizmos = true;
  181.         [SerializeField] UnityEngine.Color drawLayerRectGizmosColor = UnityEngine.Color.green;
  182.         private PsdImage psdInstance;//psd文件解析实例
  183.         private GUIStyle uiTypeLabelStyle;
  184.         public string PsdAssetName => psdAsset != null ? AssetDatabase.GetAssetPath(psdAsset) : null;
  185.         public UnityEngine.Sprite BindPsdAsset => psdAsset;
  186.         public Vector2Int UIFormCanvasSize { get; private set; } = new Vector2Int(750, 1334);
  187.         private void OnEnable()
  188.         {
  189.             Instance = this;
  190.             uiTypeLabelStyle = new GUIStyle();
  191.             uiTypeLabelStyle.fontSize = 13;
  192.             uiTypeLabelStyle.fontStyle = UnityEngine.FontStyle.BoldAndItalic;
  193.             UnityEngine.ColorUtility.TryParseHtmlString("#7ED994", out var color);
  194.             uiTypeLabelStyle.normal.textColor = color;
  195.             EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
  196.             if (psdInstance == null && !string.IsNullOrWhiteSpace(PsdAssetName))
  197.             {
  198.                 RefreshNodesBindLayer();
  199.             }
  200.         }
  201.         private void Start()
  202.         {
  203.             if (this.CheckPsdAssetHasChanged())
  204.             {
  205.                 if (EditorUtility.DisplayDialog("PSD -> UIForm", $"{gameObject.name}关联的psd文件[{this.PsdAssetName}]已改变,是否重新解析节点树?", "是", "否"))
  206.                 {
  207.                     if (Psd2UIFormConverter.ParsePsd2LayerPrefab(this.PsdAssetName, this))
  208.                     {
  209.                         RefreshNodesBindLayer();
  210.                     }
  211.                 }
  212.             }
  213.             else
  214.             {
  215.                 RefreshNodesBindLayer();
  216.             }
  217.         }
  218.         private void OnDrawGizmos()
  219.         {
  220.             if (drawLayerRectGizmos)
  221.             {
  222.                 var nodes = this.GetComponentsInChildren<PsdLayerNode>();
  223.                 Gizmos.color = drawLayerRectGizmosColor;
  224.                 foreach (var item in nodes)
  225.                 {
  226.                     if (item.NeedExportImage())
  227.                     {
  228.                         Gizmos.DrawWireCube(item.LayerRect.position * 0.01f, item.LayerRect.size * 0.01f);
  229.                     }
  230.                 }
  231.             }
  232.         }
  233.         private void OnHierarchyGUI(int instanceID, Rect selectionRect)
  234.         {
  235.             if (Event.current == null) return;
  236.             var node = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
  237.             if (node == null || node == this.gameObject) return;
  238.             if (!node.TryGetComponent<PsdLayerNode>(out var layer)) return;
  239.             Rect tmpRect = selectionRect;
  240.             tmpRect.x = 35;
  241.             tmpRect.width = 10;
  242.             Undo.RecordObject(layer, RecordLayerOperation);
  243.             EditorGUI.BeginChangeCheck();
  244.             {
  245.                 layer.markToExport = EditorGUI.Toggle(tmpRect, layer.markToExport);
  246.                 if (EditorGUI.EndChangeCheck())
  247.                 {
  248.                     if (Selection.gameObjects.Length > 1) SetExportImageTg(Selection.gameObjects, layer.markToExport);
  249.                     EditorUtility.SetDirty(layer);
  250.                 }
  251.             }
  252.             tmpRect.width = Mathf.Clamp(selectionRect.xMax * 0.2f, 100, 200);
  253.             tmpRect.x = selectionRect.xMax - tmpRect.width;
  254.             //EditorGUI.LabelField(tmpRect, layer.UIType.ToString(), uiTypeLabelStyle);
  255.             if (EditorGUI.DropdownButton(tmpRect, new GUIContent(layer.UIType.ToString()), FocusType.Passive))
  256.             {
  257.                 var dropdownMenu = PopEnumMenu<GUIType>(layer.UIType, selectUIType =>
  258.                 {
  259.                     layer.SetUIType(selectUIType);
  260.                     EditorUtility.SetDirty(layer);
  261.                 });
  262.                 dropdownMenu.ShowAsContext();
  263.             }
  264.         }
  265.         private GenericMenu PopEnumMenu<T>(T currentValue, Action<T> onSelectEnum) where T : Enum
  266.         {
  267.             var names = Enum.GetValues(typeof(T));
  268.             var dropdownMenu = new GenericMenu();
  269.             foreach (T item in names)
  270.             {
  271.                 dropdownMenu.AddItem(new GUIContent(item.ToString()), item.Equals(currentValue), () => { onSelectEnum(item); });
  272.             }
  273.             return dropdownMenu;
  274.         }
  275.         /// <summary>
  276.         /// 批量勾选导出图片
  277.         /// </summary>
  278.         /// <param name="selects"></param>
  279.         /// <param name="exportImg"></param>
  280.         private void SetExportImageTg(GameObject[] selects, bool exportImg)
  281.         {
  282.             var selectLayerNodes = selects.Where(item => item?.GetComponent<PsdLayerNode>() != null).ToArray();
  283.             foreach (var layer in selectLayerNodes)
  284.             {
  285.                 layer.GetComponent<PsdLayerNode>().markToExport = exportImg;
  286.             }
  287.         }
  288.         private void OnDestroy()
  289.         {
  290.             EditorApplication.hierarchyWindowItemOnGUI -= OnHierarchyGUI;
  291.             if (this.psdInstance != null && !psdInstance.Disposed)
  292.             {
  293.                 psdInstance.Dispose();
  294.             }
  295.         }
  296.         private void RefreshNodesBindLayer()
  297.         {
  298.             if (psdInstance == null || psdInstance.Disposed)
  299.             {
  300.                 if (!File.Exists(PsdAssetName))
  301.                 {
  302.                     Debug.LogError($"刷新节点绑定图层失败! psd文件不存在");
  303.                     return;
  304.                 }
  305.                 var psdOpts = new PsdLoadOptions()
  306.                 {
  307.                     LoadEffectsResource = true,
  308.                     ReadOnlyMode = false,
  309.                 };
  310.                 psdInstance = Aspose.PSD.Image.Load(PsdAssetName, psdOpts) as PsdImage;
  311.                 UIFormCanvasSize.Set(psdInstance.Size.Width, psdInstance.Size.Height);
  312.             }
  313.             var layers = GetComponentsInChildren<PsdLayerNode>(true);
  314.             foreach (var layer in layers)
  315.             {
  316.                 layer.InitPsdLayers(psdInstance);
  317.             }
  318.             var spRender = gameObject.GetOrAddComponent<SpriteRenderer>();
  319.             spRender.sprite = this.psdAsset;
  320.         }
  321.         #region
  322.         
  323.         const string AsposeLicenseKey = "此处为Aspose.PSD证书";
  324.         static bool licenseInitiated = false;
  325.         [InitializeOnLoadMethod]
  326.         static void InitAsposeLicense()
  327.         {
  328.             if (licenseInitiated) return;
  329.             var harmonyHook = new Harmony("Crack.Aspose");
  330.             harmonyHook.PatchAll();
  331.             new Aspose.PSD.License().SetLicense(new MemoryStream(Convert.FromBase64String(AsposeLicenseKey)));
  332.             licenseInitiated = true;
  333.             harmonyHook.UnpatchAll();
  334.             //GetAllLayerType();
  335.         }
  336.         static void GetAllLayerType()
  337.         {
  338.             var psdLib = Utility.Assembly.GetAssemblies().FirstOrDefault(item => item.GetName().Name == "Aspose.PSD");
  339.             var layers = psdLib.GetTypes().Where(tp => tp.IsSubclassOf(typeof(Layer)) && !tp.IsAbstract);
  340.             string layerEnumNames = "";
  341.             foreach (var item in layers)
  342.             {
  343.                 layerEnumNames += $"{item.Name},\n";
  344.             }
  345.             Debug.Log(layerEnumNames);
  346.         }
  347.         #endregion Aspose License
  348.         [MenuItem("Assets/GF Editor Tool/Psd2UIForm Editor", priority = 0)]
  349.         static void Psd2UIFormPrefabMenu()
  350.         {
  351.             if (Selection.activeObject == null) return;
  352.             var assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);
  353.             if (Path.GetExtension(assetPath).ToLower().CompareTo(".psd") != 0)
  354.             {
  355.                 Debug.LogWarning($"选择的文件({assetPath})不是psd格式, 工具只支持psd转换为UIForm");
  356.                 return;
  357.             }
  358.             string psdLayerPrefab = GetPsdLayerPrefabPath(assetPath);
  359.             if (!File.Exists(psdLayerPrefab))
  360.             {
  361.                 if (ParsePsd2LayerPrefab(assetPath))
  362.                 {
  363.                     OpenPsdLayerEditor(psdLayerPrefab);
  364.                 }
  365.             }
  366.             else
  367.             {
  368.                 OpenPsdLayerEditor(psdLayerPrefab);
  369.             }
  370.         }
  371.         public bool CheckPsdAssetHasChanged()
  372.         {
  373.             if (psdAsset == null) return false;
  374.             var fileTag = GetAssetChangeTag(PsdAssetName);
  375.             return psdAssetChangeTime.CompareTo(fileTag) != 0;
  376.         }
  377.         public static string GetAssetChangeTag(string fileName)
  378.         {
  379.             return new FileInfo(fileName).LastWriteTime.ToString("yyyyMMddHHmmss");
  380.         }
  381.         /// <summary>
  382.         /// 打开psd图层信息prefab
  383.         /// </summary>
  384.         /// <param name="psdLayerPrefab"></param>
  385.         public static void OpenPsdLayerEditor(string psdLayerPrefab)
  386.         {
  387.             PrefabStageUtility.OpenPrefab(psdLayerPrefab);
  388.         }
  389.         /// <summary>
  390.         /// 把Psd图层解析成节点prefab
  391.         /// </summary>
  392.         /// <param name="psdPath"></param>
  393.         /// <returns></returns>
  394.         public static bool ParsePsd2LayerPrefab(string psdFile, Psd2UIFormConverter instanceRoot = null)
  395.         {
  396.             if (!File.Exists(psdFile))
  397.             {
  398.                 Debug.LogError($"Error: Psd文件不存在:{psdFile}");
  399.                 return false;
  400.             }
  401.             var texImporter = AssetImporter.GetAtPath(psdFile) as TextureImporter;
  402.             if (texImporter.textureType != TextureImporterType.Sprite)
  403.             {
  404.                 texImporter.textureType = TextureImporterType.Sprite;
  405.                 texImporter.mipmapEnabled = false;
  406.                 texImporter.alphaIsTransparency = true;
  407.                 texImporter.SaveAndReimport();
  408.             }
  409.             var prefabFile = GetPsdLayerPrefabPath(psdFile);
  410.             var rootName = Path.GetFileNameWithoutExtension(prefabFile);
  411.             bool needDestroyInstance = instanceRoot == null;
  412.             if (instanceRoot != null)
  413.             {
  414.                 ParsePsdLayer2Root(psdFile, instanceRoot);
  415.                 instanceRoot.RefreshNodesBindLayer();
  416.                 return true;
  417.             }
  418.             else
  419.             {
  420.                 Psd2UIFormConverter rootLayer = CreatePsdLayerRoot(rootName);
  421.                 rootLayer.SetPsdAsset(psdFile);
  422.                 ParsePsdLayer2Root(psdFile, rootLayer);
  423.                 PrefabUtility.SaveAsPrefabAsset(rootLayer.gameObject, prefabFile, out bool savePrefabSuccess);
  424.                 if (needDestroyInstance) GameObject.DestroyImmediate(rootLayer.gameObject);
  425.                 AssetDatabase.Refresh();
  426.                 if (savePrefabSuccess && AssetDatabase.GUIDFromAssetPath(StageUtility.GetCurrentStage().assetPath) != AssetDatabase.GUIDFromAssetPath(prefabFile))
  427.                 {
  428.                     PrefabStageUtility.OpenPrefab(prefabFile);
  429.                 }
  430.                 return savePrefabSuccess;
  431.             }
  432.         }
  433.         private static void ParsePsdLayer2Root(string psdFile, Psd2UIFormConverter converter)
  434.         {
  435.             //清空已有节点重新解析
  436.             for (int i = converter.transform.childCount - 1; i >= 0; i--)
  437.             {
  438.                 GameObject.DestroyImmediate(converter.transform.GetChild(i).gameObject);
  439.             }
  440.             var psdOpts = new PsdLoadOptions()
  441.             {
  442.                 LoadEffectsResource = true,
  443.                 ReadOnlyMode = false
  444.             };
  445.             using (var psd = Aspose.PSD.Image.Load(psdFile, psdOpts) as PsdImage)
  446.             {
  447.                 List<GameObject> layerNodes = new List<GameObject> { converter.gameObject };
  448.                 for (int i = 0; i < psd.Layers.Length; i++)
  449.                 {
  450.                     var layer = psd.Layers[i];
  451.                     var curLayerType = layer.GetLayerType();
  452.                     if (curLayerType == PsdLayerType.SectionDividerLayer)
  453.                     {
  454.                         var layerGroup = (layer as SectionDividerLayer).GetRelatedLayerGroup();
  455.                         var layerGroupIdx = ArrayUtility.IndexOf(psd.Layers, layerGroup);
  456.                         var layerGropNode = CreatePsdLayerNode(layerGroup, layerGroupIdx);
  457.                         layerNodes.Add(layerGropNode.gameObject);
  458.                     }
  459.                     else if (curLayerType == PsdLayerType.LayerGroup)
  460.                     {
  461.                         var lastLayerNode = layerNodes.Last();
  462.                         layerNodes.Remove(lastLayerNode);
  463.                         if (layerNodes.Count > 0)
  464.                         {
  465.                             var parentLayerNode = layerNodes.Last();
  466.                             lastLayerNode.transform.SetParent(parentLayerNode.transform);
  467.                         }
  468.                     }
  469.                     else
  470.                     {
  471.                         var newLayerNode = CreatePsdLayerNode(layer, i);
  472.                         newLayerNode.transform.SetParent(layerNodes.Last().transform);
  473.                         newLayerNode.transform.localPosition = Vector3.zero;
  474.                     }
  475.                 }
  476.             }
  477.             converter.psdAssetChangeTime = GetAssetChangeTag(psdFile);
  478.             var childrenNodes = converter.GetComponentsInChildren<PsdLayerNode>(true);
  479.             foreach (var item in childrenNodes)
  480.             {
  481.                 item.RefreshUIHelper(false);
  482.             }
  483.             EditorUtility.SetDirty(converter.gameObject);
  484.         }
  485.         private void SetPsdAsset(string psdFile)
  486.         {
  487.             this.psdAsset = AssetDatabase.LoadAssetAtPath<UnityEngine.Sprite>(psdFile);
  488.             if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIImagesOutputDir))
  489.             {
  490.                 Psd2UIFormSettings.Instance.UIImagesOutputDir = Path.GetDirectoryName(psdFile);
  491.             }
  492.             if (string.IsNullOrWhiteSpace(this.uiFormName))
  493.             {
  494.                 this.uiFormName = this.psdAsset.name;
  495.             }
  496.         }
  497.         /// <summary>
  498.         /// 获取解析好的psd layers文件
  499.         /// </summary>
  500.         /// <param name="psd"></param>
  501.         /// <returns></returns>
  502.         public static string GetPsdLayerPrefabPath(string psd)
  503.         {
  504.             return UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(psd), Path.GetFileNameWithoutExtension(psd) + "_psd_layers_parsed.prefab");
  505.         }
  506.         private static Psd2UIFormConverter CreatePsdLayerRoot(string rootName)
  507.         {
  508.             var node = new GameObject(rootName);
  509.             node.gameObject.tag = "EditorOnly";
  510.             var layerRoot = node.AddComponent<Psd2UIFormConverter>();
  511.             return layerRoot;
  512.         }
  513.         private static PsdLayerNode CreatePsdLayerNode(Layer layer, int bindLayerIdx)
  514.         {
  515.             string nodeName = layer.Name;
  516.             if (string.IsNullOrWhiteSpace(nodeName))
  517.             {
  518.                 nodeName = $"PsdLayer-{bindLayerIdx}";
  519.             }
  520.             else
  521.             {
  522.                 if (Path.HasExtension(layer.Name))
  523.                 {
  524.                     nodeName = Path.GetFileNameWithoutExtension(layer.Name);
  525.                 }
  526.             }
  527.             var node = new GameObject(nodeName);
  528.             node.gameObject.tag = "EditorOnly";
  529.             var layerNode = node.AddComponent<PsdLayerNode>();
  530.             layerNode.BindPsdLayerIndex = bindLayerIdx;
  531.             InitLayerNodeData(layerNode, layer);
  532.             return layerNode;
  533.         }
  534.         /// <summary>
  535.         /// 根据psd图层信息解析并初始化图层UI类型、是否导出等信息
  536.         /// </summary>
  537.         /// <param name="layerNode"></param>
  538.         /// <param name="layer"></param>
  539.         private static void InitLayerNodeData(PsdLayerNode layerNode, Layer layer)
  540.         {
  541.             if (layer == null || layer.Disposed) return;
  542.             var layerTp = layer.GetLayerType();
  543.             layerNode.BindPsdLayer = layer;
  544.             if (UGUIParser.Instance.TryParse(layerNode, out var initRule))
  545.             {
  546.                 layerNode.SetUIType(initRule.UIType, false);
  547.             }
  548.             layerNode.markToExport = layerTp != PsdLayerType.LayerGroup && !(layerTp == PsdLayerType.TextLayer && layerNode.UIType.ToString().EndsWith("Text") && layerNode.UIType != GUIType.FillColor);
  549.             layerNode.gameObject.SetActive(layer.IsVisible);
  550.         }
  551.         /// <summary>
  552.         /// 导出psd图层为Sprites碎图
  553.         /// </summary>
  554.         /// <param name="psdAssetName"></param>
  555.         internal void ExportSprites()
  556.         {
  557.             //var pngOpts = new PngOptions()
  558.             //{
  559.             //    ColorType = Aspose.PSD.FileFormats.Png.PngColorType.Truecolor
  560.             //};
  561.             //this.psdInstance.Save("Assets/AAAGame/Sprites/UI/Preview.png", pngOpts);
  562.             //return;
  563.             var exportLayers = this.GetComponentsInChildren<PsdLayerNode>().Where(node => node.NeedExportImage());
  564.             var exportDir = GetUIFormImagesOutputDir();
  565.             if (!Directory.Exists(exportDir))
  566.             {
  567.                 Directory.CreateDirectory(exportDir);
  568.             }
  569.             int exportIdx = 0;
  570.             int totalCount = exportLayers.Count();
  571.             foreach (var layer in exportLayers)
  572.             {
  573.                 var assetName = layer.ExportImageAsset();
  574.                 if (assetName == null)
  575.                 {
  576.                     Debug.LogWarning($"导出图层[name:{layer.name}, layerIdx:{layer.BindPsdLayerIndex}]图片失败!");
  577.                 }
  578.                 ++exportIdx;
  579.                 EditorUtility.DisplayProgressBar($"导出进度({exportIdx}/{totalCount})", $"导出UI图片:{assetName}", exportIdx / (float)totalCount);
  580.             }
  581.             EditorUtility.ClearProgressBar();
  582.             AssetDatabase.Refresh();
  583.         }
  584.         /// <summary>
  585.         /// 根据解析后的节点树生成UIForm Prefab
  586.         /// </summary>
  587.         internal void GenerateUIForm()
  588.         {
  589.             if (Psd2UIFormSettings.Instance.UseUIFormOutputDir && string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir))
  590.             {
  591.                 Debug.LogError($"生成UIForm失败! UIForm导出路径为空:{Psd2UIFormSettings.Instance.UIFormOutputDir}");
  592.                 return;
  593.             }
  594.             if (Psd2UIFormSettings.Instance.UseUIFormOutputDir)
  595.             {
  596.                 ExportUIPrefab(Psd2UIFormSettings.Instance.UIFormOutputDir);
  597.             }
  598.             else
  599.             {
  600.                 string lastSaveDir = string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.LastUIFormOutputDir) ? "Assets" : Psd2UIFormSettings.Instance.LastUIFormOutputDir;
  601.                 string selectDir = EditorUtility.SaveFolderPanel("保存目录", lastSaveDir, null);
  602.                 if (!string.IsNullOrWhiteSpace(selectDir))
  603.                 {
  604.                     if (!selectDir.StartsWith("Assets/"))
  605.                         selectDir = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, selectDir);
  606.                     Psd2UIFormSettings.Instance.LastUIFormOutputDir = selectDir;
  607.                     ExportUIPrefab(selectDir);
  608.                 }
  609.             }
  610.         }
  611.         private bool ExportUIPrefab(string outputDir)
  612.         {
  613.             if (!string.IsNullOrWhiteSpace(outputDir))
  614.             {
  615.                 if (!Directory.Exists(outputDir))
  616.                 {
  617.                     try
  618.                     {
  619.                         Directory.CreateDirectory(outputDir);
  620.                         AssetDatabase.Refresh();
  621.                     }
  622.                     catch (Exception err)
  623.                     {
  624.                         Debug.LogError($"导出UI prefab失败:{err.Message}");
  625.                         return false;
  626.                     }
  627.                 }
  628.             }
  629.             if (string.IsNullOrWhiteSpace(uiFormName))
  630.             {
  631.                 Debug.LogError("导出UI Prefab失败: UI Form Name为空, 请填写UI Form Name.");
  632.                 return false;
  633.             }
  634.             var prefabName = UtilityBuiltin.ResPath.GetCombinePath(outputDir, $"{uiFormName}.prefab");
  635.             if (File.Exists(prefabName))
  636.             {
  637.                 if (!EditorUtility.DisplayDialog("警告", $"prefab文件已存在, 是否覆盖:{prefabName}", "覆盖生成", "取消生成"))
  638.                 {
  639.                     return false;
  640.                 }
  641.             }
  642.             var uiHelpers = GetAvailableUIHelpers();
  643.             if (uiHelpers == null || uiHelpers.Length < 1)
  644.             {
  645.                 return false;
  646.             }
  647.             var uiFormRoot = GameObject.Instantiate(UGUIParser.Instance.UIFormTemplate, Vector3.zero, Quaternion.identity);
  648.             uiFormRoot.name = uiFormName;
  649.             int curIdx = 0;
  650.             int totalCount = uiHelpers.Length;
  651.             foreach (var uiHelper in uiHelpers)
  652.             {
  653.                 EditorUtility.DisplayProgressBar($"生成UIFrom:({curIdx++}/{totalCount})", $"正在生成UI元素:{uiHelper.name}", curIdx /
  654.                 (float)totalCount);
  655.                 var uiElement = uiHelper.CreateUI();
  656.                 if (uiElement == null) continue;
  657.                 var goPath = GetGameObjectInstanceIdPath(uiHelper.gameObject, out var goNames);
  658.                 var parentNode = GetOrCreateNodeByInstanceIdPath(uiFormRoot, goPath, goNames);
  659.                 uiElement.transform.SetParent(parentNode.transform, true);
  660.                 uiElement.transform.position += new Vector3(this.UIFormCanvasSize.x * 0.5f, this.UIFormCanvasSize.y * 0.5f, 0);
  661.             }
  662.             var uiStrKeys = uiFormRoot.GetComponentsInChildren<UIStringKey>(true);
  663.             for (int i = uiStrKeys.Length - 1; i >= 0; i--)
  664.             {
  665.                 DestroyImmediate(uiStrKeys[i]);
  666.             }
  667.             
  668.             var uiPrefab = PrefabUtility.SaveAsPrefabAsset(uiFormRoot, prefabName, out bool saveSuccess);
  669.             if (saveSuccess)
  670.             {
  671.                 DestroyImmediate(uiFormRoot);
  672.                 Selection.activeGameObject = uiPrefab;
  673.             }
  674.             EditorUtility.ClearProgressBar();
  675.             return true;
  676.         }
  677.         private GameObject GetOrCreateNodeByInstanceIdPath(GameObject uiFormRoot, string[] goPath, string[] goNames)
  678.         {
  679.             GameObject result = uiFormRoot;
  680.             if (goPath != null && goNames != null)
  681.             {
  682.                 for (int i = 0; i < goPath.Length; i++)
  683.                 {
  684.                     var nodeId = goPath[i];
  685.                     var nodeName = goNames[i];
  686.                     GameObject targetNode = null;
  687.                     foreach (Transform child in result.transform)
  688.                     {
  689.                         if (child.gameObject == result) continue;
  690.                         var idKey = child.GetComponent<UIStringKey>();
  691.                         if (idKey != null && nodeId == idKey.Key)
  692.                         {
  693.                             targetNode = child.gameObject;
  694.                             break;
  695.                         }
  696.                     }
  697.                     if (targetNode == null)
  698.                     {
  699.                         targetNode = new GameObject(nodeName);
  700.                         targetNode.transform.SetParent(result.transform, false);
  701.                         targetNode.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
  702.                         var targetNodeKey = targetNode.GetOrAddComponent<UIStringKey>();
  703.                         targetNodeKey.Key = nodeId;
  704.                     }
  705.                     result = targetNode;
  706.                 }
  707.             }
  708.             return result;
  709.         }
  710.         private string[] GetGameObjectInstanceIdPath(GameObject go, out string[] names)
  711.         {
  712.             names = null;
  713.             if (go == null || go.transform.parent == null || go.transform.parent == this.transform) return null;
  714.             var parentGo = go.transform.parent;
  715.             string[] result = new string[1] { parentGo.gameObject.GetInstanceID().ToString() };
  716.             names = new string[1] { parentGo.gameObject.name };
  717.             while (parentGo.parent != null && parentGo.parent != this.transform)
  718.             {
  719.                 ArrayUtility.Insert(ref result, 0, parentGo.parent.gameObject.GetInstanceID().ToString());
  720.                 ArrayUtility.Insert(ref names, 0, parentGo.parent.gameObject.name);
  721.                 parentGo = parentGo.parent;
  722.             }
  723.             return result;
  724.         }
  725.         private UIHelperBase[] GetAvailableUIHelpers()
  726.         {
  727.             var uiHelpers = this.GetComponentsInChildren<UIHelperBase>();
  728.             uiHelpers = uiHelpers.Where(ui => ui.LayerNode.IsMainUIType).ToArray();
  729.             List<int> dependInstIds = new List<int>();
  730.             foreach (var item in uiHelpers)
  731.             {
  732.                 foreach (var depend in item.GetDependencies())
  733.                 {
  734.                     int dependId = depend.gameObject.GetInstanceID();
  735.                     if (!dependInstIds.Contains(dependId))
  736.                     {
  737.                         dependInstIds.Add(dependId);
  738.                     }
  739.                 }
  740.             }
  741.             for (int i = uiHelpers.Length - 1; i >= 0; i--)
  742.             {
  743.                 var uiHelper = uiHelpers[i];
  744.                 if (dependInstIds.Contains(uiHelper.gameObject.GetInstanceID()))
  745.                 {
  746.                     ArrayUtility.RemoveAt(ref uiHelpers, i);
  747.                 }
  748.             }
  749.             return uiHelpers;
  750.         }
  751.         /// <summary>
  752.         /// 把图片设置为为Sprite或Texture类型
  753.         /// </summary>
  754.         /// <param name="dir"></param>
  755.         public static void ConvertTexturesType(string[] texAssets, bool isImage = true)
  756.         {
  757.             foreach (var item in texAssets)
  758.             {
  759.                 var texImporter = AssetImporter.GetAtPath(item) as TextureImporter;
  760.                 if (texImporter == null)
  761.                 {
  762.                     Debug.LogError($"TextureImporter为空:{item}");
  763.                     continue;
  764.                 }
  765.                 if (isImage)
  766.                 {
  767.                     texImporter.textureType = TextureImporterType.Sprite;
  768.                     texImporter.spriteImportMode = SpriteImportMode.Single;
  769.                     texImporter.alphaSource = TextureImporterAlphaSource.FromInput;
  770.                     texImporter.alphaIsTransparency = true;
  771.                     texImporter.mipmapEnabled = false;
  772.                 }
  773.                 else
  774.                 {
  775.                     texImporter.textureType = TextureImporterType.Default;
  776.                     texImporter.textureShape = TextureImporterShape.Texture2D;
  777.                     texImporter.alphaSource = TextureImporterAlphaSource.FromInput;
  778.                     texImporter.alphaIsTransparency = true;
  779.                     texImporter.mipmapEnabled = false;
  780.                 }
  781.                 texImporter.SaveAndReimport();
  782.             }
  783.         }
  784.         /// <summary>
  785.         /// 压缩图片文件
  786.         /// </summary>
  787.         /// <param name="asset">文件名(相对路径Assets)</param>
  788.         /// <returns></returns>
  789.         public static bool CompressImageFile(string asset)
  790.         {
  791.             var assetPath = asset.StartsWith("Assets/") ? Path.GetFullPath(asset, Directory.GetParent(Application.dataPath).FullName) : asset;
  792.             var compressTool = Utility.Assembly.GetType("UGF.EditorTools.CompressTool");
  793.             if (compressTool == null) return false;
  794.             var compressMethod = compressTool.GetMethod("CompressImageOffline", new Type[] { typeof(string), typeof(string) });
  795.             if (compressMethod == null) return false;
  796.             return (bool)compressMethod.Invoke(null, new object[] { assetPath, assetPath });
  797.         }
  798.         /// <summary>
  799.         /// 获取UIForm对应的图片导出目录
  800.         /// </summary>
  801.         /// <returns></returns>
  802.         public string GetUIFormImagesOutputDir()
  803.         {
  804.             return UtilityBuiltin.ResPath.GetCombinePath(Psd2UIFormSettings.Instance.UIImagesOutputDir, uiFormName);
  805.         }
  806.         public SmartObjectLayer ConvertToSmartObjectLayer(Layer layer)
  807.         {
  808.             var smartObj = psdInstance.SmartObjectProvider.ConvertToSmartObject(new Layer[] { layer });
  809.             return smartObj;
  810.         }
  811.     }
  812. }
  813. #endif
复制代码
7. 图层节点编辑器扩展,提供导出图片按钮以便单独导出选择图层,UI范例切换时自动添加对应的Helper剖析器并自动绑定子UI

  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. using Aspose.PSD.FileFormats.Psd.Layers;
  4. using Aspose.PSD.ImageOptions;
  5. using UnityEditor;
  6. using System.IO;
  7. using System.Linq;
  8. using Aspose.PSD.FileFormats.Psd;
  9. using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;
  10. using GameFramework;
  11. namespace UGF.EditorTools.Psd2UGUI
  12. {
  13.     [CanEditMultipleObjects]
  14.     [CustomEditor(typeof(PsdLayerNode))]
  15.     public class PsdLayerNodeInspector : Editor
  16.     {
  17.         PsdLayerNode targetLogic;
  18.         private void OnEnable()
  19.         {
  20.             targetLogic = target as PsdLayerNode;
  21.             targetLogic.RefreshLayerTexture();
  22.         }
  23.         public override void OnInspectorGUI()
  24.         {
  25.             serializedObject.Update();
  26.             base.OnInspectorGUI();
  27.             EditorGUI.BeginChangeCheck();
  28.             {
  29.                 targetLogic.UIType = (GUIType)EditorGUILayout.EnumPopup("UI Type", targetLogic.UIType);
  30.                 if (EditorGUI.EndChangeCheck())
  31.                 {
  32.                     targetLogic.SetUIType(targetLogic.UIType);
  33.                 }
  34.             }
  35.             EditorGUILayout.BeginHorizontal();
  36.             {
  37.                 if (GUILayout.Button("导出图片"))
  38.                 {
  39.                     foreach (var item in targets)
  40.                     {
  41.                         if (item == null) continue;
  42.                         (item as PsdLayerNode)?.ExportImageAsset();
  43.                     }
  44.                 }
  45.                 EditorGUILayout.EndHorizontal();
  46.             }
  47.             serializedObject.ApplyModifiedProperties();
  48.         }
  49.         public override bool HasPreviewGUI()
  50.         {
  51.             var layerNode = (target as PsdLayerNode);
  52.             return layerNode != null && layerNode.PreviewTexture != null;
  53.         }
  54.         public override void OnPreviewGUI(Rect r, GUIStyle background)
  55.         {
  56.             var layerNode = (target as PsdLayerNode);
  57.             GUI.DrawTexture(r, layerNode.PreviewTexture, ScaleMode.ScaleToFit);
  58.             //base.OnPreviewGUI(r, background);
  59.         }
  60.         public override string GetInfoString()
  61.         {
  62.             var layerNode = (target as PsdLayerNode);
  63.             return layerNode.LayerInfo;
  64.         }
  65.     }
  66.     [ExecuteInEditMode]
  67.     [DisallowMultipleComponent]
  68.     public class PsdLayerNode : MonoBehaviour
  69.     {
  70.         [ReadOnlyField] public int BindPsdLayerIndex = -1;
  71.         [ReadOnlyField][SerializeField] PsdLayerType mLayerType = PsdLayerType.Unknown;
  72.         [SerializeField] public bool markToExport;
  73.         [HideInInspector] public GUIType UIType;
  74.         public Texture2D PreviewTexture { get; private set; }
  75.         public string LayerInfo { get; private set; }
  76.         public Rect LayerRect { get; private set; }
  77.         public PsdLayerType LayerType { get => mLayerType; }
  78.         public bool IsMainUIType => UGUIParser.IsMainUIType(UIType);
  79.         /// <summary>
  80.         /// 绑定的psd图层
  81.         /// </summary>
  82.         private Layer mBindPsdLayer;
  83.         public Layer BindPsdLayer
  84.         {
  85.             get => mBindPsdLayer;
  86.             set
  87.             {
  88.                 mBindPsdLayer = value;
  89.                 mLayerType = mBindPsdLayer.GetLayerType();
  90.                 //if (IsTextLayer(out var txtLayer) && !txtLayer.TextBoundBox.IsEmpty)
  91.                 //{
  92.                 //    LayerRect = AsposePsdExtension.PsdRect2UnityRect(txtLayer.TextBoundBox, Psd2UIFormConverter.Instance.UIFormCanvasSize);
  93.                 //}
  94.                 //else
  95.                 {
  96.                     LayerRect = mBindPsdLayer.GetLayerRect();
  97.                 }
  98.                 LayerInfo = $"{LayerRect}";
  99.             }
  100.         }
  101.         private void OnDestroy()
  102.         {
  103.             if (PreviewTexture != null)
  104.             {
  105.                 DestroyImmediate(PreviewTexture);
  106.             }
  107.         }
  108.         public void SetUIType(GUIType uiType, bool triggerParseFunc = true)
  109.         {
  110.             this.UIType = uiType;
  111.             RemoveUIHelper();
  112.             if (triggerParseFunc)
  113.             {
  114.                 RefreshUIHelper(true);
  115.             }
  116.         }
  117.         public void RefreshUIHelper(bool refreshParent = false)
  118.         {
  119.             if (UIType == GUIType.Null) return;
  120.             var uiHelperTp = UGUIParser.Instance.GetHelperType(UIType);
  121.             if (uiHelperTp != null)
  122.             {
  123.                 var helper = gameObject.GetOrAddComponent(uiHelperTp) as UIHelperBase;
  124.                 helper.ParseAndAttachUIElements();
  125.             }
  126.             if (refreshParent)
  127.             {
  128.                 var parentHelper = transform.parent?.GetComponent<UIHelperBase>();
  129.                 parentHelper?.ParseAndAttachUIElements();
  130.             }
  131.             EditorUtility.SetDirty(this);
  132.         }
  133.         private void RemoveUIHelper()
  134.         {
  135.             var uiHelpers = this.GetComponents<UIHelperBase>();
  136.             if (uiHelpers != null)
  137.             {
  138.                 foreach (var uiHelper in uiHelpers)
  139.                 {
  140.                     DestroyImmediate(uiHelper);
  141.                 }
  142.             }
  143.             EditorUtility.SetDirty(this);
  144.         }
  145.         /// <summary>
  146.         /// 是否需要导出此图层
  147.         /// </summary>
  148.         /// <returns></returns>
  149.         public bool NeedExportImage()
  150.         {
  151.             return gameObject.activeSelf && markToExport;
  152.         }
  153.         /// <summary>
  154.         /// 导出图片
  155.         /// </summary>
  156.         /// <param name="forceSpriteType">强制贴图类型为Sprite</param>
  157.         /// <returns></returns>
  158.         public string ExportImageAsset(bool forceSpriteType = false)
  159.         {
  160.             string assetName = null;
  161.             if (this.RefreshLayerTexture())
  162.             {
  163.                 var bytes = PreviewTexture.EncodeToPNG();
  164.                 var imgName = Utility.Text.Format("{0}_{1}", string.IsNullOrWhiteSpace(name) ? UIType : name, BindPsdLayerIndex);
  165.                 var exportDir = Psd2UIFormConverter.Instance.GetUIFormImagesOutputDir();
  166.                 if (!Directory.Exists(exportDir))
  167.                 {
  168.                     try
  169.                     {
  170.                         Directory.CreateDirectory(exportDir);
  171.                         AssetDatabase.Refresh();
  172.                     }
  173.                     catch (System.Exception)
  174.                     {
  175.                         return null;
  176.                     }
  177.                 }
  178.                 var imgFileName = UtilityBuiltin.ResPath.GetCombinePath(exportDir, imgName + ".png");
  179.                 File.WriteAllBytes(imgFileName, bytes);
  180.                 if (Psd2UIFormSettings.Instance.CompressImage)
  181.                 {
  182.                     bool compressResult = Psd2UIFormConverter.CompressImageFile(imgFileName);
  183.                     if (compressResult)
  184.                     {
  185.                         Debug.Log($"成功压缩图片:{imgFileName}");
  186.                     }
  187.                     else
  188.                     {
  189.                         Debug.LogWarning($"压缩图片失败:{imgFileName}");
  190.                     }
  191.                 }
  192.                 assetName = imgFileName;
  193.                 bool isImage = !(this.UIType == GUIType.FillColor || this.UIType == GUIType.RawImage);
  194.                 AssetDatabase.Refresh();
  195.                 Psd2UIFormConverter.ConvertTexturesType(new string[] { imgFileName }, isImage || forceSpriteType);
  196.             }
  197.             return assetName;
  198.         }
  199.         public bool RefreshLayerTexture(bool forceRefresh = false)
  200.         {
  201.             if (!forceRefresh && PreviewTexture != null)
  202.             {
  203.                 return true;
  204.             }
  205.             if (BindPsdLayer == null || BindPsdLayer.Disposed) return false;
  206.             var pngOpt = new PngOptions
  207.             {
  208.                 ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha
  209.             };
  210.             if (BindPsdLayer.CanSave(pngOpt))
  211.             {
  212.                 if (PreviewTexture != null)
  213.                 {
  214.                     DestroyImmediate(PreviewTexture);
  215.                 }
  216.                 PreviewTexture = this.ConvertPsdLayer2Texture2D();
  217.             }
  218.             return PreviewTexture != null;
  219.         }
  220.         /// <summary>
  221.         /// 把psd图层转成Texture2D
  222.         /// </summary>
  223.         /// <param name="psdLayer"></param>
  224.         /// <returns>Texture2D</returns>
  225.         public Texture2D ConvertPsdLayer2Texture2D()
  226.         {
  227.             if (BindPsdLayer == null || BindPsdLayer.Disposed) return null;
  228.             MemoryStream ms = new MemoryStream();
  229.             var pngOpt = new Aspose.PSD.ImageOptions.PngOptions()
  230.             {
  231.                 ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha,
  232.                 FullFrame = true
  233.             };
  234.             if (BindPsdLayer.Opacity >= 255 || LayerType == PsdLayerType.LayerGroup)
  235.             {
  236.                 BindPsdLayer.Save(ms, pngOpt);
  237.             }
  238.             else
  239.             {
  240.                 var smartLayer = Psd2UIFormConverter.Instance.ConvertToSmartObjectLayer(BindPsdLayer);
  241.                 smartLayer.Save(ms, pngOpt);
  242.             }
  243.             //var bitmap = BindPsdLayer.ToBitmap();
  244.             //bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
  245.             var buffer = new byte[ms.Length];
  246.             ms.Position = 0;
  247.             ms.Read(buffer, 0, buffer.Length);
  248.             Texture2D texture = new Texture2D(BindPsdLayer.Width, BindPsdLayer.Height);
  249.             texture.alphaIsTransparency = true;
  250.             texture.LoadImage(buffer);
  251.             texture.Apply();
  252.             ms.Dispose();
  253.             return texture;
  254.         }
  255.         /// <summary>
  256.         /// 从第一层子节点按类型查找LayerNode
  257.         /// </summary>
  258.         /// <param name="uiTp"></param>
  259.         /// <returns></returns>
  260.         public PsdLayerNode FindSubLayerNode(GUIType uiTp)
  261.         {
  262.             for (int i = 0; i < transform.childCount; i++)
  263.             {
  264.                 var child = transform.GetChild(i)?.GetComponent<PsdLayerNode>();
  265.                 if (child != null && child.UIType == uiTp) return child;
  266.             }
  267.             return null;
  268.         }
  269.         /// <summary>
  270.         /// 依次查找给定多个类型,返回最先找到的类型
  271.         /// </summary>
  272.         /// <param name="uiTps"></param>
  273.         /// <returns></returns>
  274.         public PsdLayerNode FindSubLayerNode(params GUIType[] uiTps)
  275.         {
  276.             foreach (var tp in uiTps)
  277.             {
  278.                 var result = FindSubLayerNode(tp);
  279.                 if (result != null) return result;
  280.             }
  281.             return null;
  282.         }
  283.         public PsdLayerNode FindLayerNodeInChildren(GUIType uiTp)
  284.         {
  285.             var layers = GetComponentsInChildren<PsdLayerNode>(true);
  286.             if (layers != null && layers.Length > 0)
  287.             {
  288.                 return layers.FirstOrDefault(layer => layer.UIType == uiTp);
  289.             }
  290.             return null;
  291.         }
  292.         /// <summary>
  293.         /// 判断该图层是否为文本图层
  294.         /// </summary>
  295.         /// <param name="layer"></param>
  296.         /// <returns></returns>
  297.         public bool IsTextLayer(out TextLayer layer)
  298.         {
  299.             layer = null;
  300.             if (BindPsdLayer == null) return false;
  301.             if (BindPsdLayer is SmartObjectLayer smartLayer)
  302.             {
  303.                 layer = smartLayer.GetSmartObjectInnerTextLayer() as TextLayer;
  304.                 return layer != null;
  305.             }
  306.             else if (BindPsdLayer is TextLayer txtLayer)
  307.             {
  308.                 layer = txtLayer;
  309.                 return layer != null;
  310.             }
  311.             return false;
  312.         }
  313.         internal void InitPsdLayers(PsdImage psdInstance)
  314.         {
  315.             BindPsdLayer = psdInstance.Layers[BindPsdLayerIndex];
  316.         }
  317.         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)
  318.         {
  319.             text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;
  320.             if (IsTextLayer(out var txtLayer))
  321.             {
  322.                 text = txtLayer.Text;
  323.                 fontSize = (int)txtLayer.Font.Size;
  324.                 fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;
  325.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  326.                 {
  327.                     fontStyle = UnityEngine.FontStyle.BoldAndItalic;
  328.                 }
  329.                 else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
  330.                 {
  331.                     fontStyle = UnityEngine.FontStyle.Bold;
  332.                 }
  333.                 else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  334.                 {
  335.                     fontStyle = UnityEngine.FontStyle.Italic;
  336.                 }
  337.                 else
  338.                 {
  339.                     fontStyle = UnityEngine.FontStyle.Normal;
  340.                 }
  341.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  342.                 {
  343.                     tmpFontStyle |= TMPro.FontStyles.Italic;
  344.                 }
  345.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
  346.                 {
  347.                     tmpFontStyle |= TMPro.FontStyles.Bold;
  348.                 }
  349.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline))
  350.                 {
  351.                     tmpFontStyle |= TMPro.FontStyles.Underline;
  352.                 }
  353.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout))
  354.                 {
  355.                     tmpFontStyle |= TMPro.FontStyles.Strikethrough;
  356.                 }
  357.                 fontName = txtLayer.Font.Name;
  358.                 if (txtLayer.TextData.Items.Length > 0)
  359.                 {
  360.                     var txtData = txtLayer.TextData.Items[0];
  361.                     characterSpace = txtData.Style.Tracking * 0.1f;
  362.                     lineSpace = (float)txtData.Style.Leading * 0.1f;
  363.                 }
  364.                 return true;
  365.             }
  366.             return false;
  367.         }
  368.     }
  369. }
  370. #endif
复制代码
五、UI元素剖析/天生器Helper

界说HelperBase剖析器基类,差别的UI范例重写UI初始化方法,如需支持新的UI范例可以很方便进行扩展支持:
  1. #if UNITY_EDITOR
  2. using System.Collections.Generic;
  3. using UnityEditor;
  4. using UnityEngine;
  5. using UnityGameFramework.Runtime;
  6. namespace UGF.EditorTools.Psd2UGUI
  7. {
  8.     public abstract class UIHelperBase : MonoBehaviour
  9.     {
  10.         public PsdLayerNode LayerNode => this.GetComponent<PsdLayerNode>();
  11.         private void OnEnable()
  12.         {
  13.             ParseAndAttachUIElements();
  14.         }
  15.         /// <summary>
  16.         /// 解析并关联UI元素,并且返回已经关联过的图层(已关联图层不再处理)
  17.         /// </summary>
  18.         /// <param name="layerNode"></param>
  19.         /// <returns></returns>
  20.         public abstract void ParseAndAttachUIElements();
  21.         /// <summary>
  22.         /// 获取UI依赖的LayerNodes
  23.         /// </summary>
  24.         /// <returns></returns>
  25.         public abstract PsdLayerNode[] GetDependencies();
  26.         /// <summary>
  27.         /// 把UI实例进行UI元素初始化
  28.         /// </summary>
  29.         /// <param name="uiRoot"></param>
  30.         protected abstract void InitUIElements(GameObject uiRoot);
  31.         /// <summary>
  32.         /// 筛选出UI依赖的非空LayerNode
  33.         /// </summary>
  34.         /// <param name="nodes"></param>
  35.         /// <returns></returns>
  36.         protected PsdLayerNode[] CalculateDependencies(params PsdLayerNode[] nodes)
  37.         {
  38.             if (nodes == null || nodes.Length == 0) return null;
  39.             for (int i = nodes.Length - 1; i >= 0; i--)
  40.             {
  41.                 var node = nodes[i];
  42.                 if (node == null || node == LayerNode) ArrayUtility.RemoveAt(ref nodes, i);
  43.             }
  44.             return nodes;
  45.         }
  46.         internal GameObject CreateUI(GameObject uiInstance = null)
  47.         {
  48.             if ((int)this.LayerNode.UIType > UGUIParser.UITYPE_MAX || LayerNode.UIType == GUIType.Null) return null;
  49.             if (uiInstance == null)
  50.             {
  51.                 var rule = UGUIParser.Instance.GetRule(this.LayerNode.UIType);
  52.                 if (rule == null || rule.UIPrefab == null)
  53.                 {
  54.                     Debug.LogWarning($"创建UI类型{LayerNode.UIType}失败:Rule配置项不存在或UIPrefab为空");
  55.                     return null;
  56.                 }
  57.                 uiInstance = GameObject.Instantiate(rule.UIPrefab, Vector3.zero, Quaternion.identity);
  58.                 if (LayerNode.IsMainUIType)
  59.                 {
  60.                     uiInstance.name = this.name;
  61.                     var key = uiInstance.GetOrAddComponent<UIStringKey>();
  62.                     key.Key = this.gameObject.GetInstanceID().ToString();
  63.                 }
  64.             }
  65.             
  66.             InitUIElements(uiInstance);
  67.             return uiInstance;
  68.         }
  69.     }
  70. }
  71. #endif
复制代码
1. Text剖析器:
  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. namespace UGF.EditorTools.Psd2UGUI
  4. {
  5.     [DisallowMultipleComponent]
  6.     public class TextHelper : UIHelperBase
  7.     {
  8.         [SerializeField] PsdLayerNode text;
  9.         public override PsdLayerNode[] GetDependencies()
  10.         {
  11.             return CalculateDependencies(text);
  12.         }
  13.         public override void ParseAndAttachUIElements()
  14.         {
  15.             if (LayerNode.IsTextLayer(out var _))
  16.             {
  17.                 text = LayerNode;
  18.             }
  19.             else
  20.             {
  21.                 LayerNode.SetUIType(UGUIParser.Instance.DefaultImage);
  22.             }
  23.         }
  24.         protected override void InitUIElements(GameObject uiRoot)
  25.         {
  26.             var textCom = uiRoot.GetComponentInChildren<UnityEngine.UI.Text>();
  27.             UGUIParser.SetTextStyle(text, textCom);
  28.             UGUIParser.SetRectTransform(text, textCom);
  29.         }
  30.     }
  31. }
  32. #endif
复制代码
从ps文本图层获取文本字体、字号、颜色、字间距等信息,然后从Unity工程中查找对应的字体文件并赋值给Text组件:
  1. 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)
  2.         {
  3.             text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;
  4.             if (IsTextLayer(out var txtLayer))
  5.             {
  6.                 text = txtLayer.Text;
  7.                 fontSize = (int)txtLayer.Font.Size;
  8.                 fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;
  9.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  10.                 {
  11.                     fontStyle = UnityEngine.FontStyle.BoldAndItalic;
  12.                 }
  13.                 else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
  14.                 {
  15.                     fontStyle = UnityEngine.FontStyle.Bold;
  16.                 }
  17.                 else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  18.                 {
  19.                     fontStyle = UnityEngine.FontStyle.Italic;
  20.                 }
  21.                 else
  22.                 {
  23.                     fontStyle = UnityEngine.FontStyle.Normal;
  24.                 }
  25.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))
  26.                 {
  27.                     tmpFontStyle |= TMPro.FontStyles.Italic;
  28.                 }
  29.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))
  30.                 {
  31.                     tmpFontStyle |= TMPro.FontStyles.Bold;
  32.                 }
  33.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline))
  34.                 {
  35.                     tmpFontStyle |= TMPro.FontStyles.Underline;
  36.                 }
  37.                 if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout))
  38.                 {
  39.                     tmpFontStyle |= TMPro.FontStyles.Strikethrough;
  40.                 }
  41.                 fontName = txtLayer.Font.Name;
  42.                 if (txtLayer.TextData.Items.Length > 0)
  43.                 {
  44.                     var txtData = txtLayer.TextData.Items[0];
  45.                     characterSpace = txtData.Style.Tracking * 0.1f;
  46.                     lineSpace = (float)txtData.Style.Leading * 0.1f;
  47.                 }
  48.                 return true;
  49.             }
  50.             return false;
  51.         }
复制代码
根据字体内部名查找ttf字体和TextMeshPro字体资源:
  1. /// <summary>
  2.         /// 根据字体名查找TMP_FontAsset
  3.         /// </summary>
  4.         /// <param name="fontName"></param>
  5.         /// <returns></returns>
  6.         public static TMP_FontAsset FindTMPFontAsset(string fontName)
  7.         {
  8.             var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");
  9.             foreach (var guid in fontGuids)
  10.             {
  11.                 var fontPath = AssetDatabase.GUIDToAssetPath(guid);
  12.                 var font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(fontPath);
  13.                 if (font != null && font.faceInfo.familyName == fontName)
  14.                 {
  15.                     return font;
  16.                 }
  17.             }
  18.             return null;
  19.         }
  20.         /// <summary>
  21.         /// 根据字体名查找Font Asset
  22.         /// </summary>
  23.         /// <param name="fontName"></param>
  24.         /// <returns></returns>
  25.         public static UnityEngine.Font FindFontAsset(string fontName)
  26.         {
  27.             var fontGuids = AssetDatabase.FindAssets("t:font");
  28.             foreach (var guid in fontGuids)
  29.             {
  30.                 var fontPath = AssetDatabase.GUIDToAssetPath(guid);
  31.                 var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;
  32.                 if (font != null && font.fontTTFName == fontName)
  33.                 {
  34.                     return AssetDatabase.LoadAssetAtPath<UnityEngine.Font>(fontPath);
  35.                 }
  36.             }
  37.             return null;
  38.         }
复制代码
2. Image剖析器:
  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. namespace UGF.EditorTools.Psd2UGUI
  4. {
  5.     [DisallowMultipleComponent]
  6.     public class ImageHelper : UIHelperBase
  7.     {
  8.         [SerializeField] PsdLayerNode image;
  9.         public override PsdLayerNode[] GetDependencies()
  10.         {
  11.             return CalculateDependencies(image);
  12.         }
  13.         public override void ParseAndAttachUIElements()
  14.         {
  15.             image = LayerNode;
  16.         }
  17.         protected override void InitUIElements(GameObject uiRoot)
  18.         {
  19.             var imgCom = uiRoot.GetComponentInChildren<UnityEngine.UI.Image>();
  20.             UGUIParser.SetRectTransform(image,imgCom);
  21.             imgCom.sprite = UGUIParser.LayerNode2Sprite(image, imgCom.type == UnityEngine.UI.Image.Type.Sliced);
  22.         }
  23.     }
  24. }
  25. #endif
复制代码
自动把ps图层导出为Sprite资源,若Image为Sliced模式则自动盘算并设置Sprite 9宫边界:
  1. /// <summary>
  2.         /// 把LayerNode图片保存到本地并返回
  3.         /// </summary>
  4.         /// <param name="layerNode"></param>
  5.         /// <param name="auto9Slice">若没有设置Sprite的九宫,是否自动计算并设置九宫</param>
  6.         /// <returns></returns>
  7.         public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false)
  8.         {
  9.             if (layerNode != null)
  10.             {
  11.                 var spAssetName = layerNode.ExportImageAsset(true);
  12.                 var sprite = AssetDatabase.LoadAssetAtPath<Sprite>(spAssetName);
  13.                 if (sprite != null)
  14.                 {
  15.                     if (auto9Slice)
  16.                     {
  17.                         var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;
  18.                         var rawReadable = spImpt.isReadable;
  19.                         if (!rawReadable)
  20.                         {
  21.                             spImpt.isReadable = true;
  22.                             spImpt.SaveAndReimport();
  23.                         }
  24.                         if (spImpt.spriteBorder == Vector4.zero)
  25.                         {
  26.                             spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);
  27.                             spImpt.isReadable = rawReadable;
  28.                             spImpt.SaveAndReimport();
  29.                         }
  30.                     }
  31.                     return sprite;
  32.                 }
  33.             }
  34.             return null;
  35.         }
复制代码
根据图片的Alpha通道盘算出9宫边界,通常设置9宫边界还会思量图片纹理因素,但程序难以智能辨认,这里自动9宫只是实用于普通环境,还需要根据实际效果进行手动调解:
  1. /// <summary>
  2.         /// 自动计算贴图的 9宫 Border
  3.         /// </summary>
  4.         /// <param name="texture"></param>
  5.         /// <param name="alphaThreshold">0-255</param>
  6.         /// <returns></returns>
  7.         public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3)
  8.         {
  9.             int width = texture.width;
  10.             int height = texture.height;
  11.             Color32[] pixels = texture.GetPixels32();
  12.             int minX = width;
  13.             int minY = height;
  14.             int maxX = 0;
  15.             int maxY = 0;
  16.             // 寻找不透明像素的最小和最大边界
  17.             for (int y = 0; y < height; y++)
  18.             {
  19.                 for (int x = 0; x < width; x++)
  20.                 {
  21.                     int pixelIndex = y * width + x;
  22.                     Color32 pixel = pixels[pixelIndex];
  23.                     if (pixel.a >= alphaThreshold)
  24.                     {
  25.                         minX = Mathf.Min(minX, x);
  26.                         minY = Mathf.Min(minY, y);
  27.                         maxX = Mathf.Max(maxX, x);
  28.                         maxY = Mathf.Max(maxY, y);
  29.                     }
  30.                 }
  31.             }
  32.             // 计算最优的borderSize
  33.             int borderSizeX = (maxX - minX) / 3;
  34.             int borderSizeY = (maxY - minY) / 3;
  35.             int borderSize = Mathf.Min(borderSizeX, borderSizeY);
  36.             // 根据边界和Border Size计算Nine Slice Border
  37.             int left = minX + borderSize;
  38.             int right = maxX - borderSize;
  39.             int top = minY + borderSize;
  40.             int bottom = maxY - borderSize;
  41.             // 确保边界在纹理范围内
  42.             left = Mathf.Clamp(left, 0, width - 1);
  43.             right = Mathf.Clamp(right, 0, width - 1);
  44.             top = Mathf.Clamp(top, 0, height - 1);
  45.             bottom = Mathf.Clamp(bottom, 0, height - 1);
  46.             return new Vector4(left, top, width - right, height - bottom);
  47.         }
复制代码
3. Dropdown剖析器,对于多种元素构成的复合型、嵌套型UI,可以很好的支持,而且可以任意嵌套组合,没有限定和束缚。比方Dropdown内包含了一个ScrollView和一个Toggle范例的Item,就可以直接用ScrollView Helper和Toggle Helper分别对其剖析:
  1. #if UNITY_EDITOR
  2. using UnityEngine;
  3. using UnityEngine.UI;
  4. namespace UGF.EditorTools.Psd2UGUI
  5. {
  6.     [DisallowMultipleComponent]
  7.     public class DropdownHelper : UIHelperBase
  8.     {
  9.         [SerializeField] PsdLayerNode background;
  10.         [SerializeField] PsdLayerNode label;
  11.         [SerializeField] PsdLayerNode arrow;
  12.         [SerializeField] PsdLayerNode scrollView;
  13.         [SerializeField] PsdLayerNode toggleItem;
  14.         public override PsdLayerNode[] GetDependencies()
  15.         {
  16.             return CalculateDependencies(background, label, arrow, scrollView, toggleItem);
  17.         }
  18.         public override void ParseAndAttachUIElements()
  19.         {
  20.             background = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);
  21.             label = LayerNode.FindSubLayerNode(GUIType.Dropdown_Label, GUIType.Text, GUIType.TMPText);
  22.             arrow = LayerNode.FindSubLayerNode(GUIType.Dropdown_Arrow);
  23.             scrollView = LayerNode.FindSubLayerNode(GUIType.ScrollView);
  24.             toggleItem = LayerNode.FindSubLayerNode(GUIType.Toggle);
  25.         }
  26.         protected override void InitUIElements(GameObject uiRoot)
  27.         {
  28.             var dpd = uiRoot.GetComponent<Dropdown>();
  29.             UGUIParser.SetRectTransform(background, dpd);
  30.             var bgImg = dpd.targetGraphic as Image;
  31.             bgImg.sprite = UGUIParser.LayerNode2Sprite(background, bgImg.type == Image.Type.Sliced) ?? bgImg.sprite;
  32.             UGUIParser.SetTextStyle(label, dpd.captionText);
  33.             UGUIParser.SetRectTransform(label, dpd.captionText);
  34.             var arrowImg = dpd.transform.Find("Arrow")?.GetComponent<Image>();
  35.             if (arrowImg != null)
  36.             {
  37.                 UGUIParser.SetRectTransform(arrow, arrowImg);
  38.                 arrowImg.sprite = UGUIParser.LayerNode2Sprite(arrow, arrowImg.type == Image.Type.Sliced);
  39.             }
  40.             if (scrollView != null)
  41.             {
  42.                 var svTmp = uiRoot.GetComponentInChildren<ScrollRect>(true).GetComponent<RectTransform>();
  43.                 if (svTmp != null)
  44.                 {
  45.                     var sViewGo = scrollView.GetComponent<ScrollViewHelper>()?.CreateUI(svTmp.gameObject);
  46.                     if (sViewGo != null)
  47.                     {
  48.                         var sViewRect = sViewGo.GetComponent<RectTransform>();
  49.                         UGUIParser.SetRectTransform(scrollView, sViewRect);
  50.                         sViewRect.anchorMin = Vector2.zero;
  51.                         sViewRect.anchorMax = new Vector2(1, 0);
  52.                         sViewRect.anchoredPosition = new Vector2(0, -2);
  53.                     }
  54.                     if (toggleItem != null)
  55.                     {
  56.                         var itemTmp = dpd.itemText != null ? dpd.itemText.transform.parent : null;
  57.                         if (itemTmp != null)
  58.                         {
  59.                             toggleItem.GetComponent<ToggleHelper>()?.CreateUI(itemTmp.gameObject);
  60.                         }
  61.                     }
  62.                 }
  63.             }
  64.         }
  65.     }
  66. }
  67. #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
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则