【項目維護拓展】Unity3D利用子類組件替換項目中所有的父類組件

Unity3D利用子類組件替換項目中所有的父類組件

本文轉自:http://gad.qq.com/article/detail/47897,請點擊鏈接查看原文,尊重樓主版權。

參考鏈接:Unity文件、文件引用、Meta詳解
C#正則表達式可參考:https://blog.csdn.net/qq_33337811/article/details/54729050

背景:在進行多語言功能的開發中,我遇到了這樣的問題:項目開發時並沒有考慮將來需要開發多語言版本,因此很多中文直接寫在了代碼或者prefab中。項目開發完成,但想在原來的基礎上開發海外版本,就要實現多語言的拓展,因而必須將所有的中文字符替換成多語言字典的Key值,進而通過查詢多語言字典得到當地文字。需要用自定義的Text組件的子類來替換項目中所有的Text組件,從而實現擴展功能。

(本文主要闡述如何解決prefab中控件的替換問題,僅以UnityEngine.UI.Text組件爲例闡述,但類似方法可以運用在任何類型的組件上)

最爲常見的帶文本的基礎控件爲UnityEngine.UI.Text控件,其餘的如Button、Dropdown、InputField等皆引用了Text組件作爲子控件以顯示文字。因此,我們只需要對原生的Text組件進行拓展即可。

有以下幾個可選思路:

  • 在所有帶有Text組件的GameObject上都附加一個新組件,通過新組件中GetComponent().text="Localization Dictionary Key"的方式來進行更改。優點:簡單暴力,開發容易;缺點:運行時開銷很大,無法對衆多Text組件進行統一管理。

  • 繼承,創建一個LocalizationText類繼承自Text類,並在其中加入一些支持多語言的功能。優點:可以對所有的Text組件統一管理,不影響原有組件和代碼之間的互相引用,一次性替換、運行時無額外開銷;缺點:開發較麻煩。

第一種方式容易實現,缺點也很明顯,這裏就不討論了。

按照第二種方式,先創建一個Text控件的子類:

按第二種方式寫好子類之後,在替換父類組件時將會遇到以下問題:

  • 手動替換不現實,項目中有成百上千個Text組件需要替換

  • 採用Destroy組件+AddComponent的方式會導致prefab之間的引用斷開

針對第一個問題,我們可以寫一個Editor腳本來遍歷所有的Text組件。

針對第二個問題,我進行了一番研究。

關於Prefab的內容,這裏不過多描述,可參閱:原文樓主的研究過程和關於Prefab的內容

注:所有不同的GameObject、Script、Component、Prefab等都有自己唯一的fileID,但guid的值由其所在的文件確定,可在meta文件中查到。

例1:Text組件引用的m_Script、Image組件引用的m_Script是在同一個文件下,因此其guid是相同的,但其fileID不同;

例2:兩個的Text組件自身的fileID不同,但其引用的m_Script的fileID和guid都相同。

結論:

如果將Text組件替換爲LocalizationText組件,可以有兩個方案:

  • 通過Editor代碼Destory(Text)然後AddComponent(),再在prefab中把丟失引用的fileID和guid都替換爲新組件的fileID和guid。一開始我是用這個方法來實現的,但後面在InputField控件中遇到了特殊的問題(同一個prefab中一個組件引用另一個組件,如果另一個組件被destroy,原組件的引用fileID將置0,無法追蹤替換位置),因此選用了第二種方案。

  • 由於我們的新組件LocalizationText.cs沒有像系統原生組件那樣封裝在dll文件中,因此我們可以輕鬆獲得該組件的guid值和fileID值。因此,將原Text組件中的m_Script中的fileID和guid改爲LocalizationText.cs的guid和fileID,即可實現全部替換。(子類有新變量的可以手動寫個值在這裏)

至於說如何獲得新、舊組件的fileID和guid,可以通過創建兩個prefab分別獲取(對組件來說,fileID和guid在同一個項目中是不會變化的)。

Tips:在重寫prefab文件後,注意要執行一下:

   // EditorApplication.SaveAssets(); //2017已經棄用 下面的

    AssetDatabase.SaveAssets();
    AssetDatabase.Refresh();

否則無法生效。

代碼:(有個人修改)

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text.RegularExpressions;
using UnityEngine.UI;
using System.Collections.Generic;
using System.Reflection;

public class ReplaceTextComponent
{
    private static Dictionary<string, string> replaceStringPairs = new Dictionary<string, string>();//替換string對_fileID和GUID同時替換
    private static List<long> oldFileIDs = new List<long>();
    static string oldScriptGUID;
    static long oldScriptFileID;
    static string newScriptGUID;
    static long newScriptFileID;
    private static List<Text> texts = new List<Text>();

    private static Regex rg_Number = new Regex("-?[0-9]+");
    private static Regex rg_FileID = new Regex(@"(?<=m_Script: {fileID:\s)-?[0-9]+");
    private static Regex rg_GUID = new Regex(@"(?<=guid:\s)[a-z0-9]{32,}(?=,)");

    #region 獲取所有Text內容
    [MenuItem("HIEngine/Localization/獲取所有Text內容")]
    static void GetAllTextContent()
    {
        List<string> executePaths = getExecutePaths();
        OnGetAllTextContent(executePaths);
    }
    static void OnGetAllTextContent(List<string> executePaths)
    {
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        List<string> allPrefabPaths = getAllPrefabsFromPaths(executePaths);
        int count = 0;
        foreach (string file in allPrefabPaths)
        {
            string path = getAssetPath(file);
            var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
            Text[] comps_Old = prefab.GetComponentsInChildren<Text>();
            foreach (Text com_Old in comps_Old)
            {
                count++;
                Stack<string> parentNames = new Stack<string>();
                string debugString = "控件_" + count + ":";
                Transform point = com_Old.transform;
                while (point.parent != null)
                {
                    parentNames.Push(point.parent.name);
                    point = point.parent;
                }
                while (parentNames.Count != 0)
                {
                    debugString += parentNames.Pop() + " > ";
                }
                debugString += "[" + com_Old.name + "] 內容: {" + com_Old.text + "}";
                Debug.Log(debugString);
            }
        }
    }
    #endregion

    #region 替換Text組件爲LocalText
    [MenuItem("HIEngine/Localization/替換Text組件爲LocalText")]
    public static void Replace()
    {
        List<string> executePaths = getExecutePaths();
        OnExecute(executePaths);
    }
    private static List<string> getExecutePaths()
    {
        List<string> executePaths = new List<string>();
        Object[] arr = Selection.GetFiltered(typeof(Object), SelectionMode.TopLevel);
        if (arr == null || arr.Length == 0)
        {
            executePaths.Add("Assets/BundleResources/UI");
            return executePaths;
        }
        foreach (Object dir in arr)
        {
            executePaths.Add(AssetDatabase.GetAssetPath(dir));
        }
        return executePaths;
    }

    static void OnExecute(List<string> executePaths)
    {
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        texts.Clear();
        oldFileIDs.Clear();
        replaceStringPairs.Clear();

        //獲取新腳本GUID
        var newScriptFile = Directory.GetFiles(Application.dataPath+ "/HIEngine/Script/Language/Localization", "LocalizationText.cs", SearchOption.TopDirectoryOnly);
        newScriptGUID = AssetDatabase.AssetPathToGUID(getAssetPath(newScriptFile[0]));
        Debug.Log("newScriptGUID:" + newScriptGUID);

        //獲取新腳本FileID
        string[] newComponentPrefabFile = Directory.GetFiles(Application.dataPath+ "/HIEngine/Script/Language/Localization/Editor", "LocalizationTextTempPrefab.prefab", SearchOption.TopDirectoryOnly);
        GameObject localizationTextPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(getAssetPath(newComponentPrefabFile[0]));
        long newComponentFileID = getFileID(localizationTextPrefab.GetComponent<LocalizationText>());
        newScriptFileID = getScriptFileIDbyFileID(newComponentFileID, getAssetPath(newComponentPrefabFile[0]), newScriptGUID);
        Debug.Log("newScriptFileID:" + newScriptFileID);

        //獲取老腳本FileID,GUID
        string[] oldComponentPrefabFile = Directory.GetFiles(Application.dataPath + "/HIEngine/Script/Language/Localization/Editor", "TextTempPrefab.prefab", SearchOption.TopDirectoryOnly);
        GameObject textPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(getAssetPath(oldComponentPrefabFile[0]));
        long oldComponentFileID = getFileID(textPrefab.GetComponent<Text>());
        oldScriptGUID = getScriptGUIDbyFilePath(getAssetPath(oldComponentPrefabFile[0]));
        oldScriptFileID = getScriptFileIDbyFileID(oldComponentFileID, getAssetPath(oldComponentPrefabFile[0]), oldScriptGUID);

        Debug.Log("oldScriptFileID:" + oldScriptFileID);
        Debug.Log("oldScriptGUID:" + oldScriptGUID);

        List<string> allPrefabPaths = getAllPrefabsFromPaths(executePaths);
        Debug.Log("begin:replaceTextComponents,prefab num:" + allPrefabPaths.Count);

        foreach (string file in allPrefabPaths)
        {
            texts.Clear();
            oldFileIDs.Clear();
            replaceStringPairs.Clear();

            string path = getAssetPath(file);
            //Debug.Log("Prepare path:"+ path);
            getAllTextComponents(path);
            getReplaceStringPairs(path); 
            updatePrefab(path);
        }

        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        Debug.Log("Replace Complete");
    }
    private static List<string> getAllPrefabsFromPaths(List<string> executePaths)
    {
        List<string> allPrefabPaths = new List<string>();
        foreach (string dir in executePaths)
        {
            string absolute_dir = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf('/')) + "/" + dir;
            if (Directory.Exists(dir))
            {
                string[] files = Directory.GetFiles(absolute_dir, "*.prefab", SearchOption.AllDirectories);
                allPrefabPaths.AddRange(files);
            }
            if (Path.GetExtension(absolute_dir).Equals(".prefab"))
            {
                allPrefabPaths.Add(absolute_dir);
            }
        }
        return allPrefabPaths;
    }
    private static string getScriptGUIDbyFilePath(string prefabPath)
    {
        Regex rg = new Regex(@"(?<=m_Script:\s{fileID:\s-?[0-9]+, guid:\s)[a-z0-9]{32,}(?=,)");
        using (StreamReader sr = new StreamReader(prefabPath))
        {
            int beginLineNumber = 3;
            for (int i = 0; i < beginLineNumber - 1; i++)
            {
                sr.ReadLine();
            }
            string line;
            while (!string.IsNullOrEmpty(line = sr.ReadLine()))
            {
                MatchCollection mc_Scripts = rg.Matches(line);
                if (mc_Scripts.Count != 0)
                {
                    return mc_Scripts[0].ToString();
                }
            }
        }
        return "";
    }
    private static long getScriptFileIDbyFileID(long newComponentFileID, string prefabPath, string matchString)
    {
        using (StreamReader sr = new StreamReader(prefabPath))
        {
            int beginLineNumber = 3;
            for (int i = 0; i < beginLineNumber - 1; i++)
            {
                sr.ReadLine();
            }
            string line;
            while (!string.IsNullOrEmpty(line = sr.ReadLine()))
            {
                if (line.StartsWith("---"))
                {
                    MatchCollection mc_ComponentFileID = rg_Number.Matches(line);
                    if (newComponentFileID == long.Parse(mc_ComponentFileID[1].ToString()))
                    {
                        long fileID = 0;
                        string guid = "";
                        while (!string.IsNullOrEmpty(line = sr.ReadLine()))
                        {
                            MatchCollection mc = rg_FileID.Matches(line);
                            MatchCollection mc_guid = rg_GUID.Matches(line);
                            if (mc.Count != 0 && long.Parse(mc[0].ToString()) != 0)
                            {
                                if (mc_guid.Count != 0 && !string.IsNullOrEmpty(mc_guid[0].ToString()))
                                {
                                    guid = mc_guid[0].ToString();
                                    if (guid == matchString)
                                    {
                                        fileID = long.Parse(mc[0].ToString());
                                        return fileID;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return 0;
    }
    private static void getAllTextComponents(string prefabPath)
    {
        var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
        Text[] comps_Old = prefab.GetComponentsInChildren<Text>();
        foreach (Text com_Old in comps_Old)
        {
            texts.Add(com_Old);
            long old_fileID = getFileID(com_Old);
            if (!oldFileIDs.Contains(old_fileID))
            {
                oldFileIDs.Add(old_fileID);
            }
        }
    }
    private static void getReplaceStringPairs(string prefabPath)
    {
        using (StreamReader sr = new StreamReader(prefabPath))
        {
            int beginLineNumber = 3;
            for (int i = 0; i < beginLineNumber - 1; i++)
            {
                sr.ReadLine();
            }
            string line;
            int index = 0;
            while (!string.IsNullOrEmpty(line = sr.ReadLine()))
            {
                index++;
                if (line.StartsWith("---"))
                {
                    MatchCollection mc_ComponentFileID = rg_Number.Matches(line);
                    long thisComID = long.Parse(mc_ComponentFileID[1].ToString());
                    if (oldFileIDs.Contains(thisComID))
                    {
                        long this_FileID = 0;
                        string this_GUID = "";
                        while (!string.IsNullOrEmpty(line = sr.ReadLine()))
                        {
                            index++;

                            if (line.StartsWith("---"))
                            {
                                mc_ComponentFileID = rg_Number.Matches(line);
                                thisComID = long.Parse(mc_ComponentFileID[1].ToString());
                                if (! oldFileIDs.Contains(thisComID))
                                {
                                    break;
                                }
                            }

                            MatchCollection mc = rg_FileID.Matches(line);
                            MatchCollection mc_guid = rg_GUID.Matches(line);
                            if (mc.Count != 0 && long.Parse(mc[0].ToString()) != 0)
                            {
                                if (mc_guid.Count != 0 && !string.IsNullOrEmpty(mc_guid[0].ToString()))
                                {
                                    this_FileID = long.Parse(mc[0].ToString());
                                    this_GUID = mc_guid[0].ToString();
                                    if (oldScriptGUID == this_GUID || oldScriptFileID == this_FileID)
                                    {
                                        if (this_GUID != "" && this_FileID != 0)
                                        {
                                            string replace_old = "fileID: " + this_FileID + ", guid: " + this_GUID;
                                            string replace_new = "fileID: " + newScriptFileID + ", guid: " + newScriptGUID;
                                            if (!replaceStringPairs.ContainsKey(replace_old))
                                            {
                                                replaceStringPairs.Add(replace_old, replace_new);
                                            }
                                            break;
                                        }
                                        else
                                        {
                                            Debug.LogError("this_GUID and this_FileID is null.");
                                        }
                                        
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    private static void updatePrefab(string prefabPath)
    {
        string con;
        bool changed = false;
        using (FileStream fs = new FileStream(prefabPath, FileMode.Open, FileAccess.Read))
        {
            using (StreamReader sr = new StreamReader(fs))
            {
                con = sr.ReadToEnd();

                foreach (KeyValuePair<string, string> rsp in replaceStringPairs)
                {
                    con = con.Replace(rsp.Key, rsp.Value);
                    Debug.Log("Find and Replace:" + rsp.Key + "->" + rsp.Value);
                    changed = true;
                }
            }
        }
        if (changed)
        {
            using (StreamWriter sw = new StreamWriter(prefabPath,false))
            {
                try
                {
                    sw.WriteLine(con);
                }
                catch (System.Exception ex)
                {
                    Debug.LogError(ex.ToString());
                }
            }
        }
    }

    private static string getAssetPath(string str)
    {
        var path = str.Replace(@"\", "/");
        path = path.Substring(path.IndexOf("Assets"));
        return path;
    }
    private static PropertyInfo inspectorMode = typeof(SerializedObject).GetProperty("inspectorMode", BindingFlags.NonPublic | BindingFlags.Instance);
    private static long getFileID(UnityEngine.Object target)
    {
        SerializedObject serializedObject = new SerializedObject(target);
        inspectorMode.SetValue(serializedObject, InspectorMode.Debug, null);
        SerializedProperty localIdProp = serializedObject.FindProperty("m_LocalIdentfierInFile");
        return localIdProp.longValue;
    }
    #endregion
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章