本文转自: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 replaceStringPairs = new Dictionary();//替换string对_fileID和GUID同时替换
private static List oldFileIDs = new List();
static string oldScriptGUID;
static long oldScriptFileID;
static string newScriptGUID;
static long newScriptFileID;
private static List texts = new List();
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 executePaths = getExecutePaths();
OnGetAllTextContent(executePaths);
}
static void OnGetAllTextContent(List executePaths)
{
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
List allPrefabPaths = getAllPrefabsFromPaths(executePaths);
int count = 0;
foreach (string file in allPrefabPaths)
{
string path = getAssetPath(file);
var prefab = AssetDatabase.LoadAssetAtPath(path);
Text[] comps_Old = prefab.GetComponentsInChildren();
foreach (Text com_Old in comps_Old)
{
count++;
Stack parentNames = new Stack();
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 executePaths = getExecutePaths();
OnExecute(executePaths);
}
private static List getExecutePaths()
{
List executePaths = new List();
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 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(getAssetPath(newComponentPrefabFile[0]));
long newComponentFileID = getFileID(localizationTextPrefab.GetComponent());
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(getAssetPath(oldComponentPrefabFile[0]));
long oldComponentFileID = getFileID(textPrefab.GetComponent());
oldScriptGUID = getScriptGUIDbyFilePath(getAssetPath(oldComponentPrefabFile[0]));
oldScriptFileID = getScriptFileIDbyFileID(oldComponentFileID, getAssetPath(oldComponentPrefabFile[0]), oldScriptGUID);
Debug.Log("oldScriptFileID:" + oldScriptFileID);
Debug.Log("oldScriptGUID:" + oldScriptGUID);
List 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 getAllPrefabsFromPaths(List executePaths)
{
List allPrefabPaths = new List();
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(prefabPath);
Text[] comps_Old = prefab.GetComponentsInChildren();
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 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
}