1. 程式人生 > >unity編輯器拓展之自動生成指令碼模板

unity編輯器拓展之自動生成指令碼模板

    專案開發過程中,UI面板有許多,關於UI面板上面按鈕,文字是應該宣告public直接拖拽賦值還是應該定義成private一層層去find,其中利弊各有說法,以前有個老大說是find會影響執行速度,但是現在的老大又不讓直接拖拽賦值,說實話這些東西都無所謂,用哪種方式主要看老大的習慣,咱們就不去分析其中利弊了,但是如果定義成private去find,那麼多控制元件,find起來還是比較煩,所以只好寫一個工具類去直接生成了。

    其實這個工具是現在的老大有一天突然發出來讓用的,但是是NGUI用的,博主最近Demo用的UGUI,就拿來改寫了一下,所以嚴格上說不是博主自創,只是借鑑然後略微改進了一些而已。原理上就是遍歷子物體,然後將需要定義的物件如文字,按鈕,圖片定義一下,然後加入find程式碼,按鈕的話再加個點選事件,把這些內容寫入一個文字就行。

    先說一下具體的原理吧。就是根據預設某個子物件的名字,判斷該物件是什麼型別(text or img or btn),也就是說在建立預設的時候名字不能隨便起,例如所有要定義在指令碼中的文字都要以Txt結尾(或者名字中包含Txt),之後在遍歷到這個物件時就可以確定這個物件的型別,然後就可以根據物件型別做不同處理,比如find時候的getcomponment,以及按鈕的新增事件。然後還有..好像也沒什麼了我覺得。總之名字一定要起好,不能重名這樣子,其他的也沒什麼了。

    因為這個工具有兩個功能,一個是create,一個是fix,而且是混合到一塊的,所以就有些地方的引數就有點多,這個就暫時不用管,後面的話會有原始碼。

static Dictionary<string, string> childrenNames;//物件子物體列表
    static Dictionary<string, string> Rules = new Dictionary<string, string>()
    {
        {"Txt","Text" }, {"Btn","Button" }, { "Img","Image"}
    };//命名規則
    static Transform SelTran;//選擇的物件
    static List<string> names;//重名的子物體
    static List<string> btnNames;//所有按鈕
    TextAsset txt;//Fix選擇的指令碼
    string input;//Fix規則


    static Dictionary<string, string> FixChilderNames;//fix新加的子物體
    static List<string> FixBtnNames;//fix新加的按鈕

    字典的key是命名格式,value是對應的物件型別。這個是UGUI的,NGUI的就是{“Txt”,“Label”}。字典就按照自己的命名習慣來好了。注意value別寫錯了。

    const string FixDef = "//FixStartDefiened";
    const string FixFind = "//FixStartFind";
    const string FixAddEvent = "//FixStartAddEvent";
    const string FixEvent = "//FixStartEvent";

    這幾個是後面用到的,相當於於一個記號吧,後面會解釋,暫時不用理會。

    private void OnGUI()
    {
        txt = EditorGUILayout.ObjectField("drag cs", txt, typeof(TextAsset), true) as TextAsset;
        EditorGUILayout.Space();

        SelTran = EditorGUILayout.ObjectField("Drag transform", SelTran, typeof(Transform), true) as Transform;
        EditorGUILayout.Space();

        input = EditorGUILayout.TextField("輸入結尾符,fix指令碼時必填:",input);
        EditorGUILayout.Space();

        if (GUILayout.Button("Create", GUILayout.Width(200)))
        {
            Create();
        }

        EditorGUILayout.Space();

        if (GUILayout.Button("Fix", GUILayout.Width(200)))
        {
            FixScripts();
        }

    }

    拓展視窗,ongui繪製介面,各個引數上面都有註釋。先說下create方法,建立指令碼。

    void Create()
    {
        childrenNames = new Dictionary<string, string>();
        names = new List<string>();
        btnNames = new List<string>();
        GetChildren(SelTran);
        CreateScript(Application.dataPath + "/" + SelTran.name + ".cs", DealScript().ToString());

    }

    首先例項化字典,列表。然後GetChilder方法是獲取所有的指令碼中要定義的所有物件名稱,CreateScript只是IO流的寫入。

void GetChildren(Transform tran, string name = "",string rule = "")
    {
        if (string.IsNullOrEmpty(name)) name = tran.name;
        int childNum = tran.childCount;
        if(string.IsNullOrEmpty(rule))
        {
            string value = AddName(tran.name);
            if (!string.IsNullOrEmpty(value)) childrenNames.Add(name, value);//符合規則,加入列表
        }
        else//fix
        {
            string valuefix = AddName(tran.name, rule);
            if (!string.IsNullOrEmpty(valuefix)) FixChilderNames.Add(name, valuefix);
        }

        if (childNum == 0)
        {
            return;
        }
        else
        {
            string temp = name;
            for (int i = 0; i < childNum; i++)
            {
                temp = name;
                temp = temp + "/" + tran.GetChild(i).name;
                GetChildren(tran.GetChild(i), temp,rule);
            }
        }
    }
     string AddName(string name,string rule="")
    {
        if (names.Contains(name))
        {
            Debug.LogError(name + "重名!!!!");
        }
        foreach (KeyValuePair<string, string> item in Rules)
        {
            if (name.Contains(item.Key))
            {
                if (string.IsNullOrEmpty(rule))//非fix
                {
                    return item.Value;
                }
                else
                {
                    if (name.EndsWith(rule)) return item.Value;
                }
            }
        }
        return string.Empty;
    }

    GetChilder方法第一個引數是要遍歷的物件,第二個引數是路徑,也就是find時候的路徑,是需要一個物件一個物件連起來的,第三個引數暫時用不到。然後邏輯就這個樣子,用遞迴的方法去遍歷,然後符合規則的寫入到字典中。Addname方法是判斷是不是符合規則,符合規則則返回當前名字對應的型別,不符合的返回空,不寫入字典。遞迴這個東西理解了就是理解了,理解不了就鑽牛角尖出不來了,不過有個技巧,就是理解遞迴的時候不要憑空的去想,看著一個預設,然後假設將這個預設套入到這個方法裡面執行結果是什麼,是不是符合要求。

    獲取到所有的要定義的物件名字及型別,之後就是寫入檔案中了,這個程式碼有點多,就不貼上了,比較簡單,列舉一部分,定義stringbuilder,然後用stringbuild的append,appenline,appendformat方法去規定指令碼格局,例如下面程式碼是引用及定義部分的。

StringBuilder sb = new StringBuilder();
        sb.AppendLine("using UnityEngine;");
        sb.AppendLine("using UnityEngine.UI;");
        sb.AppendLine("using System.Text;");
        sb.AppendLine("using XLua;");
        sb.AppendLine();
        sb.AppendLine();

        sb.AppendFormat("public class {0} : MonoBehaviour ", SelTran.name);
        sb.AppendLine();
        sb.AppendLine("{");

        var dic = childrenNames.GetEnumerator();
        while (dic.MoveNext())
        {
            string[] names = dic.Current.Key.Split('/');
            string name = names[names.Length - 1];
            string value = dic.Current.Value;
            sb.AppendFormat("\tprivate {0} {1};", value, name);
            sb.AppendLine();

        }
        sb.AppendLine(FixDef);

    是不是就很簡單,就是平時寫程式碼那樣..注意appendformat的第一個引數string,不要以“{}”結尾,這樣的話應該會有識別問題,不是很確定,有興趣的可以試一下。定義部分就這樣子,然後下面find,以及按鈕的事件新增,就類似,主要還是看自己的專案中的寫法是怎麼樣的,注意寫find的時候不要寫給按鈕的點選事件的程式碼,這樣的比較亂,比較好的做法就是在寫find的時候順便將按鈕篩選出來,然後find之後再寫按鈕的程式碼,這樣子就好很多。程式碼全部寫完之後就直接IO寫入就行了,目前博主檔案直接寫在assets了,後面讀者自己優化路徑吧。


    第一個是博主的預設,第二個圖是拓展視窗,將預設拖到拓展視窗的Trannstorm欄,點選create之後,看到“ok”log之後就可以看到生成的指令碼,如下


    是不是很方便,路徑也沒問題。到此,create就算完成了,具體的內容還是根據自己的專案來定,比如引用名稱空間,按鈕事件定義。這些也是根據我們老大的程式碼改寫的UGUI版本,但是有個缺點就是假如說預設中新加了一些物件需要定義的,但是這個指令碼程式碼已經寫了很多,所以不可能刪了重新建立,然後就還是需要手寫find,還是挺麻煩的,所以樓主尋思的實現一下在不改變原有程式碼的情況下,新增物件定義,然後算是找到了還算可以的解決方法。

    博主的思路是在建立指令碼的時候,留下標誌,作為拓展用,比如在find程式碼塊結尾加入一個標誌,作為拓展的入口。博主一共加了四個標誌,也就是最上面定義的四個常量,分別是物件定義結尾,物件find結尾,按鈕新增事件結尾,以及按鈕事件結尾。然後博主只要找到新加入的物件,就可以直接在這些結尾的地方寫入相應的程式碼,這樣就不會影響原有的程式碼了。接下來是如何區分原有的和新加的物件,博主的方法是在名字結尾加入標誌符號,以此來作為區分,也就是上面定義的rule,雖然很low,但是還可以用,拋磚引玉吧。然後先將新加的物件的定義,find等程式碼寫好,直接替換掉原來定義的入口就行,不過記得在下面記得還要加入入口,以便下次修改。程式碼如下 

    void FixScripts()
    {
        FixChilderNames = new Dictionary<string, string>();
        FixBtnNames = new List<string>();
        names = new List<string>();
        string path = AssetDatabase.GetAssetPath(txt);//獲取選定指令碼的路徑
        Debug.Log(path);
        if (string.IsNullOrEmpty(input))
        {
            Debug.LogError("Error!!!");
            return;
        }
        GetChildren(SelTran,string.Empty,input);

        StreamReader sr = new StreamReader(path, Encoding.UTF8);
        string msg = sr.ReadToEnd();
        FixDealScript(ref msg);
        sr.Close();
        //FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Write);
        //StreamWriter sw = new StreamWriter(fs);
        StreamWriter sw = new StreamWriter(path,false,Encoding.UTF8);
        sw.Write(msg);
        sw.Close();
        AssetDatabase.Refresh();
        Debug.Log("fix ok------");
    }

    首先要獲取文字路徑,然後用於寫入,但是記得rule不能為空,不然就報錯了,這些非法檢測博主沒有寫,然後還是利用上面的 GetChildren方法獲取到所有的新增的物件名。之後就是讀取指令碼內容,處理指令碼字串了。處理過程也比較簡單,就是先寫新增物件的程式碼,然後替換入口就好,這個後面就後面自己看吧。

    博主在修改了剛才了預設,新增的以“1”結尾,同時修改程式碼,然後開啟視窗,將預設拖入transform欄,將指令碼拖入TextAsset欄,規則欄輸入1,點選fix。看到“fix ok”log,效果如下。


    新加的程式碼privite string name 沒有影響到,新增的物件有全部加入,就佈局問題,可以不用糾結,直接快捷鍵自動對齊就行,如果真的糾結的話就在寫指令碼的時候規劃下吧,博主就不調了,直接快捷鍵解決。如果下次又有新增的,就換一個標識,例如“2”,再新增就繼續換,再新增的話你就可以找策劃談話了..雖然這樣標識很low.但是.博主沒想到其他什麼辦法,而且,只是一個工具類,主要是節約時間,方便寫程式碼,覺得不用糾結太多。

    好了,本篇到此結束,需要優化的地方還有很多,比如建立指令碼的路徑,比如非法操作的判斷等等,有不懂的或者有bug可以私信,或者直接改..博主覺得也沒什麼難度...下面程式碼原檔案連結。