FastGUI For NGUI 3.0.X ——从PSD到NGUI UI 工具

阅读更多

FastGUI For NGUI 3.0.X ——PSD到NGUI UI 工具

By D.S.Qiu

尊重他人的劳动,支持原创,转载请注明出处:http://dsqiu.iteye.com     

       由于NGUI 已经到3.0的版本,FastGUI 1.3.2只能支持NGUI2.6.3,由于NGUI 2.7版本以后很多脚本的命名和属性都改了,所以就不能用了,然后我就只有自己动手改造。

       主要改动和改进有:

               1)图集名的动态替换,以前为了公用其他图集的图片,每次在Unity中导入之后都有收到替换其他图集和图片,挺麻烦的,所以就干脆在PSD的图层组中加了命名规则来自动替换,比如 PSD 图层组 backgroun(common_bg1), 这样background的  这张图片就会自动替换成common的bg1图片,并且不导出background组内的图片。

              2)具有更多的可扩展行,简化了原来的逻辑,原来的逻辑很复杂,而且没必要,其实NGUI 的基本组件就是UIWidget的子类UILabel和UISprite,当然还有UITexture,其他都可以看做UIWidget的组合。也就是说定义的Button,Input等都是这些的集合,只要将定义的组件的成员变量进行手动赋值初始化一下就可以了。这样如果你要自己定义一个组件,只要在Unity中导入的时候,写好对应的初始化操作就可以了,然后在PSD的图层中指定一个命名规则就可以了。

              3)根据NGUI3.0的DrawCall规则,对UIWdiget进行了按图集排序,“机械”的减少DC。

              4)手动输出View类的需要的一些代码,比如找到某些控件等。

 

『PSD2NGUI

       FastGUI致命的缺点:导出的时间太久了,用过的人一定深有体会。然后发现还有另外一款插件——PSD2NGUI,看了下应该是完全脱离Adobe ExtenedScripte Toolkit的运行环境,由于手头的版本跟NGUI的版本没有对应,所以就没有具体试过,应该会很不错吧。』

                                                                                                                                                                                                                   增补于 2014,01,07 22:50

下面附上代码:

        使用Adobe ExtenedScript Toolkit  执行的脚本:  

#target photoshop
app.bringToFront();
var originalRulerUnits = preferences.rulerUnits;
preferences.rulerUnits = Units.PIXELS;
preferences.typeUnits = TypeUnits.PIXELS;
var cleanDocumentName = "";
var targetImageFolders;
var actualPath  = "";
var textInfos    = "";
var baseDoc     = app.activeDocument;
var docWidth    = baseDoc.width.value;
var docHeight   = baseDoc.height.value;
var stackorder  = 0;
var parentObj = "";
var layerData;

var layerReadCount = 0;
var totalLayerCount = baseDoc.layers.length;

var win, windowResource;
var createProgressWindow, progressWindow;
var exportType = "WIDGETS";
// create a string to hold the data
var str ="\n";
// add header line1
str += "\n";

function cLayer(doc, layer) 
{
    this.layerWidth = layer.bounds[2].value - layer.bounds[0].value;
    this.layerHeight = layer.bounds[3].value - layer.bounds[1].value;

    this.middleCenterX = this.layerWidth / 2 + layer.bounds[0].value;
    this.middleCenterY = this.layerHeight / 2 + layer.bounds[1].value;

    this.center = this.middleCenterX + ", " + this.middleCenterY;


    return this;
}
function getActiveLayerID()
{
   var d = getActiveLayerDescriptor();
   return d.getInteger(cTID('LyrI'));
}

function cTID(s) {return app.charIDToTypeID(s);}
function sTID(s) {return app.stringIDToTypeID(s);}
function getActiveLayerDescriptor() {
   var ref = new ActionReference();
   ref.putEnumerated(cTID('Lyr '), cTID('Ordn'), cTID('Trgt'));
   return executeActionGet(ref);
}


function newDocFromLayer( curDoc, newDocName, layer )
{
    var newDoc = app.documents.add( curDoc.width, curDoc.height, curDoc.resolution, newDocName , NewDocumentMode.RGB, DocumentFill.TRANSPARENT); 
    newDoc.activeLayer.isBackgroundLayer = false;
    app.activeDocument = curDoc; 
    layer.duplicate( newDoc ); 
    app.activeDocument = newDoc;
    return app.activeDocument;
}
function RemoveLayerSetsFromObject(pTargetObj, pRemoveInvisible)
{
    var arrToRemove = [];
    for(var i = 0; i < pTargetObj.layers[0].layers.length; i++)
    {
        if(pTargetObj.layers[0].layers[i].typename == "LayerSet")
        {
            arrToRemove.push(pTargetObj.layers[0].layers[i]);
        }
        else
        {
            if(pRemoveInvisible)
            {
                if(pTargetObj.layers[0].layers[i].visible == false)
                {
                    arrToRemove.push(pTargetObj.layers[0].layers[i]);
                }
            }
        }
    }
    for(var i = 0; i < arrToRemove.length; i++)
    {
        arrToRemove[i].remove();
    }
}
function CloseAndSaveAsPNG(pDoc)
{
    var pngSaveOptions = new PNGSaveOptions();
    pDoc.trim(TrimType.TRANSPARENT);
    if(atlasName == "")
        pDoc.saveAs(new File(targetImageFolders+pDoc.name+'.png'), pngSaveOptions, true, Extension.LOWERCASE);
    pDoc.close(SaveOptions.DONOTSAVECHANGES);
}

function ExportSpriteData(pTargetObj, pPath)
{
    var tParentName = GetParentName (pPath);
    var tObjName = pTargetObj.name;    
    var tLayerID = getActiveLayerID();
    if(exportType == "WIDGETS")    
    {
        atlasName = "";
        var tmpString=pTargetObj.name;
        var a = tmpString.indexOf(")");
        var b= tmpString.indexOf("(");
        if(a!==-1&&b!==-1)
        {
            tSourceName = tmpString.substring(b+1,a);
            var temp = tSourceName.split ("_");
            atlasName = temp[0];
            //str += " \n"+tSourceName+"  atalsName:"+temp[0]+"  "+atlasName;
            tSourceName = tSourceName.substring(atlasName.length+1);
            tObjName=tObjName.substring(0,b);
        }
        else if(tParentName=="")
        {
            tSourceName = tObjName;
        }
        else if(widgetType=="Sliced"||widgetType=="Tiled"||widgetType=="Filled")
        {
            tSourceName = tParentName+"_"+tObjName.substring(4);
        }    
         else 
         {
            tSourceName = tParentName+"_"+tObjName;
        } 
    }
    var newDoc = newDocFromLayer(baseDoc, tSourceName, pTargetObj);
    layerData = cLayer(newDoc, newDoc.layers[0]);
    
    if(exportType == "WIDGETS")
        RemoveLayerSetsFromObject(newDoc, true)
    
    layerReadCount++;
    str += " \n";
    if(widgetType=="Sliced"||widgetType=="Tiled"||widgetType=="Filled")
        str += "        "+tObjName.substring(4)+"\n";
    else 
        str += "        "+tObjName+"\n";
    str += "        "+widgetType+"\n";
    str += "        "+ pPath +"\n";
    str += "        "+ layerData.middleCenterX +"\n";
    str += "        "+ layerData.middleCenterY +"\n";
    str += "        "+ layerData.layerWidth +"\n";
    str += "        "+ layerData.layerHeight +"\n";
    str += "        "+atlasName+"\n";
    str += "        "+ tSourceName+"\n";
    str += "        "+ tLayerID +"\n"
    str += " \n";
   // if(atlasName=="")
        CloseAndSaveAsPNG(newDoc);
}

function ExportAnchor(pDoc, pTargetObj, pPath)
{
    var tLayerID = getActiveLayerID();
    layerData = cLayer(pDoc, pTargetObj);
    
    layerReadCount
    
     str += " \n";
    str += "        "+pTargetObj.name+"\n";
    str += "        "+ pPath +"\n";
    str += "        "+ layerData.middleCenterX +"\n";
    str += "        "+ layerData.middleCenterY +"\n";
    str += "        "+ tLayerID +"\n";
    str += " \n";
}


function ExportTextLabel(pTargetObj, pPath)
{
    var tLayerID = getActiveLayerID();
    $.writeln ("Layer ID: "+ tLayerID);    
    
    var newDoc  = newDocFromLayer(baseDoc, pTargetObj.name, pTargetObj);
    layerData   = cLayer(newDoc, newDoc.layers[0]);
    //var tPrefix = newDoc.layers[0].name.substring(0,4) ;
    var tLayer  = newDoc.layers[0].layers[0];

    if( tLayer.kind == LayerKind.TEXT )
    {
        layerReadCount+=1;
        var tText   = tLayer.textItem;
        var tSize   = new Number( tText.size );
        tSize       = Math.round (tSize);
        var tName   = newDoc.layers[0].name;
        tName       = tName.substring(4);
        //str += " \n"+ tText.font;
        //var tType = tPrefix == "txt_" ? "TEXT_LABEL" : "INPUT_TEXT";

        str += " \n";
        str += "        "+pTargetObj.name.substring(4)+"\n";
        str += "        "+ pPath +"\n";
        str += "        "+ layerData.middleCenterX +"\n";
        str += "        "+ layerData.middleCenterY +"\n";
        str += "        "+ layerData.layerWidth +"\n";
        str += "        "+ layerData.layerHeight +"\n";
        str += "        "+ tText.font +"\n";
        str += "        "+ tSize +"\n";
        str += "        "+ tText.color.rgb.red +";"+ tText.color.rgb.green +";"+ tText.color.rgb.blue +"\n";
        str += "        "+ tText.contents+"\n";
        str += "        "+ tLayerID +"\n";
        str += "\n";
    }
    newDoc.close(SaveOptions.DONOTSAVECHANGES);

}




function GetParentName(pPath)
{
    if(pPath.substring(0, pPath.length-1) == "/")
        pPath = pPath.substring(0, pPath.length-1);
        
    var strPathLeng = pPath.length;
    var lastIndex = pPath.lastIndexOf ("/");
    if(lastIndex > -1)
    {
        pPath = pPath.substring ((lastIndex+1), strPathLeng);
    }

    return pPath;
}

function ExportSprite(pDoc, pTargetObj)
{
    var newDoc = newDocFromLayer(pDoc, pTargetObj.name, pTargetObj);
    layerData = cLayer(newDoc, newDoc.layers[0]);
    
    CloseAndSaveAsPNG(newDoc);
}

function ReadGroup(pTargetGroup, pPath)
{
   
    if(exportType == "WIDGETS")
    {
        totalLayerCount += pTargetGroup.layerSets.length;
        for(var i = (pTargetGroup.layerSets.length-1); i >= 0 ; i--)
        {
             var tmpObj = pTargetGroup.layerSets[i];
            baseDoc.activeLayer = tmpObj;
            
            progressWindow.bar.value = (layerReadCount/totalLayerCount)*100;
            //progressWindow.update();  
           
            if(tmpObj.artLayers.length > 0)   //基本组件 txt  sprite
            {
                if(tmpObj.name.substring(0, 4) == "txt_" )
                {
                    ExportTextLabel(tmpObj, pPath);
                }
                else if(tmpObj.name.substring(0, 4) == "slc_" )
                {
                    widgetType="Sliced";
                    ExportSpriteData(tmpObj, pPath);
                }
                else if(tmpObj.name.substring(0, 4) == "til_" )
                {
                    widgetType="Tiled";
                    ExportSpriteData(tmpObj, pPath);
                }
                else if(tmpObj.name.substring(0, 4) == "fil_" )
                {
                    widgetType="Filled";
                    ExportSpriteData(tmpObj, pPath);
                }
                else
                {
                    widgetType="Simple";
                    ExportSpriteData(tmpObj, pPath);
                }
            }
            else
            {
                if(tmpObj.name.substring(0, 4) == "btn_")
                {
                    widgetType="BUTTON";
                }
                else if(tmpObj.name.substring(0, 4) == "ckb_" )
                {
                    widgetType="CHECKBOX";
                }
                else if(tmpObj.name.substring(0, 4) == "sld_" )
                {
                    widgetType="SLIDER";
                }
                else if(tmpObj.name.substring(0, 4) == "psb_" )
                {
                    widgetType="PROGRESSBAR";
                }
                else if(tmpObj.name.substring(0, 4) == "inp_" )
                {
                     widgetType="INPUT";
                }
                else
                {
                    widgetType="ANCHOR";
                    
                }
                ExportAnchor(pTargetGroup, tmpObj, pPath);
            }
            if(tmpObj.layerSets.length > 0)
            {
                var target="";
                if(pPath == "")
                {
                    target = tmpObj.name;
                 }
                else
                {
                    target= pPath+"/"+tmpObj.name;
                }
                ReadGroup(tmpObj, target);
            }
        }
        
    }
    else if(exportType == "SPRITES")
    {
        totalLayerCount += pTargetGroup.layerSets.length;
         for(var i = (pTargetGroup.layerSets.length-1); i >= 0 ; i--)
        {
            
            progressWindow.bar.value = (layerReadCount/totalLayerCount)*100;
            //progressWindow.update(); 
            
            var tmpObj = pTargetGroup.layerSets[i];
            baseDoc.activeLayer = tmpObj;
            
            var target = "";
            if(pPath == "")
            {
                target = tmpObj.name;
             }
            else
            {
                target= pPath+"/"+tmpObj.name;
            }
            //ExportAnchor(pTargetGroup, pTargetGroup.layerSets[i], pPath);
            ReadGroup(tmpObj, target);
        }
        totalLayerCount += pTargetGroup.artLayers.length;
        for(var i = (pTargetGroup.artLayers.length-1); i >= 0; i--)   
        {
            var tmpObj = pTargetGroup.artLayers[i];
            baseDoc.activeLayer = tmpObj;
            atlasName = "";
            var tempPath = pPath.substring (0, pPath.lastIndexOf("/")) ;   //调整命名为: 父目录(组)名+ 当前目录(组)名
            var tGrandParentName = GetParentName (tempPath);
            var tParentName = GetParentName (pPath);
            var tObjName = tmpObj.name;    
            
            if(tGrandParentName=="")
            {
                    tSourceName = tParentName;
            }
            else 
            {
                tSourceName = tGrandParentName+"_"+tParentName;
            } 
            var newDoc = newDocFromLayer(baseDoc, tSourceName, tmpObj);
            CloseAndSaveAsPNG(newDoc);
            layerReadCount++;
            //ExportSprite(baseDoc, pPath);
        }

    }
}

windowResource = "dialog {  \
    orientation: 'column', \
    alignChildren: ['fill', 'top'],  \
    preferredSize:[140, 60], \
    text: 'FastGUI Photoshop Exporter',  \
    margins:15, \
    \
    bottomGroup: Group{ \
        cancelButton: Button { text: 'Cancel', properties:{name:'cancel'}, size: [120,24], alignment:['right', 'center'] }, \
        exportAllAsSprite: Button { text: 'Export Sprites', properties:{name:'exportsprites'}, size: [120,24], alignment:['right', 'center'] }, \
        exportWidgets: Button { text: 'Export Widgets', properties:{name:'exportwidgets'}, size: [120,24], alignment:['right', 'center'] }, \
    }\
}"
win = new Window(windowResource);

win.bottomGroup.cancelButton.onClick = function() 
{
  return win.close();
};
win.bottomGroup.exportAllAsSprite.onClick = function() 
{
    exportType = "SPRITES";
    StartFolder();
};
win.bottomGroup.exportWidgets.onClick = function() {
    exportType = "WIDGETS";
    StartFolder();
};

function StartFolder()
{
    win.close();
    var theFolder    = Folder.selectDialog ("Select the target folder:");

    if (theFolder) 
    {
        progressWindow = createProgressWindow("Please wait...", undefined, 0, 100); 
        progressWindow.show();
        cleanDocumentName = app.activeDocument.name.match(/([^\.]+)/)[1];
        
        var TargetFolder = new Folder(theFolder+'/'+cleanDocumentName+'/');
        TargetFolder.create();     
        
        var folder = new Folder(TargetFolder+"/Images");
        folder.create();
        targetImageFolders = TargetFolder+"/Images/" ;
        
        ReadGroup(baseDoc, "");
        // Use this to export XML file to same directory where PSD file is located
        var mySourceFilePath = TargetFolder + "/";
        // create a reference to a file for output
        var csvFile = new File(mySourceFilePath.toString().match(/([^\.]+)/)[1] + "FastGUIData.xml");
        // open the file, write the data, then close the file
        csvFile.encoding = "utf-8";
        csvFile.open('w');
        csvFile.writeln(str + "");
        csvFile.close();
        preferences.rulerUnits = originalRulerUnits;
        // Confirm that operation has completed
        alert("Operation Complete!" + "\n" + "Layer coordinates were successfully exported to:" + "\n" + "\n" + mySourceFilePath.toString().match(/([^\.]+)/)[1] + app.activeDocument.name.match(/([^\.]+)/)[1] + ".xml");
    }
}
createProgressWindow = function(title, message, min, max) {
  var win;
  win = new Window('palette', title);
  win.bar = win.add('progressbar', undefined, min, max);
  win.bar.preferredSize = [300, 20];
  win.stProgress = win.add("statictext");
  win.stProgress.preferredSize.width = 200;
  return win;
};


win.show();

        在Unity中使用的脚本(比原来上了很多,XML解析的三个脚本就不贴了)

 

FastGUIOutPut.cs:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;


public class FastGUIOutput : MonoBehaviour 
{
	public Dictionary references = new Dictionary();
}

 FastGUI.cs:

using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System.Text;

[ExecuteInEditMode]
public class FastGUI : EditorWindow
{
    //ͼ����depth����
	private const int COMMON_DEPTH = 0;
	private const int FONT_DEPTH = 2000;
	private const int MIDDLE_DEPTH = 1000;
    public static Dictionary pathTransDic = new Dictionary();
	[MenuItem("Monster Juice/Fast GUI")]
    static void ShowWindow () 
	{
		EditorWindow.GetWindow();
    }
	private string applicationDataPath 	= Application.dataPath.Substring(0, Application.dataPath.IndexOf("Assets"));
	public static string assetFolderToBeParseed;
	private string applicationFolderToBeParseed;
	private string lastFolderChecked;
	
	static public int targetWidht;
	static public int targetHeight;
	
	public static Object objectToBeLoaded;
	public static string sourceFolder ="";

	private bool haveXML;
	private bool haveRoot;
	private bool haveTargetPanel;
	private bool isOnSize;
	
	private Color defaultColor;
	
	private UIPanel targetRootPanel;
	private UIPanel lastCheckedtargetRootPanel;
	public static UIRoot actualRoot;
	private float itemActual = 1;
	private float totalItens = 1;
	
	private UIAtlas targetAtlas;

    private Dictionary transformDic = new Dictionary();
	
	
	private XMLNodeList xmlObjects;
	
	
	private string debugerText;	
	
	private string currentObjectName = "";
	
	public static FastGUIOutput actualFastGUIOutput = null;
	StreamReader reader;
	
	void OnSelectionChange () 
	{ 
		Repaint();
	}
	void OnEnable()
	{
		defaultColor = GUI.backgroundColor;
	}
    void OnGUI () 
	{
		GUILayout.Label("FastGUI", EditorStyles.boldLabel);
		FastGUIEditorTools.DrawSeparator();
		GUILayout.BeginHorizontal();
		GUILayout.Label("FastGUI Folder:");
		objectToBeLoaded 				= EditorGUILayout.ObjectField(objectToBeLoaded, typeof(Object), false);
		if(objectToBeLoaded != null)
		{
			if(AssetDatabase.GetAssetPath(objectToBeLoaded).IndexOf(".xml") > -1)
			{
				objectToBeLoaded = null;
			}
		}
		assetFolderToBeParseed 			= AssetDatabase.GetAssetPath(objectToBeLoaded);
		applicationFolderToBeParseed 	= applicationDataPath+assetFolderToBeParseed;
		GUILayout.EndHorizontal();
		
		GUILayout.BeginVertical();
		if(lastFolderChecked != applicationFolderToBeParseed)
		{
			lastFolderChecked 	= applicationFolderToBeParseed;
			haveXML 			= HasXML();
		}
		GUILayout.EndVertical();
		
		GUILayout.BeginHorizontal();
		GUILayout.Label("Parent Panel:");
		targetRootPanel 				= EditorGUILayout.ObjectField(targetRootPanel, typeof(UIPanel), true) as UIPanel;
		GUILayout.EndHorizontal();
		if(targetRootPanel!=null)
		{
			haveTargetPanel = true;
		}
		else
		{
			haveTargetPanel = false;
		}
		if(haveTargetPanel)
		{
			if(lastCheckedtargetRootPanel != targetRootPanel)
			{
				lastCheckedtargetRootPanel = targetRootPanel;
				actualRoot = GetUIRoot(targetRootPanel.gameObject);
				
				
				if(actualRoot == null)
				{
					haveRoot = false;
				}
				else
				{
					haveRoot = true;
				}
			}
		}
		GUILayout.BeginHorizontal();
		GUILayout.Label("Target Atlas:");
		targetAtlas 				= EditorGUILayout.ObjectField(targetAtlas, typeof(UIAtlas), false) as UIAtlas;
		GUILayout.EndHorizontal();
		
		
		GUILayout.Space(10);
		if(actualRoot != null && objectToBeLoaded!=null)
		{
			if(actualRoot.manualHeight != targetHeight || actualRoot.minimumHeight < targetHeight)
			{
				isOnSize = false;
			}
			else
			{
				isOnSize = true;
			}
			if(!isOnSize)
			{
				GUI.backgroundColor = new Color(171f/255, 26f/255, 37f/255,1);
				if(GUILayout.Button("Update the UIRoot"))
				{
					UpdateUIRootSize();
				}
				GUI.backgroundColor = defaultColor;
			}
		}
		if(haveXML && haveRoot && isOnSize && haveTargetPanel)
		{
			GUI.backgroundColor = new Color(17f/255, 146f/255, 156f/255,1);
			if(GUILayout.Button("FastGUI it!"))
			{
				ParseeTargetFolder();
			}
			GUI.backgroundColor = defaultColor;
			
		}
		else
		{
			UpdateDebugger();
		}
	}
	private void ResetFields()
	{
		objectToBeLoaded 	= null;
		targetRootPanel		= null;
		targetAtlas			= null;
		
	}
	private void ResetProperties()
	{
		reader						= null;
		FastGUI.actualFastGUIOutput = null;
		NGUISettings.atlas 			= null;
		NGUISettings.fontData 		= null;
		NGUISettings.fontTexture 	= null;
		targetAtlas					= null;
		FastGUI.sourceFolder		= "";
	}
	private void ParseeTargetFolder()
	{
		actualRoot = GetUIRoot(targetRootPanel.gameObject);
        pathTransDic = new Dictionary();
        pathTransDic.Add("", targetRootPanel.transform);
		if(targetAtlas != null)
			NGUISettings.atlas = targetAtlas;
		
		if(xmlObjects == null)
			ReadXML();
		
		if(actualFastGUIOutput == null)
			CreateOutputPrefab();
		
		itemActual = 0;
		string		tType		= null;
		Transform	tLastAnchor	= null;
		string		tLastAnchorPath = null;	
			
		foreach(XMLNode tNode in xmlObjects)
		{
			EditorUtility.DisplayProgressBar(
				"FastGUI Progress",
				("Object: "+currentObjectName+"("+itemActual+"/"+totalItens+")"),
				itemActual/totalItens);
			
			tType		= tNode["@type"].ToString();
			itemActual 	+= 1.0f;
			
			if(tNode["@type"].ToString() == "ANCHOR")
			{
				tLastAnchor 	= FastGUIPostProcessing.CreateAnchor( tNode, targetRootPanel, tLastAnchor, tLastAnchorPath);;
			}
			else if(tNode["@type"].ToString() == "BUTTON")
			{
                tLastAnchor = FastGUIPostProcessing.CreateWidgetContainer

小结:

       最近工作有点忙,就没怎么更新博客,虽然满篇都是代码,D.S.Qiu觉得对这篇博客感兴趣的一定是了解用过FastGUI 的,代码上精简了很多,但是PSD导出的过程还是很慢,对JavaScript(没有类型的语言)还是挺无语的和没空去琢磨Adobe Script api,所以只能在原来的基础上造车,如果你有更好的从PSD到上次Unity的UI的方案或插件(其实FastGUI的最主要的作用就是定位),希望能够得到您的分享。

 

 

       如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件([email protected])交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。

        转载请在文首注明出处:http://dsqiu.iteye.com/blog/1995248

更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)

 

你可能感兴趣的:(FastGUI For NGUI 3.0.X ——从PSD到NGUI UI 工具)