PS自动导出切图并在Unity中自动搭建UGUI

更新时间:2022-06-08 10:35:41

由于公司功能重复性项目很多,故思考用自动化脚本代替原本繁杂的拼UI的工作。还能减少被UI喷UI摆的位置不对的问题。网上搜有PSD2UGUI这个插件,但是要30刀。我需要的功能应该没有插件那么复杂,凭借我多年复制粘贴工程师的代码经验,觉得自己应该也能写出来。

设计思路:

1,PS执行自动化脚本将需要的切图切出,并保存好图片在PS中的位置。

2,Unity编写editor脚本,根据导入的PS图片,自动生成相关UI布局,并绑定相关脚本。

步骤一:PS自动切图

PS脚本是用JS写的,官方给的编辑器是AdobeExtendScriptToolkit。由于我比较懒用的绿色版装不上就直接用记事本写了。直接上代码:

//用户选择导出的文件夹
var outputFolder = Folder.selectDialog("选择输出的文件夹");

var doc = app.activeDocument;

var layers = app.activeDocument.layers;



//遍历所有父级层。
for(var i=0; i<layers.length; i++)
{	
	var childlayer = layers[i].layers;
	//alert(layers[i].name);
	FindLayers(outputFolder+"/"+layers[i].name,layers[i]);			
}
//递归调用每个层级,遇到图层就导出,否则继续递归
function FindLayers(parentname,_currentLayer){
	if(_currentLayer instanceof LayerSet){		
		for(var i=0; i<_currentLayer.layers.length; i++){	
			nowdir = parentname +"/";
			checkFolder(nowdir);
			nowdir += _currentLayer.layers[i].name; 
			FindLayers(nowdir,_currentLayer.layers[i]);
		}						
	}else{
		//alert(parentname+"isImage");	
		SolveLayer(_currentLayer,parentname);
	}
}
//单独导出图片
function SolveLayer(mylayer,filename){
	mylayer.copy();			
	var bounds = mylayer.bounds;
	var posx = bounds[0].toString();
	posx = posx.replace(' ','_');
	var posy = bounds[1].toString();
	posy = posy.replace(' ','_');
		//计算当前图层的宽度,为范围数组变量的第三个值与第一个值的差。
	var width= bounds[2] - bounds[0];
		//计算当前图层的高度,为范围数组变量的第四个值与第二个值的差。
	var height = bounds[3] - bounds[1];
			//alert("width:"+width+"height:"+height);
		//创建一个新文档,新文档的尺寸为拷贝到内存中图层的尺寸一致。
	app.documents.add(width, height, 72, "myDocument", NewDocumentMode.RGB, 
DocumentFill.TRANSPARENT);

	//将内存中的图层,复制到新文档。
	app.activeDocument.paste();
	var newfilename = filename+"_"+posx+"_"+posy+".png";
	//alert(newfilename);
	var file = new File(newfilename);
	SaveImage(doc,file);
	app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);
}

function checkFolder(path){
var folder = Folder(path)
if(!folder.exists) folder.create()
}


function SaveImage(doc,saveDir){	
	var options = PNGSaveOptions;//保存png模式
	var asCopy = true;//副本方式保存
	var extensionType = Extension.LOWERCASE;//拓展名小写
	doc.saveAs(saveDir, options, asCopy, extensionType);
}

代码是百度各个功能然后拼一起的。没怎么研究API,如果哪位大佬有更好的写法,请指出。文件保存成.js或者.jsx都行。.jsx好点,PS默认读取.jsx。可以节约重新选择文件类型的时间,至少多摸鱼2秒钟。

PSD文件内需要切图的图片需要提前处理好,每个图层就是一个切图。文字需要栅格化,形状需要先转换成智能对象再栅格化。各位请自行处理,如果不会可以呼叫UI。

注意!:需要将PS内的单位设置成像素,否则在UGUI中无法自动准确的定位。设置方法:

在菜单栏中的 视图->标尺。或者直接ctrl+R 打开标尺.  在标尺处右键,选择像素。如下图所示:

PS点击  文件->脚本->浏览  选中刚才的.jsx结尾的脚本。脚本运行后选择导出的文件路径,不要在运行过程中操作PS。脚本会根据图层关系,自动生成相应的文件关系。

步骤二:UGUI自动创建。

在unity项目中创建Editor文件夹,文件夹下任意创建个C#脚本。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;

public class ConvertTexture2Sprite : Editor
{
private static Dictionary<string, Sprite> SpriteDic = new Dictionary<string, Sprite>();
private static Dictionary<string, Image> CreatedImageDic = 
new Dictionary<string,Image>();
private static List<Image> NoPaireToggleList = new List<Image>();
[MenuItem("Convert/ConvertSprite")]
// Update is called once per frame
public static void ChangeSprite()
{
var selection = Selection.GetFiltered<Texture>(SelectionMode.DeepAssets);

foreach (var item in selection)
{
var path = AssetDatabase.GetAssetPath(item);
var import = (TextureImporter)AssetImporter.GetAtPath(path);
import.textureType = TextureImporterType.Sprite;
import.SaveAndReimport();
}
Debug.Log("转换完成:"+selection.Length);
}

[MenuItem("Tools/GenerateGUI")]
public static void AutoGUI()
{
SpriteDic.Clear();
CreatedImageDic.Clear();
NoPaireToggleList.Clear();
var selections = Selection.GetFiltered<Texture>(SelectionMode.DeepAssets);
foreach (var item in selections)
{
var path = AssetDatabase.GetAssetPath(item);
var icon = AssetDatabase.LoadAssetAtPath<Sprite>(path);
SpriteDic.Add(path, icon);
}
CreateUI(SpriteDic);

//foreach (var item in SpriteDic)
//{
//var re = GetRelativePath(item.Key);
//Debug.Log(re);
//Debug.Log(item.Key + ":" + item.Value);
//}
}

private static void CreateUI(Dictionary<string, Sprite> spriteDic)
{
var canvas = GameObject.Find("Canvas");
if (canvas == null)
{
Debug.Log("找不到canvas,请手动生成,并命名为Canvas");
return;
}
foreach (var item in spriteDic)
{
var relativePath = GetRelativePath(item.Key);
var layers = relativePath.Split('/');
var currentParent = canvas.transform;
for (int i = 0; i < layers.Length; i++)
{
currentParent = CheckPathAndAutoCreate(currentParent, layers[i]);
}
var imagename = GetName(item.Value.name);
var image = new GameObject(imagename, typeof(RectTransform), typeof(Image)).GetComponent<Image>();
image.transform.SetParent(currentParent);
var rect = image.GetComponent<RectTransform>();
rect.anchorMin = new Vector2(0, 1);
rect.anchorMax = new Vector2(0, 1);
rect.pivot = new Vector2(0, 1);
image.sprite = item.Value;
image.SetNativeSize();
var pos = GetImagPos(item.Value.name);
rect.anchoredPosition = pos;
 
Debug.Log(image.name);
CreatedImageDic[image.name] = image;
if (imagename.Contains("_On")|| imagename.Contains("_on")) {
var sourcename = imagename.Replace("_On", "").Replace("_on","");
if (CreatedImageDic.ContainsKey(sourcename))
{
var targetimage = CreatedImageDic[sourcename];
var to = targetimage.gameObject.AddComponent<Toggle>();
to.graphic = image;
image.transform.SetParent(targetimage.transform);
}
else {
NoPaireToggleList.Add(image);
}
}
if (imagename.Contains("_Btn")|| imagename.Contains("_btn")) {
image.gameObject.AddComponent<Button>();
}

RestPivot(rect);
 
}
for (int i = 0; i < NoPaireToggleList.Count; i++)
{
var sourcename = NoPaireToggleList[i].name.Replace("_On", "");
if (CreatedImageDic.ContainsKey(sourcename))
{
var targetimage = CreatedImageDic[sourcename];
var to = targetimage.gameObject.AddComponent<Toggle>();
to.graphic = NoPaireToggleList[i];
NoPaireToggleList[i].transform.SetParent(targetimage.transform);
}
else
{
Debug.LogError("can't find ToggleP is" + sourcename);
}
}
}
private static void RestPivot(RectTransform rect) {

var tpos = rect.transform.position;
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.transform.position = tpos;
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition += new Vector2(rect.sizeDelta.x / 2f, -rect.sizeDelta.y / 2f);
}

private static string GetName(string name)
{
var names = name.Split('_');
var tempname = "";
if (names.Length < 4)
return name;
for (int i = 0; i < names.Length - 4; i++)
{
tempname += names[i] + "_";
}
return tempname.Substring(0,tempname.Length-1);
}

private static Vector2 GetImagPos(string name)
{
if (!name.Contains("px"))
{
Debug.LogError(name + "图片不含关键字px");
return Vector2.zero;
}
var shortname = name.Replace("_px", "");
var elems = shortname.Split('_');
if (elems.Length < 2)
return Vector2.zero;
var length = elems.Length;
var x = int.Parse(elems[length - 2]);
var y = int.Parse(elems[length - 1]);
return new Vector2(x, -y);
}

private static Transform CheckPathAndAutoCreate(Transform p, string path)
{

var child = p.Find(path);
if (child == null)
{
child = new GameObject(path, typeof(RectTransform)).transform;
child.transform.SetParent(p);
var rect = child.GetComponent<RectTransform>();
rect.anchorMin = new Vector2(0, 0);
rect.anchorMax = new Vector2(1, 1);
rect.sizeDelta = Vector2.zero;
rect.anchoredPosition = Vector3.zero;
}
return child;
}

public static string GetRelativePath(string source)
{
var firstindex = source.IndexOf("/");
var lastindex = source.LastIndexOf("/");
var result = source.Substring(firstindex + 1, lastindex - firstindex - 1);
return result;
}

[MenuItem("Tools/SimpleUI")]
public static void SimpleUI() {
var select = Selection.activeGameObject;
var images = select.GetComponentsInChildren<Image>(true);
for (int i = 0; i < images.Length; i++)
{
var parent = images[i].transform.parent;
var image = parent.GetComponent<Image>();
if (parent.childCount == 1&& image == null) {
var grandparent = parent.transform.parent;
if (grandparent != null) {
images[i].transform.SetParent(grandparent);
DestroyImmediate(parent.gameObject);
}

}
}
}



}

该脚本共提供三个方法,在菜单栏上会出现

一:Convert->convertSprite.该方法可以把导入的texture批量转换成sprite。使用方法:

在Project面板中选中刚才导入的切图完毕的目录文件夹。点击convert->convertsprite。

二:Tools->GenerateGUI.该方法可以将导入的切图各元素根据像素位置拼好。使用方法:

先确保场景里存在一个Canvas,且名称是默认的。在project中选中刚才转换好sprite的目录文件夹,点击Tools->GenerateGUI。程序会自动生成相应UGUI。

三:Tools->SimpleUI,用于简化UI层级结构。由于PS导出的结构可能会存在多层空的父物体,故设计了该功能用于简化层级结构,可以多点几次。

至此整个自动化脚本流程结束,各位可以自行设定相应规则进行button和Toggle等组件的创建,比如上述脚本中会将以"_Btn"结尾的切图自动挂载上button组件。将“_On”结尾的切图自动匹配上没有“On”结尾的图片且成为一个Toggle。

PS:由于PS的层级结构是最上面的图层在最上面,跟UGUI相反,各位记得做相应的调整。