书接上回:【Unity编辑器扩展】(一)PSD转UGUI Prefab, Aspose.PSD和Harmony库的使用_TopGames的博客-CSDN博客

工具使用预览:

工具目标:

1. 实现将psd解析生成为UI预制体,并导出UI图片。需支持UGUI和TextMeshProGUI, 如Button、Toggle、ScrollView、Text、Slider、Dropdown、Image、RawImage以及纯色填充图等UGUI元素(ps:纯色填充图:例如PS的纯色图层,通过调整Unity的Color可以达到相同效果,无需导出为纯色图片,以节省资源大小)

2. 要求工具使用简单便捷、并且能满足不同需求。支持自动解析类型和手动调节UI类型和层级。

3. 并且工具不能高度依赖或要求UI设计师必须遵守某种规范。

4. 工具只需要psd文件。不能依赖PS或Unity以外的第三方软件(目前已有的psd转ugui工具中通常依赖安装PS脚本,且对PS版本有严格要求,需强制UI设计师安装使用脚本,这很不方便且难以维护)

工具和工作流设计:

psd图层结构如图所示:

工具实现主要需解决以下问题:

1. 节点层级

从psd图层结构可以看出,psd图层与Unity Transform树状节点完全不同,psd只有"LayerGroup(组)"的概念,一个图层不能成为另一个图层的子图层,只能成为组的子图层。而UGUI对节点的层级有严格的要求,比如Button节点下通常会有个Text显示按钮文字,Text需作为Button的子节点进行布局。而psd中Button图片图层和文字图层无法以父子节点的形式存在。

所以,工具需要把psd图层解析为Unity 节点,每个节点绑定一个psd图层或组,这样程序就可以任意调整节点层级以满足自己的需求。

psd解析为Unity节点以支持层级调整

2. UI类型

psd图层类型最终只能归为两种,图片或文本。然而图片可以是Button、可以是Image,也可以是ScrollView的背景图,所以就需求为psd图层做类型标记,以便自动解析为对应的UI类型。图层名字是存放这一信息的最佳选择,但由于种种原因,你不能强制要求UI设计师遵守某种规范。

所以工具不能过于依赖UI设计师规范化,解析psd图层时先按照命名规范初始化UI类型,同时支持手动以下拉菜单的形式选择UI类型。

psd图层名称以.button(.btn)结尾命名规范,从而实现UI类型标记,如果没有标记则图像图层默认为常用的Image类型(可配RawImage),文本图层默认为Text类型(可配TextMeshProUGUI)。UI类型标识不区分大小写、并且支持自定义配置多种匹配字符,如:Button对应的标识可以配置为bt、btn、button等。

UI类型解析配置界面:

UI类型解析支持自由配置

通过图层命名规范标记UI类型

3. 复合型UI类型

例如ScrollView,有多个UI节点组成,一个ScrollView有背景图、Viewport遮罩图、水平滚动条/垂直滚动条(包含背景和滑块两张图)。这种情况就不是标记一个psd图层能够解决的了。因此可以当成一个组处理,比如把组当成ScrollView,ScrollView的背景图、遮罩图、滚动条对应的图层都包含在这个组中。

4. 实时预览

psd转ugui编辑界面需支持预览效果,如支持预览每个图层或组的图像、UI类型、是否导出图片等设置。

工具面板组成和功能

工作流总结:

1. 将psd文件导入Unity, 右键psd文件,选择Psd2UIForm Editor菜单,自动把psd解析为prefab节点树状图,并打开prefab

2. 若UI设计师未按照规范制作,可手动根据需求调节UI类型或UI层级(可选)

3. 点击扩展按钮,重新解析psd、导出碎图或生成UI界面prefab。(工具会自动记录psd文件最后修改时间,若发生变更,自动弹窗提示是否重新解析)

功能实现:

一,图层解析(psd转节点树状图prefab)

由于PS图层没有树状节点的概念,只有LayerGroup(组)类似树状节点。并且图层遮挡顺序自下而上,与Unity节点相反。Aspose.PSD解析出的layers数组是根据psd图层栏自下而上遍历所有图层。并且如果下一图层属于其它组内图层,还会出现一个SectionDividerLayer层,然后继续遍历图层。而这个SectionDividerLayer作用类似开始标签,如<SectionDividerLayer>和<LayerGroup>是组的开始/结束标签。

Aspose.PSD解析图层数组layers的顺序和索引如下:

1. 定义一个PsdLayerNode MonoBehavior脚本,用于关联到每个psd图层节点,每个PsdLayerNode绑定着一个PS图层(Layer)

2. 把psd文件解析为PsdLayerNode节点树状图:

/// <summary>/// 把Psd图层解析成节点prefab/// </summary>/// <param name="psdPath"></param>/// <returns></returns>public static bool ParsePsd2LayerPrefab(string psdFile, Psd2UIFormConverter instanceRoot = null){if (!File.Exists(psdFile)){Debug.LogError($"Error: Psd文件不存在:{psdFile}");return false;}var texImporter = AssetImporter.GetAtPath(psdFile) as TextureImporter;if (texImporter.textureType != TextureImporterType.Sprite){texImporter.textureType = TextureImporterType.Sprite;texImporter.mipmapEnabled = false;texImporter.alphaIsTransparency = true;texImporter.SaveAndReimport();}var prefabFile = GetPsdLayerPrefabPath(psdFile);var rootName = Path.GetFileNameWithoutExtension(prefabFile);bool needDestroyInstance = instanceRoot == null;Psd2UIFormConverter rootLayer;if (instanceRoot != null){rootLayer = instanceRoot;//清空已有节点重新解析for (int i = rootLayer.transform.childCount - 1; i >= 0; i--){GameObject.DestroyImmediate(rootLayer.transform.GetChild(i).gameObject);}}else{rootLayer = CreatePsdLayerRoot(rootName);}rootLayer.SetPsdAsset(AssetDatabase.LoadAssetAtPath<UnityEngine.Sprite>(psdFile));rootLayer.psdAssetChangeTime = GetAssetChangeTag(psdFile);var psdOpts = new PsdLoadOptions(){LoadEffectsResource = true};using (var psd = Aspose.PSD.Image.Load(psdFile, psdOpts) as PsdImage){List<GameObject> layerNodes = new List<GameObject> { rootLayer.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;}}}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 void SetPsdAsset(Sprite texture2D){this.psdAsset = texture2D;if (string.IsNullOrWhiteSpace(this.uiFormName)){this.uiFormName = this.psdAsset.name;}}/// <summary>/// 获取解析好的psd layers文件/// </summary>/// <param name="psd"></param>/// <returns></returns>public static string GetPsdLayerPrefabPath(string psd){return UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(psd), Path.GetFileNameWithoutExtension(psd) + "_psd_layers_parsed.prefab");}private static Psd2UIFormConverter CreatePsdLayerRoot(string rootName){var node = new GameObject(rootName);node.gameObject.tag = "EditorOnly";var layerRoot = node.AddComponent<Psd2UIFormConverter>();return layerRoot;}private static PsdLayerNode CreatePsdLayerNode(Layer layer, int bindLayerIdx){string nodeName = layer.Name;if (string.IsNullOrWhiteSpace(nodeName)){nodeName = $"PsdLayer-{bindLayerIdx}";}else{if (Path.HasExtension(layer.Name)){nodeName = Path.GetFileNameWithoutExtension(layer.Name);}}var node = new GameObject(nodeName);node.gameObject.tag = "EditorOnly";var layerNode = node.AddComponent<PsdLayerNode>();layerNode.BindPsdLayerIndex = bindLayerIdx;InitLayerNodeData(layerNode, layer);return layerNode;}

二、显示预览图

事实上步骤一的解析只是把psd图层转换为可修改层级的GameObject节点,此时处于编辑阶段, 节点上也没有任何Render组件,并没有(也不应该)把图层导出成图片给节点显示。

编辑阶段还不明确用户需要导出图片的图层(而且若导出图片会触发Unity自动导入资源,影响体验),出于工具运行效率和用户体验的考量,不能直接生成图片文件。而是设计为用户首次点选节点时,把节点对应的图层转换为Texture2D实例以便预览,用户没点击的节点就不会转换。当点击导出碎图按钮时,标记为导出但没有转换为Texture2D实例的图层再执行转换操作。这样就大大提高了工具解析速度和效率。

1. 点选节点时,把psd图层的Bitmap转成Unity的Texture2D类型作为预览图(支持把psd整个组转换成一张图):

/// <summary>/// 把psd图层转成Texture2D/// </summary>/// <param name="psdLayer"></param>/// <returns>Texture2D</returns>public Texture2D ConvertPsdLayer2Texture2D(){if (BindPsdLayer == null || BindPsdLayer.Disposed) return null;var parentNode = transform.parent.GetComponent<PsdLayerNode>();Rectangle bounds;if (parentNode == null || (this.LayerType == PsdLayerType.LayerGroup && parentNode.LayerType != PsdLayerType.LayerGroup)){bounds = BindPsdLayer.GetFixedLayerBounds();}else{bounds = BindPsdLayer.Bounds;}MemoryStream ms = new MemoryStream();var pngOpt = new PngOptions(){ColorType = Aspose.PSD.FileFormats.Png.PngColorType.TruecolorWithAlpha};BindPsdLayer.Save(ms, pngOpt, bounds);var buffer = new byte[ms.Length];ms.Position = 0;ms.Read(buffer, 0, buffer.Length);Texture2D texture = new Texture2D(bounds.Width, bounds.Height);texture.alphaIsTransparency = true;texture.LoadImage(buffer);texture.Apply();ms.Dispose();return texture;}

这里把psd组转成一张图会有一个坑,当组为最外层组时,Aspose.PSD库无法获取正确的图片区域(Rectangle),导致生成的图片有位置偏移。为了绕过这个坑我是通过遍历计算组的所有子图层区域,从而得到一个合并区域:

/// <summary>/// 修复当LayerGroup为第一层时,对应Bounds错位/// </summary>/// <param name="layerGroup"></param>/// <returns></returns>public static Rectangle GetFixedLayerBounds(this Layer layerGroup){if (layerGroup.GetLayerType() != PsdLayerType.LayerGroup){return layerGroup.Bounds;}//根据子图层算出包围所有子图层的最小包围盒var subLayers = (layerGroup as LayerGroup).Layers;int minLeft = int.MaxValue;int minTop = int.MaxValue;int maxRight = int.MinValue;int maxBottom = int.MinValue;foreach (var item in subLayers){var itemTp = item.GetLayerType();if (itemTp == PsdLayerType.Unknown || itemTp == PsdLayerType.LayerGroup || itemTp == PsdLayerType.SectionDividerLayer) continue;var itemBounds = item.Bounds;if (item.Left < minLeft) minLeft = item.Left;if (item.Top < minTop) minTop = item.Top;if (item.Right > maxRight) maxRight = item.Right;if (item.Bottom > maxBottom) maxBottom = item.Bottom;}//var result = new Rectangle(new Point(minLeft, minTop), new Size(maxRight - minLeft, maxBottom - minTop));var result = new Rectangle(){Top = minTop,Left = minLeft,Right = maxRight,Bottom = maxBottom};return result;}

2. 重写HasPreviewGUI、OnPreviewGUI、GetInfoString函数以自定义显示Inspector面板的预览界面:

[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);}}serializedObject.ApplyModifiedProperties();}public override bool HasPreviewGUI(){return targetLogic.PreviewTexture != null;}public override void OnPreviewGUI(Rect r, GUIStyle background){GUI.DrawTexture(r, targetLogic.PreviewTexture, ScaleMode.ScaleToFit);//base.OnPreviewGUI(r, background);}public override string GetInfoString(){//var size = targetLogic.PreviewTexture.Size();return targetLogic.LayerInfo;}}

点击节点预览图层

3. Scene界面显示图层边界debug框

PS的坐标系左上角为原点(0,0),而Unity Scene界面坐标系原点为视口中心。把Aspose.PSD Rectangle转换为Unity Rect:

/// <summary>/// 获取psd图层的Rect边框(Unity坐标系)/// </summary>/// <param name="layer"></param>/// <returns></returns>public static Rect GetLayerRect(this Layer layer){var layerTp = layer.GetLayerType();int left, right, top, bottom;if (layerTp == PsdLayerType.LayerGroup){var bounds = layer.GetFixedLayerBounds();left = bounds.Left;right = bounds.Right;top = bounds.Top;bottom = bounds.Bottom;}else{left = layer.Left;right = layer.Right;top = layer.Top;bottom = layer.Bottom;}float halfWidth = Mathf.Abs(right - left) * 0.5f;float halfHeight = Mathf.Abs(bottom - top) * 0.5f;var canvasSize = layer.Container.Size;Rect result = new Rect(left + halfWidth - canvasSize.Width * 0.5f, canvasSize.Height - (top + halfHeight) - canvasSize.Height * 0.5f, right - left, bottom - top);return result;}

为PsdLayerNode写一个CustomEditor,当点选节点时就会触发OnEnable方法,在首次OnEnable时生成预览Texture2D:

[CanEditMultipleObjects][CustomEditor(typeof(PsdLayerNode))]public class PsdLayerNodeInspector : Editor{PsdLayerNode targetLogic;private void OnEnable(){targetLogic = target as PsdLayerNode;targetLogic.RefreshLayerTexture();}}[ExecuteInEditMode][DisallowMultipleComponent]public class PsdLayerNode : MonoBehaviour{[ReadOnlyField] public int BindPsdLayerIndex = -1;[ReadOnlyField][SerializeField] PsdLayerType mLayerType = PsdLayerType.Unknown;[SerializeField] public bool ExportImage;[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; }/// <summary>/// 绑定的psd图层/// </summary>private Layer mBindPsdLayer;public Layer BindPsdLayer{get => mBindPsdLayer;set{mBindPsdLayer = value;mLayerType = mBindPsdLayer.GetLayerType();LayerRect = mBindPsdLayer.GetLayerRect();LayerInfo = $"{LayerRect}";}}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;}}

在根节点的Psd2UIFormConverter脚本的OnGizmos方法中统一绘制所有图层节点边框:

private void OnDrawGizmos(){if (drawLayerRectGizmos){var nodes = this.GetComponentsInChildren<PsdLayerNode>();Gizmos.color = drawLayerRectGizmosColor;foreach (var item in nodes){if (item.NeedExportImage()){Gizmos.DrawWireCube(item.LayerRect.position * 0.01f, item.LayerRect.size * 0.01f);}}}}

图层区域debug框

三,导出UI碎图

可通过勾选控制需要导出的图片,被其它UI元素引用的图层即使不勾选也会导出图片。导出的图片Unity默认为Texture类型,所以还需要用代码转换为Sprite类型。

代码实现:

/// <summary>/// 导出psd图层为Sprites碎图/// </summary>/// <param name="psdAssetName"></param>internal void ExportSprites(){var exportLayers = this.GetComponentsInChildren<PsdLayerNode>().Where(node => node.NeedExportImage());var exportDir = UtilityBuiltin.ResPath.GetCombinePath(Path.GetDirectoryName(PsdAssetName), this.uiFormName);if (!Directory.Exists(exportDir)){Directory.CreateDirectory(exportDir);}int exportIdx = 1;int totalCount = exportLayers.Count();foreach (var layer in exportLayers){if (layer.RefreshLayerTexture()){var bytes = layer.PreviewTexture.EncodeToPNG();var imgName = string.IsNullOrWhiteSpace(layer.name) ? $"UI_Layer_{layer.BindPsdLayerIndex}" : layer.name;var imgFileName = UtilityBuiltin.ResPath.GetCombinePath(exportDir, imgName + ".png");if (File.Exists(imgFileName)){imgFileName = UtilityBuiltin.ResPath.GetCombinePath(exportDir, imgName + $"_{layer.BindPsdLayerIndex}.png");}EditorUtility.DisplayProgressBar($"导出({exportIdx}/{totalCount})", $"导出Image文件:{imgFileName}", exportIdx / (float)totalCount);File.WriteAllBytes(imgFileName, bytes);}}EditorUtility.ClearProgressBar();AssetDatabase.Refresh();ConvertTextureToUIByDir(exportDir);}/// <summary>/// 把指定目录下所有Texture转换为Sprite类型/// </summary>/// <param name="dir"></param>public static void ConvertTextureToUIByDir(string dir){var guidArr = AssetDatabase.FindAssets("t:Texture", new string[] { dir });foreach (var item in guidArr){var assetName = AssetDatabase.GUIDToAssetPath(item);var texImporter = AssetImporter.GetAtPath(assetName) as TextureImporter;if (texImporter == null) continue;texImporter.textureType = TextureImporterType.Sprite;texImporter.spriteImportMode = SpriteImportMode.Single;texImporter.alphaSource = TextureImporterAlphaSource.FromInput;texImporter.alphaIsTransparency = true;texImporter.mipmapEnabled = false;texImporter.SaveAndReimport();}}

【Unity编辑器扩展】(二)PSD转UGUI Prefab, 图层解析和碎图导出相关推荐

  1. 【Unity编辑器扩展】语言国际化工具,生成多语言Excel自动翻译并导出多语言表

    工具效果如图: 多语言是个非常简单且常用的功能.但是重复工作量大,程序手动把多语言Key配置到多语言表经常会出现错漏,或者几经改版,有些Key已经不用却没有剔除,久而久之造成冗余.这中简单且重复的工作 ...

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

    工具效果: 第一步,把psd图层转换为可编辑的节点树,并自动解析UI类型.自动绑定UI子元素: 第二步, 点击"生成UIForm"按钮生成UI预制体 (若有UI类型遗漏可在下拉菜单 ...

  3. 【Unity编辑器扩展】(一)PSD转UGUI Prefab, Aspose.PSD和Harmony库的使用

    [Unity编辑器扩展](二)PSD转UGUI Prefab, 图层解析和碎图导出_psd导入unity_TopGames的博客-CSDN博客 [Unity编辑器扩展](三)PSD转UGUI Pref ...

  4. Unity 编辑器扩展菜单

    Unity 编辑器扩展菜单 目录 Unity 编辑器扩展菜单 一.MenuItem 特性 菜单栏扩展 1.创建多级菜单 2.创建可使用快捷键的菜单项 3.创建可被勾选的菜单项 4.检查菜单是否使用 5 ...

  5. Unity编辑器扩展: 程序化打图集工具

    开始前的声明:该案例中图集所使用图片资源均来源于网络,仅限于学习使用 一.前言 关于编辑器扩展相关的知识,在前面的两篇内容中做了详细的描述,链接地址: 第一篇 :Unity编辑器扩展 UI控件篇 第二 ...

  6. 【Unity编辑器扩展实践】、查找所有引用该图片的预制体

    上一篇Unity编辑器扩展实践二.通过代码查找所有预制中已经查到到所有的预制体了. 然后我们就可以用这些预制体做一些其他的操作了,比如查找该预制的资源引用.可以直接遍历预制,找到预制里面的所有Imag ...

  7. Unity编辑器扩展之EditorWindow

    Unity编辑器扩展之EditorWindow 继承这个类的编辑器脚本可以用来创建一个编辑器窗口,类似Inspector窗口 若要在这个类中添加一些控件,可以使用GUI和GUILayout控件,还可以 ...

  8. Unity编辑器扩展 UI控件篇

    前摇 :认识编辑器扩展的必要性 由于各种各样的原因,无论是移动端亦或是主机/PC端,进几年的发行的游戏体量是越来越大.通常来说大体量的游戏开发需要一套很成熟完善的工作流,亦或说有很强的工业化的能力,像 ...

  9. Unity编辑器扩展-生成prefab的预览图并保存为图片

    Project视图里的prefab文件都是一个蓝色的正方体,我想给它们加个预览图,暂时不知道怎么改Project视图的,但反正我想像NGUI那样有个专门的窗口存放一些常用的prefab,所以做成了如下 ...

最新文章

  1. Android Animation学习(五) ApiDemos解析:容器布局动画 LayoutTransition
  2. 华为4G路由器成软银快速部署宽带业务新利器
  3. mongoDB 数据库简介
  4. JavaScript GetAbsoultURl
  5. 奇点云集聚数据中台优势,加速企业数智化升级
  6. HDFS源码解析:教你用HDFS客户端写数据
  7. Windows_cmd_命令
  8. MongoDB数据库学习
  9. BZOJ 3326 [SCOI2013]数数 (数位DP)
  10. Julia: bug? = split ,isnumber
  11. 计算机组装常用工具有锤子,工具箱中常用的五金工具有哪些?
  12. Avalondock 第二步 创建文档面板
  13. dorado 7 注意总结
  14. 1.从第一道面试题谈起
  15. mac tortoisesvn客户端_TortoiseSVN
  16. erlang中的ets和dets
  17. 会议OA项目之我的审批签字功能
  18. 流体连续性方程【The Equation of Continuity】
  19. 抽奖活动的奖品怎么设置?
  20. 基于JAVA社区老人健康服务跟踪系统计算机毕业设计源码+数据库+lw文档+系统+部署

热门文章

  1. 原生js与css3结合的电风扇
  2. Zabbix监控系统开发(2):JSON多维数组筛选字段是否包含字符串的解决方案
  3. [苹果开发者账号]05 换收款的银行账号
  4. python 好玩_好用好玩的Python包
  5. 可喜可贺,又一ThinkPHP 5.1开源多用户商城系统上架了商家客户端
  6. 小黄鸡的秃头日记【JAVA】---循环
  7. c语言算数运算,C语言:算数运算符
  8. uni-app rtm插件集成指南及常见问题--iOS
  9. 数据分析思维模式(上)
  10. 湖南电信分公司客服中心提升服务质量的对策研究