解放UI程序/美术? psd文件一键转ui prefab 支持所有ui类型 支持textmeshpro

psd一键转ugui prefab工具 设计原理和详细使用方法

工具效果:

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

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

 验证一键生成UI效果:

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

先上总结:

工具包含的功能:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

buttonBackground = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);

buttonText = LayerNode.FindSubLayerNode(GUIType.Button_Text, GUIType.Text, GUIType.TMPText);

二、解析规则配置

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

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

 UIPrefab: UI模板预制体。

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

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

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

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

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

使用说明:

单元素UI:即单个图层的UI,如Image、Text、单图Button,可以直接在图层命名结尾加上".类型"来标记UI类型。 如"A.btn"表示按钮。 多元素UI: 对于多个图片组成的复合型UI,可以通过使用"组"包裹多个UI元素。在“组”命名结尾加上".类型"来标记UI类型。 组里的图层命名后夹".类型"来标记为UI子元素类型。 各种UI类型支持任意组合:如一个组类型标记为Button,组内包含一个按钮背景图层,一个艺术字图层(非文本图层),就可以组成一个按钮内带有艺术字图片的按钮。

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

UI类型标识列表:

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

类型标识: .img, .image,

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

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

Text: UGUI普通Text文本

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

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

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

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

类型标识: .msk, .mask,

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

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

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

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

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

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

TMPButton: 按钮(Text Mesh Pro)

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

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

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

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

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

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

类型标识: .select, .focus,

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

类型标识: .disable, .forbid,

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

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

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

类型标识: .dpd, .dropdown,

TMPDropdown: 按钮(Text Mesh Pro)

类型标识: .tmpdpd, .tmpdropdown,

Dropdown_Label: 下拉框上显示的文本

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

Dropdown_Arrow: 下拉框箭头图标

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

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

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

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

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

InputField_Placeholder: 输入框内的提示文本

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

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

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

Toggle: 单选框/复选框

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

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

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

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

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

Toggle_Label: 勾选框文本

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

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

类型标识: .sld, .slider,

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

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

Slider_Handle: 滑动条的拖动滑块

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

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

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

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

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

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

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

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

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

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

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

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

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

 UGUI Parser代码:

#if UNITY_EDITOR

using Aspose.PSD.FileFormats.Psd.Layers.FillLayers;

using System;

using System.IO;

using System.Linq;

using System.Text;

using TMPro;

using UnityEditor;

using UnityEngine;

namespace UGF.EditorTools.Psd2UGUI

{

public enum GUIType

{

Null = 0,

Image,

RawImage,

Text,

Button,

Dropdown,

InputField,

Toggle,

Slider,

ScrollView,

Mask,

FillColor, //纯色填充

TMPText,

TMPButton,

TMPDropdown,

TMPInputField,

TMPToggle,

//UI的子类型, 以101开始。 0-100预留给UI类型, 新类型从尾部追加

Background = 101, //通用背景

//Button的子类型

Button_Highlight,

Button_Press,

Button_Select,

Button_Disable,

Button_Text,

//Dropdown/TMPDropdown的子类型

Dropdown_Label,

Dropdown_Arrow,

//InputField/TMPInputField的子类型

InputField_Placeholder,

InputField_Text,

//Toggle的子类型

Toggle_Checkmark,

Toggle_Label,

//Slider的子类型

Slider_Fill,

Slider_Handle,

//ScrollView的子类型

ScrollView_Viewport, //列表可视区域的遮罩图

ScrollView_HorizontalBarBG, //水平滑动栏背景

ScrollView_HorizontalBar,//水平滑块

ScrollView_VerticalBarBG, //垂直滑动栏背景

ScrollView_VerticalBar, //垂直滑动块

}

[Serializable]

public class UGUIParseRule

{

public GUIType UIType;

public string[] TypeMatches; //类型匹配标识

public GameObject UIPrefab; //UI模板

public string UIHelper; //UIHelper类型全名

public string Comment;//注释

}

[CustomEditor(typeof(UGUIParser))]

public class UGUIParserEditor : Editor

{

private SerializedProperty readmeProperty;

private void OnEnable()

{

readmeProperty = serializedObject.FindProperty("readmeDoc");

}

public override void OnInspectorGUI()

{

serializedObject.Update();

if (GUILayout.Button("导出使用文档"))

{

(target as UGUIParser).ExportReadmeDoc();

}

EditorGUILayout.LabelField("使用说明:");

readmeProperty.stringValue = EditorGUILayout.TextArea(readmeProperty.stringValue, GUILayout.Height(100));

serializedObject.ApplyModifiedProperties();

base.OnInspectorGUI();

}

}

[CreateAssetMenu(fileName = "Psd2UIFormConfig", menuName = "ScriptableObject/Psd2UIForm Config【Psd2UIForm工具配置】")]

public class UGUIParser : ScriptableObject

{

public const int UITYPE_MAX = 100;

[SerializeField] GUIType defaultTextType = GUIType.Text;

[SerializeField] GUIType defaultImageType = GUIType.Image;

[SerializeField] GameObject uiFormTemplate;

[SerializeField] UGUIParseRule[] rules;

[HideInInspector][SerializeField] string readmeDoc = "使用说明";

public GUIType DefaultText => defaultTextType;

public GUIType DefaultImage => defaultImageType;

public GameObject UIFormTemplate => uiFormTemplate;

private static UGUIParser mInstance = null;

public static UGUIParser Instance

{

get

{

if (mInstance == null)

{

var guid = AssetDatabase.FindAssets("t:UGUIParser").FirstOrDefault();

mInstance = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid));

}

return mInstance;

}

}

public static bool IsMainUIType(GUIType tp)

{

return (int)tp <= UITYPE_MAX;

}

public Type GetHelperType(GUIType uiType)

{

if (uiType == GUIType.Null) return null;

var rule = GetRule(uiType);

if (rule == null || string.IsNullOrWhiteSpace(rule.UIHelper)) return null;

return Type.GetType(rule.UIHelper);

}

public UGUIParseRule GetRule(GUIType uiType)

{

foreach (var rule in rules)

{

if (rule.UIType == uiType) return rule;

}

return null;

}

///

/// 根据图层命名解析UI类型

///

///

///

///

public bool TryParse(PsdLayerNode layer, out UGUIParseRule result)

{

result = null;

var layerName = layer.BindPsdLayer.Name;

if (Path.HasExtension(layerName))

{

var tpTag = Path.GetExtension(layerName).Substring(1).ToLower();

foreach (var rule in rules)

{

foreach (var item in rule.TypeMatches)

{

if (tpTag.CompareTo(item.ToLower()) == 0)

{

result = rule;

return true;

}

}

}

}

switch (layer.LayerType)

{

case PsdLayerType.TextLayer:

result = rules.First(itm => itm.UIType == defaultTextType);

break;

case PsdLayerType.LayerGroup:

result = rules.First(itm => itm.UIType == GUIType.Null);

break;

default:

result = rules.First(itm => itm.UIType == defaultImageType);

break;

}

return result != null;

}

///

/// 根据图层大小和位置设置UI节点大小和位置

///

///

///

/// 是否设置位置

public static void SetRectTransform(PsdLayerNode layerNode, UnityEngine.Component uiNode, bool pos = true, bool width = true, bool height = true, int extSize = 0)

{

if (uiNode != null && layerNode != null)

{

var rect = layerNode.LayerRect;

var rectTransform = uiNode.GetComponent();

if (width) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rect.size.x + extSize);

if (height) rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rect.size.y + extSize);

if (pos)

{

rectTransform.position = rect.position + rectTransform.rect.size * (rectTransform.pivot - Vector2.one * 0.5f)*0.01f;

}

}

}

///

/// 把LayerNode图片保存到本地并返回

///

///

///

public static Texture2D LayerNode2Texture(PsdLayerNode layerNode)

{

if (layerNode != null)

{

var spAssetName = layerNode.ExportImageAsset(false);

var texture = AssetDatabase.LoadAssetAtPath(spAssetName);

return texture;

}

return null;

}

///

/// 把LayerNode图片保存到本地并返回

///

///

/// 若没有设置Sprite的九宫,是否自动计算并设置九宫

///

public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false)

{

if (layerNode != null)

{

var spAssetName = layerNode.ExportImageAsset(true);

var sprite = AssetDatabase.LoadAssetAtPath(spAssetName);

if (sprite != null)

{

if (auto9Slice)

{

var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;

var rawReadable = spImpt.isReadable;

if (!rawReadable)

{

spImpt.isReadable = true;

spImpt.SaveAndReimport();

}

if (spImpt.spriteBorder == Vector4.zero)

{

spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);

spImpt.isReadable = rawReadable;

spImpt.SaveAndReimport();

}

}

return sprite;

}

}

return null;

}

///

/// 自动计算贴图的 9宫 Border

///

///

/// 0-255

///

public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3)

{

int width = texture.width;

int height = texture.height;

Color32[] pixels = texture.GetPixels32();

int minX = width;

int minY = height;

int maxX = 0;

int maxY = 0;

// 寻找不透明像素的最小和最大边界

for (int y = 0; y < height; y++)

{

for (int x = 0; x < width; x++)

{

int pixelIndex = y * width + x;

Color32 pixel = pixels[pixelIndex];

if (pixel.a >= alphaThreshold)

{

minX = Mathf.Min(minX, x);

minY = Mathf.Min(minY, y);

maxX = Mathf.Max(maxX, x);

maxY = Mathf.Max(maxY, y);

}

}

}

// 计算最优的borderSize

int borderSizeX = (maxX - minX) / 3;

int borderSizeY = (maxY - minY) / 3;

int borderSize = Mathf.Min(borderSizeX, borderSizeY);

// 根据边界和Border Size计算Nine Slice Border

int left = minX + borderSize;

int right = maxX - borderSize;

int top = minY + borderSize;

int bottom = maxY - borderSize;

// 确保边界在纹理范围内

left = Mathf.Clamp(left, 0, width - 1);

right = Mathf.Clamp(right, 0, width - 1);

top = Mathf.Clamp(top, 0, height - 1);

bottom = Mathf.Clamp(bottom, 0, height - 1);

return new Vector4(left, top, width - right, height - bottom);

}

///

/// 把PS的字体样式同步设置到UGUI Text

///

///

///

public static void SetTextStyle(PsdLayerNode txtLayer, UnityEngine.UI.Text text)

{

if (text == null) return;

text.gameObject.SetActive(txtLayer != null);

if (txtLayer != null && txtLayer.ParseTextLayerInfo(out var str, out var size, out var charSpace, out float lineSpace, out var col, out var style, out var tmpStyle, out var fName))

{

var tFont = FindFontAsset(fName);

if (tFont != null) text.font = tFont;

text.text = str;

text.fontSize = size;

text.fontStyle = style;

text.color = col;

text.lineSpacing = lineSpace;

}

}

///

/// 把PS的字体样式同步设置到TextMeshProUGUI

///

///

///

public static void SetTextStyle(PsdLayerNode txtLayer, TextMeshProUGUI text)

{

if (txtLayer != null && txtLayer.ParseTextLayerInfo(out var str, out var size, out var charSpace, out float lineSpace, out var col, out var style, out var tmpStyle, out var fName))

{

var tFont = FindTMPFontAsset(fName);

if (tFont != null) text.font = tFont;

text.text = str;

text.fontSize = size;

text.fontStyle = tmpStyle;

text.color = col;

text.characterSpacing = charSpace;

text.lineSpacing = lineSpace;

}

}

///

/// 根据字体名查找TMP_FontAsset

///

///

///

public static TMP_FontAsset FindTMPFontAsset(string fontName)

{

var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");

foreach (var guid in fontGuids)

{

var fontPath = AssetDatabase.GUIDToAssetPath(guid);

var font = AssetDatabase.LoadAssetAtPath(fontPath);

if (font != null && font.faceInfo.familyName == fontName)

{

return font;

}

}

return null;

}

///

/// 根据字体名查找Font Asset

///

///

///

public static UnityEngine.Font FindFontAsset(string fontName)

{

var fontGuids = AssetDatabase.FindAssets("t:font");

foreach (var guid in fontGuids)

{

var fontPath = AssetDatabase.GUIDToAssetPath(guid);

var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;

if (font != null && font.fontTTFName == fontName)

{

return AssetDatabase.LoadAssetAtPath(fontPath);

}

}

return null;

}

internal static UnityEngine.Color LayerNode2Color(PsdLayerNode fillColor, Color defaultColor)

{

if (fillColor != null && fillColor.BindPsdLayer is FillLayer fillLayer)

{

var layerColor = fillLayer.GetPixel(fillLayer.Width / 2, fillLayer.Height / 2);

return new UnityEngine.Color(layerColor.R, layerColor.G, layerColor.B, fillLayer.Opacity) / (float)255;

}

return defaultColor;

}

///

/// 导出UI设计师使用规则文档

///

///

internal void ExportReadmeDoc()

{

var exportDir = EditorUtility.SaveFolderPanel("选择文档导出路径", Application.dataPath, null);

if (string.IsNullOrWhiteSpace(exportDir) || !Directory.Exists(exportDir))

{

return;

}

var docFile = UtilityBuiltin.ResPath.GetCombinePath(exportDir, "Psd2UGUI设计师使用文档.doc");

var strBuilder = new StringBuilder();

strBuilder.AppendLine("使用说明:");

strBuilder.AppendLine(this.readmeDoc);

strBuilder.AppendLine(Environment.NewLine + Environment.NewLine);

strBuilder.AppendLine("UI类型标识: 图层/组命名以'.类型'结尾");

strBuilder.AppendLine("UI类型标识列表:");

foreach (var rule in rules)

{

if (rule.UIType == GUIType.Null) continue;

strBuilder.AppendLine($"{rule.UIType}: {rule.Comment}");

strBuilder.Append("类型标识: ");

foreach (var tag in rule.TypeMatches)

{

strBuilder.Append($".{tag}, ");

}

strBuilder.AppendLine();

strBuilder.AppendLine();

}

try

{

File.WriteAllText(docFile, strBuilder.ToString(), System.Text.Encoding.UTF8);

EditorUtility.RevealInFinder(docFile);

}

catch (Exception e)

{

Debug.LogException(e);

}

}

}

}

#endif

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

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

// 判断图层是否包含特效

function hasLayerEffect(layer) {

app.activeDocument.activeLayer = layer;

var hasEffect = false;

try {

var ref = new ActionReference();

var keyLayerEffects = app.charIDToTypeID( 'Lefx' );

ref.putProperty( app.charIDToTypeID( 'Prpr' ), keyLayerEffects );

ref.putEnumerated( app.charIDToTypeID( 'Lyr ' ), app.charIDToTypeID( 'Ordn' ), app.charIDToTypeID( 'Trgt' ) );

var desc = executeActionGet( ref );

if ( desc.hasKey( keyLayerEffects ) ) {

hasEffect = true;

}

}catch(e) {

hasEffect = false;

}

return hasEffect;

}

function convertLayersToSmartObjects(layers)

{

for (var i = layers.length - 1; i >= 0; i--)

{

var layer = layers[i];

if (layer.typename === "LayerSet")

{

convertLayersToSmartObjects(layer.layers); // Recursively convert layers in layer sets

}

else

{

if (hasLayerEffect(layer)){

if(layer.kind === LayerKind.TEXT)convertToSmartObject(layer); // Convert layers with layer effects to smart objects

else layer.rasterize(RasterizeType.SHAPE);

}

}

}

}

// 把图层转换为智能对象,功能等同右键图层->转为智能对象

function convertToSmartObject(layer) {

app.activeDocument.activeLayer = layer;

// 创建一个新的智能对象

var idnewPlacedLayer = stringIDToTypeID("newPlacedLayer");

executeAction(idnewPlacedLayer, undefined, DialogModes.NO);

}

// 导出处理后的PSD文件

function exportPSD() {

var doc = app.activeDocument;

var savePath = Folder.selectDialog("选择psd导出路径");

if (savePath != null) {

var saveOptions = new PhotoshopSaveOptions();

saveOptions.embedColorProfile = true;

saveOptions.alphaChannels = true;

var saveFile = new File(savePath + "/" + doc.name);

doc.saveAs(saveFile, saveOptions, true, Extension.LOWERCASE);

alert("PSD已成功导出!");

}

}

function convertAndExport(){

convertLayersToSmartObjects (app.activeDocument.layers);

//exportPSD();

}

app.activeDocument.suspendHistory("Convert2SmartObject", "convertAndExport();");

//~ convertLayersToSmartObjects (app.activeDocument.layers);

四、Psd转UGUI编辑器

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

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

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

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

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

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

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

Psd2UIForm编辑器代码:

#if UNITY_EDITOR

using Aspose.PSD.FileFormats.Psd;

using Aspose.PSD.FileFormats.Psd.Layers;

using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;

using Aspose.PSD.ImageLoadOptions;

using GameFramework;

using HarmonyLib;

using System;

using System.Collections.Generic;

using System.IO;

using System.Linq;

using System.Text;

using UnityEditor;

using UnityEditor.SceneManagement;

using UnityEngine;

using UnityGameFramework.Runtime;

namespace UGF.EditorTools.Psd2UGUI

{

#region Crack

[HarmonyPatch(typeof(System.Xml.XmlElement), nameof(System.Xml.XmlElement.InnerText), MethodType.Getter)]

class CrackAspose

{

static void Postfix(ref string __result)

{

if (__result == "20220516")

{

__result = "20500516";

}

//else if (__result == "20210827")

//{

// __result = "20250827";

//}

}

}

#endregion

[CustomEditor(typeof(Psd2UIFormConverter))]

public class Psd2UIFormConverterInspector : UnityEditor.Editor

{

Psd2UIFormConverter targetLogic;

GUIContent parsePsd2NodesBt;

GUIContent exportUISpritesBt;

GUIContent generateUIFormBt;

GUILayoutOption btHeight;

private void OnEnable()

{

btHeight = GUILayout.Height(30);

targetLogic = target as Psd2UIFormConverter;

parsePsd2NodesBt = new GUIContent("解析psd图层", "把psd图层解析为可编辑节点树");

exportUISpritesBt = new GUIContent("导出Images", "导出勾选的psd图层为碎图");

generateUIFormBt = new GUIContent("生成UIForm", "根据解析后的节点树生成UIForm Prefab");

if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir))

{

Debug.LogWarning($"UIForm输出路径为空!");

}

}

private void OnDisable()

{

Psd2UIFormSettings.Save();

}

public override void OnInspectorGUI()

{

EditorGUILayout.BeginVertical("box");

{

EditorGUILayout.BeginHorizontal();

{

EditorGUILayout.LabelField("自动压缩图片:", GUILayout.Width(150));

Psd2UIFormSettings.Instance.CompressImage = EditorGUILayout.Toggle(Psd2UIFormSettings.Instance.CompressImage);

EditorGUILayout.EndHorizontal();

}

EditorGUILayout.BeginHorizontal();

{

EditorGUILayout.LabelField("UI图片导出路径:", GUILayout.Width(150));

Psd2UIFormSettings.Instance.UIImagesOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIImagesOutputDir);

if (GUILayout.Button("选择路径", GUILayout.Width(80)))

{

var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIImagesOutputDir, null);

if (!string.IsNullOrWhiteSpace(retPath))

{

if (!retPath.StartsWith("Assets/"))

{

retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);

}

Psd2UIFormSettings.Instance.UIImagesOutputDir = retPath;

Psd2UIFormSettings.Save();

}

GUIUtility.ExitGUI();

}

EditorGUILayout.EndHorizontal();

}

EditorGUILayout.BeginHorizontal();

{

Psd2UIFormSettings.Instance.UseUIFormOutputDir = EditorGUILayout.ToggleLeft("使用UIForm导出路径:", Psd2UIFormSettings.Instance.UseUIFormOutputDir, GUILayout.Width(150));

EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.UseUIFormOutputDir);

{

Psd2UIFormSettings.Instance.UIFormOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormOutputDir);

if (GUILayout.Button("选择路径", GUILayout.Width(80)))

{

var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormOutputDir, null);

if (!string.IsNullOrWhiteSpace(retPath))

{

if (!retPath.StartsWith("Assets/"))

{

retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);

}

Psd2UIFormSettings.Instance.UIFormOutputDir = retPath;

Psd2UIFormSettings.Save();

}

GUIUtility.ExitGUI();

}

EditorGUI.EndDisabledGroup();

}

EditorGUILayout.EndHorizontal();

}

//EditorGUILayout.BeginHorizontal();

//{

// Psd2UIFormSettings.Instance.AutoCreateUIFormScript = EditorGUILayout.ToggleLeft("生成UIForm代码:", Psd2UIFormSettings.Instance.AutoCreateUIFormScript, GUILayout.Width(150));

// EditorGUI.BeginDisabledGroup(!Psd2UIFormSettings.Instance.AutoCreateUIFormScript);

// {

// Psd2UIFormSettings.Instance.UIFormScriptOutputDir = EditorGUILayout.TextField(Psd2UIFormSettings.Instance.UIFormScriptOutputDir);

// if (GUILayout.Button("选择路径", GUILayout.Width(80)))

// {

// var retPath = EditorUtility.OpenFolderPanel("选择导出路径", Psd2UIFormSettings.Instance.UIFormScriptOutputDir, null);

// if (!string.IsNullOrWhiteSpace(retPath))

// {

// if (!retPath.StartsWith("Assets/"))

// {

// retPath = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, retPath);

// }

// Psd2UIFormSettings.Instance.UIFormScriptOutputDir = retPath;

// Psd2UIFormSettings.Save();

// }

// GUIUtility.ExitGUI();

// }

// EditorGUI.EndDisabledGroup();

// }

// EditorGUILayout.EndHorizontal();

//}

EditorGUILayout.EndVertical();

}

EditorGUILayout.BeginHorizontal();

{

if (GUILayout.Button(parsePsd2NodesBt, btHeight))

{

Psd2UIFormConverter.ParsePsd2LayerPrefab(targetLogic.PsdAssetName, targetLogic);

}

if (GUILayout.Button(exportUISpritesBt, btHeight))

{

targetLogic.ExportSprites();

}

EditorGUILayout.EndHorizontal();

}

if (GUILayout.Button(generateUIFormBt, btHeight))

{

targetLogic.GenerateUIForm();

}

base.OnInspectorGUI();

}

public override bool HasPreviewGUI()

{

return targetLogic.BindPsdAsset != null;

}

public override void OnPreviewGUI(Rect r, GUIStyle background)

{

GUI.DrawTexture(r, targetLogic.BindPsdAsset.texture, ScaleMode.ScaleToFit);

//base.OnPreviewGUI(r, background);

}

}

///

/// Psd文件转成UIForm prefab

///

[ExecuteInEditMode]

[RequireComponent(typeof(SpriteRenderer))]

public class Psd2UIFormConverter : MonoBehaviour

{

const string RecordLayerOperation = "Change Export Image";

public static Psd2UIFormConverter Instance { get; private set; }

[ReadOnlyField][SerializeField] public string psdAssetChangeTime;//文件修改时间标识

[Tooltip("UIForm名字")][SerializeField] private string uiFormName;

[Tooltip("关联的psd文件")][SerializeField] private UnityEngine.Sprite psdAsset;

[Header("Debug:")][SerializeField] bool drawLayerRectGizmos = true;

[SerializeField] UnityEngine.Color drawLayerRectGizmosColor = UnityEngine.Color.green;

private PsdImage psdInstance;//psd文件解析实例

private GUIStyle uiTypeLabelStyle;

public string PsdAssetName => psdAsset != null ? AssetDatabase.GetAssetPath(psdAsset) : null;

public UnityEngine.Sprite BindPsdAsset => psdAsset;

public Vector2Int UIFormCanvasSize { get; private set; } = new Vector2Int(750, 1334);

private void OnEnable()

{

Instance = this;

uiTypeLabelStyle = new GUIStyle();

uiTypeLabelStyle.fontSize = 13;

uiTypeLabelStyle.fontStyle = UnityEngine.FontStyle.BoldAndItalic;

UnityEngine.ColorUtility.TryParseHtmlString("#7ED994", out var color);

uiTypeLabelStyle.normal.textColor = color;

EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;

if (psdInstance == null && !string.IsNullOrWhiteSpace(PsdAssetName))

{

RefreshNodesBindLayer();

}

}

private void Start()

{

if (this.CheckPsdAssetHasChanged())

{

if (EditorUtility.DisplayDialog("PSD -> UIForm", $"{gameObject.name}关联的psd文件[{this.PsdAssetName}]已改变,是否重新解析节点树?", "是", "否"))

{

if (Psd2UIFormConverter.ParsePsd2LayerPrefab(this.PsdAssetName, this))

{

RefreshNodesBindLayer();

}

}

}

else

{

RefreshNodesBindLayer();

}

}

private void OnDrawGizmos()

{

if (drawLayerRectGizmos)

{

var nodes = this.GetComponentsInChildren();

Gizmos.color = drawLayerRectGizmosColor;

foreach (var item in nodes)

{

if (item.NeedExportImage())

{

Gizmos.DrawWireCube(item.LayerRect.position * 0.01f, item.LayerRect.size * 0.01f);

}

}

}

}

private void OnHierarchyGUI(int instanceID, Rect selectionRect)

{

if (Event.current == null) return;

var node = EditorUtility.InstanceIDToObject(instanceID) as GameObject;

if (node == null || node == this.gameObject) return;

if (!node.TryGetComponent(out var layer)) return;

Rect tmpRect = selectionRect;

tmpRect.x = 35;

tmpRect.width = 10;

Undo.RecordObject(layer, RecordLayerOperation);

EditorGUI.BeginChangeCheck();

{

layer.markToExport = EditorGUI.Toggle(tmpRect, layer.markToExport);

if (EditorGUI.EndChangeCheck())

{

if (Selection.gameObjects.Length > 1) SetExportImageTg(Selection.gameObjects, layer.markToExport);

EditorUtility.SetDirty(layer);

}

}

tmpRect.width = Mathf.Clamp(selectionRect.xMax * 0.2f, 100, 200);

tmpRect.x = selectionRect.xMax - tmpRect.width;

//EditorGUI.LabelField(tmpRect, layer.UIType.ToString(), uiTypeLabelStyle);

if (EditorGUI.DropdownButton(tmpRect, new GUIContent(layer.UIType.ToString()), FocusType.Passive))

{

var dropdownMenu = PopEnumMenu(layer.UIType, selectUIType =>

{

layer.SetUIType(selectUIType);

EditorUtility.SetDirty(layer);

});

dropdownMenu.ShowAsContext();

}

}

private GenericMenu PopEnumMenu(T currentValue, Action onSelectEnum) where T : Enum

{

var names = Enum.GetValues(typeof(T));

var dropdownMenu = new GenericMenu();

foreach (T item in names)

{

dropdownMenu.AddItem(new GUIContent(item.ToString()), item.Equals(currentValue), () => { onSelectEnum(item); });

}

return dropdownMenu;

}

///

/// 批量勾选导出图片

///

///

///

private void SetExportImageTg(GameObject[] selects, bool exportImg)

{

var selectLayerNodes = selects.Where(item => item?.GetComponent() != null).ToArray();

foreach (var layer in selectLayerNodes)

{

layer.GetComponent().markToExport = exportImg;

}

}

private void OnDestroy()

{

EditorApplication.hierarchyWindowItemOnGUI -= OnHierarchyGUI;

if (this.psdInstance != null && !psdInstance.Disposed)

{

psdInstance.Dispose();

}

}

private void RefreshNodesBindLayer()

{

if (psdInstance == null || psdInstance.Disposed)

{

if (!File.Exists(PsdAssetName))

{

Debug.LogError($"刷新节点绑定图层失败! psd文件不存在");

return;

}

var psdOpts = new PsdLoadOptions()

{

LoadEffectsResource = true,

ReadOnlyMode = false,

};

psdInstance = Aspose.PSD.Image.Load(PsdAssetName, psdOpts) as PsdImage;

UIFormCanvasSize.Set(psdInstance.Size.Width, psdInstance.Size.Height);

}

var layers = GetComponentsInChildren(true);

foreach (var layer in layers)

{

layer.InitPsdLayers(psdInstance);

}

var spRender = gameObject.GetOrAddComponent();

spRender.sprite = this.psdAsset;

}

#region

const string AsposeLicenseKey = "此处为Aspose.PSD证书";

static bool licenseInitiated = false;

[InitializeOnLoadMethod]

static void InitAsposeLicense()

{

if (licenseInitiated) return;

var harmonyHook = new Harmony("Crack.Aspose");

harmonyHook.PatchAll();

new Aspose.PSD.License().SetLicense(new MemoryStream(Convert.FromBase64String(AsposeLicenseKey)));

licenseInitiated = true;

harmonyHook.UnpatchAll();

//GetAllLayerType();

}

static void GetAllLayerType()

{

var psdLib = Utility.Assembly.GetAssemblies().FirstOrDefault(item => item.GetName().Name == "Aspose.PSD");

var layers = psdLib.GetTypes().Where(tp => tp.IsSubclassOf(typeof(Layer)) && !tp.IsAbstract);

string layerEnumNames = "";

foreach (var item in layers)

{

layerEnumNames += $"{item.Name},\n";

}

Debug.Log(layerEnumNames);

}

#endregion Aspose License

[MenuItem("Assets/GF Editor Tool/Psd2UIForm Editor", priority = 0)]

static void Psd2UIFormPrefabMenu()

{

if (Selection.activeObject == null) return;

var assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);

if (Path.GetExtension(assetPath).ToLower().CompareTo(".psd") != 0)

{

Debug.LogWarning($"选择的文件({assetPath})不是psd格式, 工具只支持psd转换为UIForm");

return;

}

string psdLayerPrefab = GetPsdLayerPrefabPath(assetPath);

if (!File.Exists(psdLayerPrefab))

{

if (ParsePsd2LayerPrefab(assetPath))

{

OpenPsdLayerEditor(psdLayerPrefab);

}

}

else

{

OpenPsdLayerEditor(psdLayerPrefab);

}

}

public bool CheckPsdAssetHasChanged()

{

if (psdAsset == null) return false;

var fileTag = GetAssetChangeTag(PsdAssetName);

return psdAssetChangeTime.CompareTo(fileTag) != 0;

}

public static string GetAssetChangeTag(string fileName)

{

return new FileInfo(fileName).LastWriteTime.ToString("yyyyMMddHHmmss");

}

///

/// 打开psd图层信息prefab

///

///

public static void OpenPsdLayerEditor(string psdLayerPrefab)

{

PrefabStageUtility.OpenPrefab(psdLayerPrefab);

}

///

/// 把Psd图层解析成节点prefab

///

///

///

public static bool ParsePsd2LayerPrefab(string psdFile, Psd2UIFormConverter instanceRoot = null)

{

if (!File.Exists(psdFile))

{

Debug.LogError($"Error: Psd文件不存在:{psdFile}");

return false;

}

var texImporter = AssetImporter.GetAtPath(psdFile) as TextureImporter;

if (texImporter.textureType != TextureImporterType.Sprite)

{

texImporter.textureType = TextureImporterType.Sprite;

texImporter.mipmapEnabled = false;

texImporter.alphaIsTransparency = true;

texImporter.SaveAndReimport();

}

var prefabFile = GetPsdLayerPrefabPath(psdFile);

var rootName = Path.GetFileNameWithoutExtension(prefabFile);

bool needDestroyInstance = instanceRoot == null;

if (instanceRoot != null)

{

ParsePsdLayer2Root(psdFile, instanceRoot);

instanceRoot.RefreshNodesBindLayer();

return true;

}

else

{

Psd2UIFormConverter rootLayer = CreatePsdLayerRoot(rootName);

rootLayer.SetPsdAsset(psdFile);

ParsePsdLayer2Root(psdFile, rootLayer);

PrefabUtility.SaveAsPrefabAsset(rootLayer.gameObject, prefabFile, out bool savePrefabSuccess);

if (needDestroyInstance) GameObject.DestroyImmediate(rootLayer.gameObject);

AssetDatabase.Refresh();

if (savePrefabSuccess && AssetDatabase.GUIDFromAssetPath(StageUtility.GetCurrentStage().assetPath) != AssetDatabase.GUIDFromAssetPath(prefabFile))

{

PrefabStageUtility.OpenPrefab(prefabFile);

}

return savePrefabSuccess;

}

}

private static void ParsePsdLayer2Root(string psdFile, Psd2UIFormConverter converter)

{

//清空已有节点重新解析

for (int i = converter.transform.childCount - 1; i >= 0; i--)

{

GameObject.DestroyImmediate(converter.transform.GetChild(i).gameObject);

}

var psdOpts = new PsdLoadOptions()

{

LoadEffectsResource = true,

ReadOnlyMode = false

};

using (var psd = Aspose.PSD.Image.Load(psdFile, psdOpts) as PsdImage)

{

List layerNodes = new List { converter.gameObject };

for (int i = 0; i < psd.Layers.Length; i++)

{

var layer = psd.Layers[i];

var curLayerType = layer.GetLayerType();

if (curLayerType == PsdLayerType.SectionDividerLayer)

{

var layerGroup = (layer as SectionDividerLayer).GetRelatedLayerGroup();

var layerGroupIdx = ArrayUtility.IndexOf(psd.Layers, layerGroup);

var layerGropNode = CreatePsdLayerNode(layerGroup, layerGroupIdx);

layerNodes.Add(layerGropNode.gameObject);

}

else if (curLayerType == PsdLayerType.LayerGroup)

{

var lastLayerNode = layerNodes.Last();

layerNodes.Remove(lastLayerNode);

if (layerNodes.Count > 0)

{

var parentLayerNode = layerNodes.Last();

lastLayerNode.transform.SetParent(parentLayerNode.transform);

}

}

else

{

var newLayerNode = CreatePsdLayerNode(layer, i);

newLayerNode.transform.SetParent(layerNodes.Last().transform);

newLayerNode.transform.localPosition = Vector3.zero;

}

}

}

converter.psdAssetChangeTime = GetAssetChangeTag(psdFile);

var childrenNodes = converter.GetComponentsInChildren(true);

foreach (var item in childrenNodes)

{

item.RefreshUIHelper(false);

}

EditorUtility.SetDirty(converter.gameObject);

}

private void SetPsdAsset(string psdFile)

{

this.psdAsset = AssetDatabase.LoadAssetAtPath(psdFile);

if (string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIImagesOutputDir))

{

Psd2UIFormSettings.Instance.UIImagesOutputDir = Path.GetDirectoryName(psdFile);

}

if (string.IsNullOrWhiteSpace(this.uiFormName))

{

this.uiFormName = this.psdAsset.name;

}

}

///

/// 获取解析好的psd layers文件

///

///

///

public static string GetPsdLayerPrefabPath(string psd)

{

return UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(psd), Path.GetFileNameWithoutExtension(psd) + "_psd_layers_parsed.prefab");

}

private static Psd2UIFormConverter CreatePsdLayerRoot(string rootName)

{

var node = new GameObject(rootName);

node.gameObject.tag = "EditorOnly";

var layerRoot = node.AddComponent();

return layerRoot;

}

private static PsdLayerNode CreatePsdLayerNode(Layer layer, int bindLayerIdx)

{

string nodeName = layer.Name;

if (string.IsNullOrWhiteSpace(nodeName))

{

nodeName = $"PsdLayer-{bindLayerIdx}";

}

else

{

if (Path.HasExtension(layer.Name))

{

nodeName = Path.GetFileNameWithoutExtension(layer.Name);

}

}

var node = new GameObject(nodeName);

node.gameObject.tag = "EditorOnly";

var layerNode = node.AddComponent();

layerNode.BindPsdLayerIndex = bindLayerIdx;

InitLayerNodeData(layerNode, layer);

return layerNode;

}

///

/// 根据psd图层信息解析并初始化图层UI类型、是否导出等信息

///

///

///

private static void InitLayerNodeData(PsdLayerNode layerNode, Layer layer)

{

if (layer == null || layer.Disposed) return;

var layerTp = layer.GetLayerType();

layerNode.BindPsdLayer = layer;

if (UGUIParser.Instance.TryParse(layerNode, out var initRule))

{

layerNode.SetUIType(initRule.UIType, false);

}

layerNode.markToExport = layerTp != PsdLayerType.LayerGroup && !(layerTp == PsdLayerType.TextLayer && layerNode.UIType.ToString().EndsWith("Text") && layerNode.UIType != GUIType.FillColor);

layerNode.gameObject.SetActive(layer.IsVisible);

}

///

/// 导出psd图层为Sprites碎图

///

///

internal void ExportSprites()

{

//var pngOpts = new PngOptions()

//{

// ColorType = Aspose.PSD.FileFormats.Png.PngColorType.Truecolor

//};

//this.psdInstance.Save("Assets/AAAGame/Sprites/UI/Preview.png", pngOpts);

//return;

var exportLayers = this.GetComponentsInChildren().Where(node => node.NeedExportImage());

var exportDir = GetUIFormImagesOutputDir();

if (!Directory.Exists(exportDir))

{

Directory.CreateDirectory(exportDir);

}

int exportIdx = 0;

int totalCount = exportLayers.Count();

foreach (var layer in exportLayers)

{

var assetName = layer.ExportImageAsset();

if (assetName == null)

{

Debug.LogWarning($"导出图层[name:{layer.name}, layerIdx:{layer.BindPsdLayerIndex}]图片失败!");

}

++exportIdx;

EditorUtility.DisplayProgressBar($"导出进度({exportIdx}/{totalCount})", $"导出UI图片:{assetName}", exportIdx / (float)totalCount);

}

EditorUtility.ClearProgressBar();

AssetDatabase.Refresh();

}

///

/// 根据解析后的节点树生成UIForm Prefab

///

internal void GenerateUIForm()

{

if (Psd2UIFormSettings.Instance.UseUIFormOutputDir && string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.UIFormOutputDir))

{

Debug.LogError($"生成UIForm失败! UIForm导出路径为空:{Psd2UIFormSettings.Instance.UIFormOutputDir}");

return;

}

if (Psd2UIFormSettings.Instance.UseUIFormOutputDir)

{

ExportUIPrefab(Psd2UIFormSettings.Instance.UIFormOutputDir);

}

else

{

string lastSaveDir = string.IsNullOrWhiteSpace(Psd2UIFormSettings.Instance.LastUIFormOutputDir) ? "Assets" : Psd2UIFormSettings.Instance.LastUIFormOutputDir;

string selectDir = EditorUtility.SaveFolderPanel("保存目录", lastSaveDir, null);

if (!string.IsNullOrWhiteSpace(selectDir))

{

if (!selectDir.StartsWith("Assets/"))

selectDir = Path.GetRelativePath(Directory.GetParent(Application.dataPath).FullName, selectDir);

Psd2UIFormSettings.Instance.LastUIFormOutputDir = selectDir;

ExportUIPrefab(selectDir);

}

}

}

private bool ExportUIPrefab(string outputDir)

{

if (!string.IsNullOrWhiteSpace(outputDir))

{

if (!Directory.Exists(outputDir))

{

try

{

Directory.CreateDirectory(outputDir);

AssetDatabase.Refresh();

}

catch (Exception err)

{

Debug.LogError($"导出UI prefab失败:{err.Message}");

return false;

}

}

}

if (string.IsNullOrWhiteSpace(uiFormName))

{

Debug.LogError("导出UI Prefab失败: UI Form Name为空, 请填写UI Form Name.");

return false;

}

var prefabName = UtilityBuiltin.ResPath.GetCombinePath(outputDir, $"{uiFormName}.prefab");

if (File.Exists(prefabName))

{

if (!EditorUtility.DisplayDialog("警告", $"prefab文件已存在, 是否覆盖:{prefabName}", "覆盖生成", "取消生成"))

{

return false;

}

}

var uiHelpers = GetAvailableUIHelpers();

if (uiHelpers == null || uiHelpers.Length < 1)

{

return false;

}

var uiFormRoot = GameObject.Instantiate(UGUIParser.Instance.UIFormTemplate, Vector3.zero, Quaternion.identity);

uiFormRoot.name = uiFormName;

int curIdx = 0;

int totalCount = uiHelpers.Length;

foreach (var uiHelper in uiHelpers)

{

EditorUtility.DisplayProgressBar($"生成UIFrom:({curIdx++}/{totalCount})", $"正在生成UI元素:{uiHelper.name}", curIdx /

(float)totalCount);

var uiElement = uiHelper.CreateUI();

if (uiElement == null) continue;

var goPath = GetGameObjectInstanceIdPath(uiHelper.gameObject, out var goNames);

var parentNode = GetOrCreateNodeByInstanceIdPath(uiFormRoot, goPath, goNames);

uiElement.transform.SetParent(parentNode.transform, true);

uiElement.transform.position += new Vector3(this.UIFormCanvasSize.x * 0.5f, this.UIFormCanvasSize.y * 0.5f, 0);

}

var uiStrKeys = uiFormRoot.GetComponentsInChildren(true);

for (int i = uiStrKeys.Length - 1; i >= 0; i--)

{

DestroyImmediate(uiStrKeys[i]);

}

var uiPrefab = PrefabUtility.SaveAsPrefabAsset(uiFormRoot, prefabName, out bool saveSuccess);

if (saveSuccess)

{

DestroyImmediate(uiFormRoot);

Selection.activeGameObject = uiPrefab;

}

EditorUtility.ClearProgressBar();

return true;

}

private GameObject GetOrCreateNodeByInstanceIdPath(GameObject uiFormRoot, string[] goPath, string[] goNames)

{

GameObject result = uiFormRoot;

if (goPath != null && goNames != null)

{

for (int i = 0; i < goPath.Length; i++)

{

var nodeId = goPath[i];

var nodeName = goNames[i];

GameObject targetNode = null;

foreach (Transform child in result.transform)

{

if (child.gameObject == result) continue;

var idKey = child.GetComponent();

if (idKey != null && nodeId == idKey.Key)

{

targetNode = child.gameObject;

break;

}

}

if (targetNode == null)

{

targetNode = new GameObject(nodeName);

targetNode.transform.SetParent(result.transform, false);

targetNode.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);

var targetNodeKey = targetNode.GetOrAddComponent();

targetNodeKey.Key = nodeId;

}

result = targetNode;

}

}

return result;

}

private string[] GetGameObjectInstanceIdPath(GameObject go, out string[] names)

{

names = null;

if (go == null || go.transform.parent == null || go.transform.parent == this.transform) return null;

var parentGo = go.transform.parent;

string[] result = new string[1] { parentGo.gameObject.GetInstanceID().ToString() };

names = new string[1] { parentGo.gameObject.name };

while (parentGo.parent != null && parentGo.parent != this.transform)

{

ArrayUtility.Insert(ref result, 0, parentGo.parent.gameObject.GetInstanceID().ToString());

ArrayUtility.Insert(ref names, 0, parentGo.parent.gameObject.name);

parentGo = parentGo.parent;

}

return result;

}

private UIHelperBase[] GetAvailableUIHelpers()

{

var uiHelpers = this.GetComponentsInChildren();

uiHelpers = uiHelpers.Where(ui => ui.LayerNode.IsMainUIType).ToArray();

List dependInstIds = new List();

foreach (var item in uiHelpers)

{

foreach (var depend in item.GetDependencies())

{

int dependId = depend.gameObject.GetInstanceID();

if (!dependInstIds.Contains(dependId))

{

dependInstIds.Add(dependId);

}

}

}

for (int i = uiHelpers.Length - 1; i >= 0; i--)

{

var uiHelper = uiHelpers[i];

if (dependInstIds.Contains(uiHelper.gameObject.GetInstanceID()))

{

ArrayUtility.RemoveAt(ref uiHelpers, i);

}

}

return uiHelpers;

}

///

/// 把图片设置为为Sprite或Texture类型

///

///

public static void ConvertTexturesType(string[] texAssets, bool isImage = true)

{

foreach (var item in texAssets)

{

var texImporter = AssetImporter.GetAtPath(item) as TextureImporter;

if (texImporter == null)

{

Debug.LogError($"TextureImporter为空:{item}");

continue;

}

if (isImage)

{

texImporter.textureType = TextureImporterType.Sprite;

texImporter.spriteImportMode = SpriteImportMode.Single;

texImporter.alphaSource = TextureImporterAlphaSource.FromInput;

texImporter.alphaIsTransparency = true;

texImporter.mipmapEnabled = false;

}

else

{

texImporter.textureType = TextureImporterType.Default;

texImporter.textureShape = TextureImporterShape.Texture2D;

texImporter.alphaSource = TextureImporterAlphaSource.FromInput;

texImporter.alphaIsTransparency = true;

texImporter.mipmapEnabled = false;

}

texImporter.SaveAndReimport();

}

}

///

/// 压缩图片文件

///

/// 文件名(相对路径Assets)

///

public static bool CompressImageFile(string asset)

{

var assetPath = asset.StartsWith("Assets/") ? Path.GetFullPath(asset, Directory.GetParent(Application.dataPath).FullName) : asset;

var compressTool = Utility.Assembly.GetType("UGF.EditorTools.CompressTool");

if (compressTool == null) return false;

var compressMethod = compressTool.GetMethod("CompressImageOffline", new Type[] { typeof(string), typeof(string) });

if (compressMethod == null) return false;

return (bool)compressMethod.Invoke(null, new object[] { assetPath, assetPath });

}

///

/// 获取UIForm对应的图片导出目录

///

///

public string GetUIFormImagesOutputDir()

{

return UtilityBuiltin.ResPath.GetCombinePath(Psd2UIFormSettings.Instance.UIImagesOutputDir, uiFormName);

}

public SmartObjectLayer ConvertToSmartObjectLayer(Layer layer)

{

var smartObj = psdInstance.SmartObjectProvider.ConvertToSmartObject(new Layer[] { layer });

return smartObj;

}

}

}

#endif

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

#if UNITY_EDITOR

using UnityEngine;

using Aspose.PSD.FileFormats.Psd.Layers;

using Aspose.PSD.ImageOptions;

using UnityEditor;

using System.IO;

using System.Linq;

using Aspose.PSD.FileFormats.Psd;

using Aspose.PSD.FileFormats.Psd.Layers.SmartObjects;

using GameFramework;

namespace UGF.EditorTools.Psd2UGUI

{

[CanEditMultipleObjects]

[CustomEditor(typeof(PsdLayerNode))]

public class PsdLayerNodeInspector : Editor

{

PsdLayerNode targetLogic;

private void OnEnable()

{

targetLogic = target as PsdLayerNode;

targetLogic.RefreshLayerTexture();

}

public override void OnInspectorGUI()

{

serializedObject.Update();

base.OnInspectorGUI();

EditorGUI.BeginChangeCheck();

{

targetLogic.UIType = (GUIType)EditorGUILayout.EnumPopup("UI Type", targetLogic.UIType);

if (EditorGUI.EndChangeCheck())

{

targetLogic.SetUIType(targetLogic.UIType);

}

}

EditorGUILayout.BeginHorizontal();

{

if (GUILayout.Button("导出图片"))

{

foreach (var item in targets)

{

if (item == null) continue;

(item as PsdLayerNode)?.ExportImageAsset();

}

}

EditorGUILayout.EndHorizontal();

}

serializedObject.ApplyModifiedProperties();

}

public override bool HasPreviewGUI()

{

var layerNode = (target as PsdLayerNode);

return layerNode != null && layerNode.PreviewTexture != null;

}

public override void OnPreviewGUI(Rect r, GUIStyle background)

{

var layerNode = (target as PsdLayerNode);

GUI.DrawTexture(r, layerNode.PreviewTexture, ScaleMode.ScaleToFit);

//base.OnPreviewGUI(r, background);

}

public override string GetInfoString()

{

var layerNode = (target as PsdLayerNode);

return layerNode.LayerInfo;

}

}

[ExecuteInEditMode]

[DisallowMultipleComponent]

public class PsdLayerNode : MonoBehaviour

{

[ReadOnlyField] public int BindPsdLayerIndex = -1;

[ReadOnlyField][SerializeField] PsdLayerType mLayerType = PsdLayerType.Unknown;

[SerializeField] public bool markToExport;

[HideInInspector] public GUIType UIType;

public Texture2D PreviewTexture { get; private set; }

public string LayerInfo { get; private set; }

public Rect LayerRect { get; private set; }

public PsdLayerType LayerType { get => mLayerType; }

public bool IsMainUIType => UGUIParser.IsMainUIType(UIType);

///

/// 绑定的psd图层

///

private Layer mBindPsdLayer;

public Layer BindPsdLayer

{

get => mBindPsdLayer;

set

{

mBindPsdLayer = value;

mLayerType = mBindPsdLayer.GetLayerType();

//if (IsTextLayer(out var txtLayer) && !txtLayer.TextBoundBox.IsEmpty)

//{

// LayerRect = AsposePsdExtension.PsdRect2UnityRect(txtLayer.TextBoundBox, Psd2UIFormConverter.Instance.UIFormCanvasSize);

//}

//else

{

LayerRect = mBindPsdLayer.GetLayerRect();

}

LayerInfo = $"{LayerRect}";

}

}

private void OnDestroy()

{

if (PreviewTexture != null)

{

DestroyImmediate(PreviewTexture);

}

}

public void SetUIType(GUIType uiType, bool triggerParseFunc = true)

{

this.UIType = uiType;

RemoveUIHelper();

if (triggerParseFunc)

{

RefreshUIHelper(true);

}

}

public void RefreshUIHelper(bool refreshParent = false)

{

if (UIType == GUIType.Null) return;

var uiHelperTp = UGUIParser.Instance.GetHelperType(UIType);

if (uiHelperTp != null)

{

var helper = gameObject.GetOrAddComponent(uiHelperTp) as UIHelperBase;

helper.ParseAndAttachUIElements();

}

if (refreshParent)

{

var parentHelper = transform.parent?.GetComponent();

parentHelper?.ParseAndAttachUIElements();

}

EditorUtility.SetDirty(this);

}

private void RemoveUIHelper()

{

var uiHelpers = this.GetComponents();

if (uiHelpers != null)

{

foreach (var uiHelper in uiHelpers)

{

DestroyImmediate(uiHelper);

}

}

EditorUtility.SetDirty(this);

}

///

/// 是否需要导出此图层

///

///

public bool NeedExportImage()

{

return gameObject.activeSelf && markToExport;

}

///

/// 导出图片

///

/// 强制贴图类型为Sprite

///

public string ExportImageAsset(bool forceSpriteType = false)

{

string assetName = null;

if (this.RefreshLayerTexture())

{

var bytes = PreviewTexture.EncodeToPNG();

var imgName = Utility.Text.Format("{0}_{1}", string.IsNullOrWhiteSpace(name) ? UIType : name, BindPsdLayerIndex);

var exportDir = Psd2UIFormConverter.Instance.GetUIFormImagesOutputDir();

if (!Directory.Exists(exportDir))

{

try

{

Directory.CreateDirectory(exportDir);

AssetDatabase.Refresh();

}

catch (System.Exception)

{

return null;

}

}

var imgFileName = UtilityBuiltin.ResPath.GetCombinePath(exportDir, imgName + ".png");

File.WriteAllBytes(imgFileName, bytes);

if (Psd2UIFormSettings.Instance.CompressImage)

{

bool compressResult = Psd2UIFormConverter.CompressImageFile(imgFileName);

if (compressResult)

{

Debug.Log($"成功压缩图片:{imgFileName}");

}

else

{

Debug.LogWarning($"压缩图片失败:{imgFileName}");

}

}

assetName = imgFileName;

bool isImage = !(this.UIType == GUIType.FillColor || this.UIType == GUIType.RawImage);

AssetDatabase.Refresh();

Psd2UIFormConverter.ConvertTexturesType(new string[] { imgFileName }, isImage || forceSpriteType);

}

return assetName;

}

public bool RefreshLayerTexture(bool forceRefresh = false)

{

if (!forceRefresh && PreviewTexture != null)

{

return true;

}

if (BindPsdLayer == null || BindPsdLayer.Disposed) return false;

var pngOpt = new PngOptions

{

ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha

};

if (BindPsdLayer.CanSave(pngOpt))

{

if (PreviewTexture != null)

{

DestroyImmediate(PreviewTexture);

}

PreviewTexture = this.ConvertPsdLayer2Texture2D();

}

return PreviewTexture != null;

}

///

/// 把psd图层转成Texture2D

///

///

/// Texture2D

public Texture2D ConvertPsdLayer2Texture2D()

{

if (BindPsdLayer == null || BindPsdLayer.Disposed) return null;

MemoryStream ms = new MemoryStream();

var pngOpt = new Aspose.PSD.ImageOptions.PngOptions()

{

ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha,

FullFrame = true

};

if (BindPsdLayer.Opacity >= 255 || LayerType == PsdLayerType.LayerGroup)

{

BindPsdLayer.Save(ms, pngOpt);

}

else

{

var smartLayer = Psd2UIFormConverter.Instance.ConvertToSmartObjectLayer(BindPsdLayer);

smartLayer.Save(ms, pngOpt);

}

//var bitmap = BindPsdLayer.ToBitmap();

//bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);

var buffer = new byte[ms.Length];

ms.Position = 0;

ms.Read(buffer, 0, buffer.Length);

Texture2D texture = new Texture2D(BindPsdLayer.Width, BindPsdLayer.Height);

texture.alphaIsTransparency = true;

texture.LoadImage(buffer);

texture.Apply();

ms.Dispose();

return texture;

}

///

/// 从第一层子节点按类型查找LayerNode

///

///

///

public PsdLayerNode FindSubLayerNode(GUIType uiTp)

{

for (int i = 0; i < transform.childCount; i++)

{

var child = transform.GetChild(i)?.GetComponent();

if (child != null && child.UIType == uiTp) return child;

}

return null;

}

///

/// 依次查找给定多个类型,返回最先找到的类型

///

///

///

public PsdLayerNode FindSubLayerNode(params GUIType[] uiTps)

{

foreach (var tp in uiTps)

{

var result = FindSubLayerNode(tp);

if (result != null) return result;

}

return null;

}

public PsdLayerNode FindLayerNodeInChildren(GUIType uiTp)

{

var layers = GetComponentsInChildren(true);

if (layers != null && layers.Length > 0)

{

return layers.FirstOrDefault(layer => layer.UIType == uiTp);

}

return null;

}

///

/// 判断该图层是否为文本图层

///

///

///

public bool IsTextLayer(out TextLayer layer)

{

layer = null;

if (BindPsdLayer == null) return false;

if (BindPsdLayer is SmartObjectLayer smartLayer)

{

layer = smartLayer.GetSmartObjectInnerTextLayer() as TextLayer;

return layer != null;

}

else if (BindPsdLayer is TextLayer txtLayer)

{

layer = txtLayer;

return layer != null;

}

return false;

}

internal void InitPsdLayers(PsdImage psdInstance)

{

BindPsdLayer = psdInstance.Layers[BindPsdLayerIndex];

}

internal bool ParseTextLayerInfo(out string text, out int fontSize, out float characterSpace, out float lineSpace, out Color fontColor, out UnityEngine.FontStyle fontStyle, out TMPro.FontStyles tmpFontStyle, out string fontName)

{

text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;

if (IsTextLayer(out var txtLayer))

{

text = txtLayer.Text;

fontSize = (int)txtLayer.Font.Size;

fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))

{

fontStyle = UnityEngine.FontStyle.BoldAndItalic;

}

else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))

{

fontStyle = UnityEngine.FontStyle.Bold;

}

else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))

{

fontStyle = UnityEngine.FontStyle.Italic;

}

else

{

fontStyle = UnityEngine.FontStyle.Normal;

}

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))

{

tmpFontStyle |= TMPro.FontStyles.Italic;

}

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))

{

tmpFontStyle |= TMPro.FontStyles.Bold;

}

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline))

{

tmpFontStyle |= TMPro.FontStyles.Underline;

}

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout))

{

tmpFontStyle |= TMPro.FontStyles.Strikethrough;

}

fontName = txtLayer.Font.Name;

if (txtLayer.TextData.Items.Length > 0)

{

var txtData = txtLayer.TextData.Items[0];

characterSpace = txtData.Style.Tracking * 0.1f;

lineSpace = (float)txtData.Style.Leading * 0.1f;

}

return true;

}

return false;

}

}

}

#endif

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

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

#if UNITY_EDITOR

using System.Collections.Generic;

using UnityEditor;

using UnityEngine;

using UnityGameFramework.Runtime;

namespace UGF.EditorTools.Psd2UGUI

{

public abstract class UIHelperBase : MonoBehaviour

{

public PsdLayerNode LayerNode => this.GetComponent();

private void OnEnable()

{

ParseAndAttachUIElements();

}

///

/// 解析并关联UI元素,并且返回已经关联过的图层(已关联图层不再处理)

///

///

///

public abstract void ParseAndAttachUIElements();

///

/// 获取UI依赖的LayerNodes

///

///

public abstract PsdLayerNode[] GetDependencies();

///

/// 把UI实例进行UI元素初始化

///

///

protected abstract void InitUIElements(GameObject uiRoot);

///

/// 筛选出UI依赖的非空LayerNode

///

///

///

protected PsdLayerNode[] CalculateDependencies(params PsdLayerNode[] nodes)

{

if (nodes == null || nodes.Length == 0) return null;

for (int i = nodes.Length - 1; i >= 0; i--)

{

var node = nodes[i];

if (node == null || node == LayerNode) ArrayUtility.RemoveAt(ref nodes, i);

}

return nodes;

}

internal GameObject CreateUI(GameObject uiInstance = null)

{

if ((int)this.LayerNode.UIType > UGUIParser.UITYPE_MAX || LayerNode.UIType == GUIType.Null) return null;

if (uiInstance == null)

{

var rule = UGUIParser.Instance.GetRule(this.LayerNode.UIType);

if (rule == null || rule.UIPrefab == null)

{

Debug.LogWarning($"创建UI类型{LayerNode.UIType}失败:Rule配置项不存在或UIPrefab为空");

return null;

}

uiInstance = GameObject.Instantiate(rule.UIPrefab, Vector3.zero, Quaternion.identity);

if (LayerNode.IsMainUIType)

{

uiInstance.name = this.name;

var key = uiInstance.GetOrAddComponent();

key.Key = this.gameObject.GetInstanceID().ToString();

}

}

InitUIElements(uiInstance);

return uiInstance;

}

}

}

#endif

1. Text解析器:

#if UNITY_EDITOR

using UnityEngine;

namespace UGF.EditorTools.Psd2UGUI

{

[DisallowMultipleComponent]

public class TextHelper : UIHelperBase

{

[SerializeField] PsdLayerNode text;

public override PsdLayerNode[] GetDependencies()

{

return CalculateDependencies(text);

}

public override void ParseAndAttachUIElements()

{

if (LayerNode.IsTextLayer(out var _))

{

text = LayerNode;

}

else

{

LayerNode.SetUIType(UGUIParser.Instance.DefaultImage);

}

}

protected override void InitUIElements(GameObject uiRoot)

{

var textCom = uiRoot.GetComponentInChildren();

UGUIParser.SetTextStyle(text, textCom);

UGUIParser.SetRectTransform(text, textCom);

}

}

}

#endif

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

internal bool ParseTextLayerInfo(out string text, out int fontSize, out float characterSpace, out float lineSpace, out Color fontColor, out UnityEngine.FontStyle fontStyle, out TMPro.FontStyles tmpFontStyle, out string fontName)

{

text = null; fontSize = 0; characterSpace = 0f; lineSpace = 0f; fontColor = Color.white; fontStyle = FontStyle.Normal; tmpFontStyle = TMPro.FontStyles.Normal; fontName = null;

if (IsTextLayer(out var txtLayer))

{

text = txtLayer.Text;

fontSize = (int)txtLayer.Font.Size;

fontColor = new Color(txtLayer.TextColor.R, txtLayer.TextColor.G, txtLayer.TextColor.B, txtLayer.Opacity) / (float)255;

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold) && txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))

{

fontStyle = UnityEngine.FontStyle.BoldAndItalic;

}

else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))

{

fontStyle = UnityEngine.FontStyle.Bold;

}

else if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))

{

fontStyle = UnityEngine.FontStyle.Italic;

}

else

{

fontStyle = UnityEngine.FontStyle.Normal;

}

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Italic))

{

tmpFontStyle |= TMPro.FontStyles.Italic;

}

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Bold))

{

tmpFontStyle |= TMPro.FontStyles.Bold;

}

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Underline))

{

tmpFontStyle |= TMPro.FontStyles.Underline;

}

if (txtLayer.Font.Style.HasFlag(Aspose.PSD.FontStyle.Strikeout))

{

tmpFontStyle |= TMPro.FontStyles.Strikethrough;

}

fontName = txtLayer.Font.Name;

if (txtLayer.TextData.Items.Length > 0)

{

var txtData = txtLayer.TextData.Items[0];

characterSpace = txtData.Style.Tracking * 0.1f;

lineSpace = (float)txtData.Style.Leading * 0.1f;

}

return true;

}

return false;

}

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

///

/// 根据字体名查找TMP_FontAsset

///

///

///

public static TMP_FontAsset FindTMPFontAsset(string fontName)

{

var fontGuids = AssetDatabase.FindAssets("t:TMP_FontAsset");

foreach (var guid in fontGuids)

{

var fontPath = AssetDatabase.GUIDToAssetPath(guid);

var font = AssetDatabase.LoadAssetAtPath(fontPath);

if (font != null && font.faceInfo.familyName == fontName)

{

return font;

}

}

return null;

}

///

/// 根据字体名查找Font Asset

///

///

///

public static UnityEngine.Font FindFontAsset(string fontName)

{

var fontGuids = AssetDatabase.FindAssets("t:font");

foreach (var guid in fontGuids)

{

var fontPath = AssetDatabase.GUIDToAssetPath(guid);

var font = AssetImporter.GetAtPath(fontPath) as TrueTypeFontImporter;

if (font != null && font.fontTTFName == fontName)

{

return AssetDatabase.LoadAssetAtPath(fontPath);

}

}

return null;

}

2. Image解析器:

#if UNITY_EDITOR

using UnityEngine;

namespace UGF.EditorTools.Psd2UGUI

{

[DisallowMultipleComponent]

public class ImageHelper : UIHelperBase

{

[SerializeField] PsdLayerNode image;

public override PsdLayerNode[] GetDependencies()

{

return CalculateDependencies(image);

}

public override void ParseAndAttachUIElements()

{

image = LayerNode;

}

protected override void InitUIElements(GameObject uiRoot)

{

var imgCom = uiRoot.GetComponentInChildren();

UGUIParser.SetRectTransform(image,imgCom);

imgCom.sprite = UGUIParser.LayerNode2Sprite(image, imgCom.type == UnityEngine.UI.Image.Type.Sliced);

}

}

}

#endif

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

///

/// 把LayerNode图片保存到本地并返回

///

///

/// 若没有设置Sprite的九宫,是否自动计算并设置九宫

///

public static Sprite LayerNode2Sprite(PsdLayerNode layerNode, bool auto9Slice = false)

{

if (layerNode != null)

{

var spAssetName = layerNode.ExportImageAsset(true);

var sprite = AssetDatabase.LoadAssetAtPath(spAssetName);

if (sprite != null)

{

if (auto9Slice)

{

var spImpt = AssetImporter.GetAtPath(spAssetName) as TextureImporter;

var rawReadable = spImpt.isReadable;

if (!rawReadable)

{

spImpt.isReadable = true;

spImpt.SaveAndReimport();

}

if (spImpt.spriteBorder == Vector4.zero)

{

spImpt.spriteBorder = CalculateTexture9SliceBorder(sprite.texture, layerNode.BindPsdLayer.Opacity);

spImpt.isReadable = rawReadable;

spImpt.SaveAndReimport();

}

}

return sprite;

}

}

return null;

}

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

///

/// 自动计算贴图的 9宫 Border

///

///

/// 0-255

///

public static Vector4 CalculateTexture9SliceBorder(Texture2D texture, byte alphaThreshold = 3)

{

int width = texture.width;

int height = texture.height;

Color32[] pixels = texture.GetPixels32();

int minX = width;

int minY = height;

int maxX = 0;

int maxY = 0;

// 寻找不透明像素的最小和最大边界

for (int y = 0; y < height; y++)

{

for (int x = 0; x < width; x++)

{

int pixelIndex = y * width + x;

Color32 pixel = pixels[pixelIndex];

if (pixel.a >= alphaThreshold)

{

minX = Mathf.Min(minX, x);

minY = Mathf.Min(minY, y);

maxX = Mathf.Max(maxX, x);

maxY = Mathf.Max(maxY, y);

}

}

}

// 计算最优的borderSize

int borderSizeX = (maxX - minX) / 3;

int borderSizeY = (maxY - minY) / 3;

int borderSize = Mathf.Min(borderSizeX, borderSizeY);

// 根据边界和Border Size计算Nine Slice Border

int left = minX + borderSize;

int right = maxX - borderSize;

int top = minY + borderSize;

int bottom = maxY - borderSize;

// 确保边界在纹理范围内

left = Mathf.Clamp(left, 0, width - 1);

right = Mathf.Clamp(right, 0, width - 1);

top = Mathf.Clamp(top, 0, height - 1);

bottom = Mathf.Clamp(bottom, 0, height - 1);

return new Vector4(left, top, width - right, height - bottom);

}

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

#if UNITY_EDITOR

using UnityEngine;

using UnityEngine.UI;

namespace UGF.EditorTools.Psd2UGUI

{

[DisallowMultipleComponent]

public class DropdownHelper : UIHelperBase

{

[SerializeField] PsdLayerNode background;

[SerializeField] PsdLayerNode label;

[SerializeField] PsdLayerNode arrow;

[SerializeField] PsdLayerNode scrollView;

[SerializeField] PsdLayerNode toggleItem;

public override PsdLayerNode[] GetDependencies()

{

return CalculateDependencies(background, label, arrow, scrollView, toggleItem);

}

public override void ParseAndAttachUIElements()

{

background = LayerNode.FindSubLayerNode(GUIType.Background, GUIType.Image, GUIType.RawImage);

label = LayerNode.FindSubLayerNode(GUIType.Dropdown_Label, GUIType.Text, GUIType.TMPText);

arrow = LayerNode.FindSubLayerNode(GUIType.Dropdown_Arrow);

scrollView = LayerNode.FindSubLayerNode(GUIType.ScrollView);

toggleItem = LayerNode.FindSubLayerNode(GUIType.Toggle);

}

protected override void InitUIElements(GameObject uiRoot)

{

var dpd = uiRoot.GetComponent();

UGUIParser.SetRectTransform(background, dpd);

var bgImg = dpd.targetGraphic as Image;

bgImg.sprite = UGUIParser.LayerNode2Sprite(background, bgImg.type == Image.Type.Sliced) ?? bgImg.sprite;

UGUIParser.SetTextStyle(label, dpd.captionText);

UGUIParser.SetRectTransform(label, dpd.captionText);

var arrowImg = dpd.transform.Find("Arrow")?.GetComponent();

if (arrowImg != null)

{

UGUIParser.SetRectTransform(arrow, arrowImg);

arrowImg.sprite = UGUIParser.LayerNode2Sprite(arrow, arrowImg.type == Image.Type.Sliced);

}

if (scrollView != null)

{

var svTmp = uiRoot.GetComponentInChildren(true).GetComponent();

if (svTmp != null)

{

var sViewGo = scrollView.GetComponent()?.CreateUI(svTmp.gameObject);

if (sViewGo != null)

{

var sViewRect = sViewGo.GetComponent();

UGUIParser.SetRectTransform(scrollView, sViewRect);

sViewRect.anchorMin = Vector2.zero;

sViewRect.anchorMax = new Vector2(1, 0);

sViewRect.anchoredPosition = new Vector2(0, -2);

}

if (toggleItem != null)

{

var itemTmp = dpd.itemText != null ? dpd.itemText.transform.parent : null;

if (itemTmp != null)

{

toggleItem.GetComponent()?.CreateUI(itemTmp.gameObject);

}

}

}

}

}

}

}

#endif

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

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

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

 工具开源地址:https://github.com/sunsvip/PSD2UGUI_X

相关阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: