日常开发中,UI制作一般是美术在PS中制作效果图,然后编写文档告诉客户端UI使用了哪些图片,图片的像素大小,字体的大小等等,客户端程序再根据这些文档制作UI布局。耗时耗力!客户端程序员难以通过肉眼确定UI图片的具体位置,图片的大小也经常没有在文档中指定出来,制作非常麻烦。如果能直接将美术制作的PSD文件解析并自动在Unity中生成UI,可以节省很多时间精力。利用PS的脚本功能,我们可以编写PS脚本,将PS中的UI图层信息导出到XML文件中,这样Unity就能读取解析XML文件,并自动生成NGUI界面。
一:导出PS图层信息到XML文件。PS支持JS脚本,还自带了编辑器ExcendScript来调试,非常良心。相关API可以在此查询:http://www.adobe.com/cn/devnet/photoshop/scripting.html。以下就是导出XML的JS代码,保存为.JSX文件即可:
var doc = app.activeDocument var out ="" //创建存储遍历结果的变量 var depth = 0; var root = new XML("<root/>"); getLayers(doc.layers, 0)//遍历图层 var xml = root.toXMLString(); var file = new File(); var fileNew = file.saveDlg("Save new file", "*.xml"); if (fileNew) { fileNew.open("w"); fileNew.write(xml); fileNew.close(); } function getLayers(layers, count) { for (var i =0; i<layers.length; i++) { if(!layers[i].visible) { continue; } if(layers[i].typename == "LayerSet")//判断是否是图层组 { //out = out + "\n " + repeat(" ",count) +"[" + layers[i].name +"]"; var group = new XML("<group/>"); group.@name = layers[i].name; group.@parnet = layers[i].parent.name; group.@opacity = layers[i].opacity.toFixed(0); root.appendChild(group); getLayers(layers[i].layers, count+1);//递归 } else { //out = out + "\n " + repeat(" ",count) + layers[i].name + "," + layers[i].bounds; var layer = new XML("<layer/>"); layer.@name = layers[i].name; layer.@index = depth; layer.@kind = layers[i].kind; layer.@parnet = layers[i].parent.name; layer.@bounds = layers[i].bounds; layer.@opacity = layers[i].opacity.toFixed(0); if(layers[i].kind == LayerKind.TEXT) { layer.@r = layers[i].textItem.color.rgb.red.toFixed(0); layer.@g = layers[i].textItem.color.rgb.green.toFixed(0); layer.@b = layers[i].textItem.color.rgb.blue.toFixed(0); layer.@txt = layers[i].textItem.contents; layer.@font = layers[i].textItem.font; } root.appendChild(layer); depth++; } } }执行后(PS中文件->脚本->浏览载入运行或直接在ExcendScript中运行),生成的XML文件如下:
<root> <layer name="comm_1_icon_coppercup" index="0" kind="LayerKind.NORMAL" parnet="比赛奖励.psd" bounds="277 px,277 px,332 px,341 px" opacity="100"/> <layer name="comm_1_icon_silvercup" index="1" kind="LayerKind.SMARTOBJECT" parnet="比赛奖励.psd" bounds="277 px,193 px,332 px,257 px" opacity="100"/> <layer name="comm_1_icon_goldcup" index="2" kind="LayerKind.SMARTOBJECT" parnet="比赛奖励.psd" bounds="276 px,103 px,331 px,167 px" opacity="100"/> <group name="组 462" parnet="比赛奖励.psd" opacity="100"/> </root>
layer节点是一个图层的信息,group节点是图层组。有了这些信息后,我们就能在Unity中解析并构建UI布局。
二:Unity中读取并构建UI布局。首先是读取文件:
string path = EditorUtility.OpenFilePanel("XML For UI", "", "xml"); if (!string.IsNullOrEmpty(path)) { FileStream aFile = new FileStream(path, FileMode.Open); if (aFile != null) { StreamReader sr = new StreamReader(aFile, Encoding.Default); string strXML = sr.ReadToEnd(); }}然后解析XML并构建UI:
XmlDocument doc = new XmlDocument(); doc.LoadXml(strXML); XmlNode nodeRoot = doc.SelectSingleNode("root"); int length = nodeRoot.ChildNodes.Count; foreach (XmlNode node in nodeRoot) { XmlAttributeCollection attrColl = node.Attributes; XmlAttribute attrName = (XmlAttribute)attrColl.GetNamedItem("name"); XmlAttribute attrIndex = (XmlAttribute)attrColl.GetNamedItem("index"); XmlAttribute attrKind = (XmlAttribute)attrColl.GetNamedItem("kind"); XmlAttribute attrParent = (XmlAttribute)attrColl.GetNamedItem("parnet"); XmlAttribute attrBounds = (XmlAttribute)attrColl.GetNamedItem("bounds"); XmlAttribute attrOpacity = (XmlAttribute)attrColl.GetNamedItem("opacity"); XmlAttribute attrR = (XmlAttribute)attrColl.GetNamedItem("r"); XmlAttribute attrG = (XmlAttribute)attrColl.GetNamedItem("g"); XmlAttribute attrB = (XmlAttribute)attrColl.GetNamedItem("b"); XmlAttribute attrTxt = (XmlAttribute)attrColl.GetNamedItem("txt"); XmlAttribute attrFont = (XmlAttribute)attrColl.GetNamedItem("font"); string name = attrName.Value; string parent = attrParent.Value; int opac = int.Parse(attrOpacity.Value); if (node.Name == "group") { GameObject gobjParent = GetParent(parent); GameObject gobjGroup = new GameObject(name); gobjGroup.transform.parent = gobjParent.transform; gobjGroup.transform.localPosition = Vector3.zero; gobjGroup.transform.localScale = Vector3.one; } else if(node.Name == "layer") { int index = int.Parse(attrIndex.Value); string kind = attrKind.Value; string strBounds = attrBounds.Value; Bounds bounds = GetBounds(strBounds); if (kind.Contains(".TEXT")) { //文本 byte R = byte.Parse(attrR.Value); byte G = byte.Parse(attrG.Value); byte B = byte.Parse(attrB.Value); string txt = attrTxt.Value; string font = attrFont.Value; GameObject gobjParent = GetParent(parent); UILabel uiLab = NGUISettings.AddLabel(gobjParent); uiLab.text = txt; uiLab.color = new Color32(R, G, B,(byte)(opac / 100f * 255)); // 层级 uiLab.depth = length - index; //大小 uiLab.transform.localPosition = new Vector3(bounds.center.x, bounds.center.y, 0); uiLab.overflowMethod = UILabel.Overflow.ResizeFreely; uiLab.fontSize = (int)bounds.size.y; uiLab.applyGradient = false; //uiLab.SetDimensions((int)bounds.size.x, (int)bounds.size.y); } else { //图片 GameObject gobjParent = GetParent(parent); UIAtlas atlas = GetAtlas(name); UISprite txu = NGUITools.AddSprite(gobjParent, atlas, name); //层级 txu.depth = length - index; //大小 txu.transform.localPosition = new Vector3(bounds.center.x, bounds.center.y, 0); txu.SetDimensions((int)bounds.size.x, (int)bounds.size.y); //alpha txu.alpha = opac / 100f; } } }
要根据parent属性确定父物体,
private static GameObject GetParent(string parent) { GameObject gobjParent = null; if (parent.Contains(".psd")) { gobjParent = Selection.activeGameObject; } else { gobjParent = GameObject.Find(parent); } return gobjParent; }
根据name属性确定并载入图集:
public static UIAtlas GetAtlas(string name) { string path = "Assets/ResourcesHot/Atlas/pcomm/pcomm.prefab"; if (name.StartsWith("comm")) { path = "Assets/ResourcesHot/Atlas/pcomm/pcomm.prefab"; } else if (name.StartsWith("battle")) { path = "Assets/ResourcesHot/Atlas/battle/battle.prefab"; } else if (name.StartsWith("icon")) { path = "Assets/ResourcesHot/Atlas/icon/icon.prefab"; } GameObject gobjAtlas = AssetDatabase.LoadMainAssetAtPath(path) as GameObject; UIAtlas atlas = gobjAtlas.GetComponent<UIAtlas>(); return atlas; }
根据bounds属性确定位置及大小:
private static Bounds GetBounds(string bounds) { string[] vals = bounds.Split(','); string valLeft = vals[0].Replace(" px", ""); string valUp = vals[1].Replace(" px", ""); string valRight = vals[2].Replace(" px", ""); string valBottom = vals[3].Replace(" px", ""); int left = int.Parse(valLeft); int right = int.Parse(valRight); int up = int.Parse(valUp); int bottom = int.Parse(valBottom); int width = right - left; int height = bottom - up; int x = Mathf.RoundToInt(left + width / 2f - mSceneWidth / 2f); int y = Mathf.RoundToInt(mSceneHeight / 2f - up - height / 2f); return new Bounds(new Vector3(x, y), new Vector3(width, height)); }
PS中的bounds属性是这样的:bounds="277 px,277 px,332 px,341 px",分别为左上及右下点坐标,而且原点在画布左上角。我们需要处理成NGUI的坐标系。