/* * @Author: 白哉 * @Description: * @Date: 2025年05月26日 星期一 17:05:14 * @Modify: */ using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using Logic.Multilingual; using TMPro; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; namespace Logic.Editor { public class MultilingualEditorWindow : EditorWindow { // 滑条 private Vector2 _barPosition; // 背景 private GUIStyle _redBoxStyle; private GUIStyle _whiteBoxStyle; private MultilingualData _asset; private Dictionary _zhStrDict = new Dictionary(); private HashSet _activeSet = new HashSet(); private uint _idIndex; private int _showIndex = 0; private List _assets = new List(); private HashSet characterSet = new HashSet(); [MenuItem("Tools/多语言编辑器")] private static void ShowWindow() { var window = CreateWindow(); window.titleContent = new GUIContent("多语言编辑器"); window.Show(); window.minSize = new Vector2(500, 600); } protected virtual void OnEnable() { } private void OnDisable() { } private void OnGUI() { if (!_asset) { var path = $"Assets/Resources/Export/Multilingual.asset"; _asset = AssetDatabase.LoadAssetAtPath(path); if (!_asset) { _asset = CreateInstance(); AssetDatabase.CreateAsset(_asset, path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } } if (_assets.Count == 0) { var pathList = new List(); pathList.Add($"Assets/Fonts/SourceHanSansCN-Bold SDF.asset"); pathList.Add($"Assets/Fonts/SourceHanSansCN-ExtraLight SDF.asset"); pathList.Add($"Assets/Fonts/SourceHanSansCN-Regular SDF.asset"); foreach (var path in pathList) { var asset = AssetDatabase.LoadAssetAtPath(path); if (asset) _assets.Add(asset); } } if (_redBoxStyle == null) { _redBoxStyle = InspectorUtils.GetHelpBoxStyle(); InspectorUtils.AddBorder(_redBoxStyle, new Color(0.5f, 0.4f, 0.4f, 0.6f)); } if (_whiteBoxStyle == null) { _whiteBoxStyle = InspectorUtils.GetHelpBoxStyle(); InspectorUtils.AddBorder(_whiteBoxStyle, new Color(1f, 1f, 1f, 0.2f)); } GUI.skin.button.wordWrap = true; _barPosition = EditorGUILayout.BeginScrollView(_barPosition); EditorGUILayout.BeginHorizontal(); if (InspectorUtils.InspectorButtonWithTextWidth("保存")) { EditorUtility.SetDirty(_asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } if (InspectorUtils.InspectorButtonWithTextWidth("清空")) { _asset.Items.Clear(); _asset.RefreshDict(); } if (InspectorUtils.InspectorButtonWithTextWidth("导出 Excel")) { AssetExportToExcel(); } if (InspectorUtils.InspectorButtonWithTextWidth("导出 Excel Prefab")) { AssetExportToExcelPrefab(); } if (InspectorUtils.InspectorButtonWithTextWidth("Excel 导回")) { ExcelExportToAsset(); } if (InspectorUtils.InspectorButtonWithTextWidth("生成基础配置(中文字体)")) { var uiObj = GameObject.Find("UICanvas"); var coms = uiObj.GetComponentsInChildren(true).ToList(); foreach (var com in coms) { var textCom = com.gameObject.GetComponent(); if (!textCom) continue; if (com.GetMultiTextConfig(MultilingualType.ZH) == null) { var cfg = new MultiTextConfig(textCom, MultilingualType.ZH); com.TextCfg.Add(cfg); } if (com.FontID == 0) com.FontID = _asset.GetFontGroupID(textCom.font); } } if (InspectorUtils.InspectorButtonWithTextWidth("清除预览")) { var uiObj = GameObject.Find("UICanvas"); var coms = uiObj.GetComponentsInChildren(true).ToList(); foreach (var com in coms) { var text = _asset.GetMultilingualStr(com.ID, MultilingualType.ZH); var font = _asset.GetMultilingualFont(com.FontID, MultilingualType.ZH); com.SetMultilingualText(text, MultilingualType.ZH, font); var textCom = com.GetComponent(); textCom?.ForceMeshUpdate(); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); if (InspectorUtils.InspectorButtonWithTextWidth("添加字体组")) { _asset.FontGroups.Add(new MultilingualFontGroup()); } if (InspectorUtils.InspectorButtonWithTextWidth("检查字符集是否有新增")) { string path = "Assets/Fonts/ChineseCharSet.txt"; if (string.IsNullOrEmpty(path)) { EditorUtility.DisplayDialog("字符集提示", "找不到字符集TXT", "确定"); } else { characterSet.Clear(); string content = System.IO.File.ReadAllText(path); foreach (var c in content) characterSet.Add(c); bool isNeedUpdate = false; foreach (var item in _asset.Items) { foreach (var c in item.ZH) { if (!IsChinese(c)) continue; if (!characterSet.Contains(c)) isNeedUpdate = true; } } if (isNeedUpdate)EditorUtility.DisplayDialog("字符集提示", "有新增,请创建新的字符集TXT", "确定"); else EditorUtility.DisplayDialog("字符集提示", "无新增,无需理会", "确定"); } } if (InspectorUtils.InspectorButtonWithTextWidth("创建新的字符集TXT")) { OnBuildChineseTxt(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginVertical(_whiteBoxStyle); if (_asset.TargetTypes.Count != 5) { _asset.TargetTypes.Clear(); for (int i = (int)MultilingualType.ZH; i <= (int)MultilingualType.KR; i++) _asset.TargetTypes.Add((MultilingualType)i); } for (int i = 0; i < _asset.TargetTypes.Count; i++) { EditorGUILayout.BeginHorizontal(); var type = (MultilingualType)(i + 1); InspectorUtils.InspectorTextWidthRich($"系统语言{type} 对应游戏语言: "); _asset.TargetTypes[i] = (MultilingualType)EditorGUILayout.EnumPopup(_asset.TargetTypes[i], GUILayout.Width(200)); EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); foreach (var asset in _assets) { EditorGUILayout.BeginHorizontal(); if (InspectorUtils.InspectorButtonWithTextWidth($"·")) Selection.activeObject = asset; EditorGUI.BeginDisabledGroup(true); // 开始禁用组 EditorGUILayout.ObjectField(asset, typeof(TMP_FontAsset), GUILayout.Width(400)); EditorGUI.EndDisabledGroup(); // 结束禁用组 EditorGUILayout.EndHorizontal(); } var deleteSet = new HashSet(); for (int i = 0; i < _asset.FontGroups.Count; i++) { _asset.FontGroups[i].FontID = (uint)i + 1; if (!ShowFontGroup(_asset.FontGroups[i])) continue; deleteSet.Add(_asset.FontGroups[i]); } foreach (var deleteGroup in deleteSet) _asset.FontGroups.Remove(deleteGroup); ShowAllMultilingualItem(); EditorGUILayout.EndScrollView(); } private void ShowAllMultilingualItem() { int maxIndex = (_asset.Items.Count - 1) / 10; _showIndex = Mathf.Clamp(_showIndex, 0, maxIndex); EditorGUILayout.BeginHorizontal(); if (_showIndex > 0 && InspectorUtils.InspectorButtonWithTextWidth("上一页")) _showIndex--; if (_showIndex < maxIndex && InspectorUtils.InspectorButtonWithTextWidth("下一页")) _showIndex++; EditorGUILayout.EndHorizontal(); for (int i = _showIndex * 10; i < (_showIndex + 1) * 10; i++) { if (i < 0 || i >= _asset.Items.Count) continue; ShowMultilingualItem(_asset.Items[i]); } } private bool ShowFontGroup(MultilingualFontGroup fontGroup) { var isDelete = false; EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); InspectorUtils.InspectorTextWidthRich($"ID : {fontGroup.FontID}"); if (InspectorUtils.InspectorButtonWithTextWidth("x")) isDelete = true; EditorGUILayout.EndHorizontal(); fontGroup.ZHFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.ZHFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.TDZHFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.TDZHFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.ENFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.ENFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.JPFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.JPFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); fontGroup.KRFont = (TMP_FontAsset)EditorGUILayout.ObjectField(fontGroup.KRFont, typeof(TMP_FontAsset), false, GUILayout.Width(400)); EditorGUILayout.EndVertical(); EditorGUILayout.Space(); return isDelete; } private void ShowMultilingualItem(MultilingualItem item) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); InspectorUtils.InspectorTextWidthRich($"{item.ID} : "); InspectorUtils.InspectorTextWidthRich($" 中文: {item.ZH}"); if (!string.IsNullOrEmpty(item.TDZH)) InspectorUtils.InspectorTextWidthRich($" 繁中: {item.TDZH}"); if (!string.IsNullOrEmpty(item.EN)) InspectorUtils.InspectorTextWidthRich($" 英语: {item.EN}"); if (!string.IsNullOrEmpty(item.JP)) InspectorUtils.InspectorTextWidthRich($" 日语: {item.JP}"); if (!string.IsNullOrEmpty(item.KR)) InspectorUtils.InspectorTextWidthRich($" 韩语: {item.KR}"); var unicode = ""; foreach (var c in item.ZH) unicode += $"{(int)c:X4} "; InspectorUtils.InspectorTextWidthRich($" Unicode: {unicode}"); EditorGUILayout.EndVertical(); EditorGUILayout.Space(); } private void DuplicateRemoval() { // 排序 asset.items 保证id从小到大 _asset.Items = _asset.Items.OrderBy(i => i.ID).ToList(); _zhStrDict.Clear(); var deleteItem = new HashSet(); foreach (var item in _asset.Items) { item.Refresh(); if (_zhStrDict.ContainsKey(item.ZH)) { deleteItem.Add(item); continue; } _zhStrDict[item.ZH] = item.ID; } foreach (var item in deleteItem) _asset.Items.Remove(item); _asset.RefreshDict(); } private string RemoveCsvQuotes(string field) { if (string.IsNullOrEmpty(field)) return field; // 去除首尾空格和换行符 field = field.Trim(); // 若字段以引号开头和结尾,则去除引号并处理内部转义 if (field.Length >= 2 && field.StartsWith("\"") && field.EndsWith("\"")) { field = field.Substring(1, field.Length - 2) // 去除首尾引号 .Replace("\"\"", "\""); // 将转义引号还原为单个引号 } return field; } private void ExcelExportToAsset() { GetExcelData(); _asset.RefreshDict(); string context; using (var reader = new StreamReader("../Tools/MultilingualTxt.txt", Encoding.Default, true)) { context = reader.ReadToEnd(); } var lines = context.Split("!@#$%"); foreach (string line in lines) { if (string.IsNullOrWhiteSpace(line)) continue; // 跳过空行 string[] cells = line.Split("%$#@!"); if (cells.Length == 0) continue; var id = uint.Parse(cells[0]); MultilingualItem item; if (_asset.ItemDict.TryGetValue(id, out var value)) item = value; else { item = new MultilingualItem(); _asset.Items.Add(item); } item.ID = id; if (cells.Length >= 2) item.ZH = RemoveCsvQuotes(cells[1]); if (cells.Length >= 3) item.TDZH = RemoveCsvQuotes(cells[2]); if (cells.Length >= 4) item.EN = RemoveCsvQuotes(cells[3]); if (cells.Length >= 5) item.JP = RemoveCsvQuotes(cells[4]); if (cells.Length >= 6) item.KR = RemoveCsvQuotes(cells[5]); } EditorUtility.SetDirty(_asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private void AssetExportToExcel() { DuplicateRemoval(); _zhStrDict.Clear(); _activeSet.Clear(); foreach (var item in _asset.Items) _zhStrDict[item.ZH] = item.ID; if (_asset.Items.Count != 0) _idIndex = _asset.Items[^1].ID + 1; else _idIndex = 1; var uiObj = GameObject.Find("UICanvas"); if (!uiObj) { Debug.LogError($"找不到UI根节点"); return; } var coms = uiObj.GetComponentsInChildren(true).ToList(); foreach (var com in coms) { if (!Regex.IsMatch(com.text, @"[\u4E00-\u9FFF\u3400-\u4DBFa-zA-Z]")) continue; // 去除首尾空格和换行符 com.text = com.text.Trim().Replace("\r\n", "\n"); var textCom = com.gameObject.GetComponent(); if (!textCom) textCom = com.gameObject.AddComponent(); if (_zhStrDict.ContainsKey(com.text)) { textCom.ID = _zhStrDict[com.text]; } else { textCom.ID = _idIndex; _zhStrDict[com.text] = _idIndex; _idIndex++; } _activeSet.Add(textCom.ID); } // 最后处理 assets var path = $"Assets/Resources/DataAssets/"; string[] assetPaths = Directory.GetFiles(path, "*.asset", SearchOption.AllDirectories); foreach (var assetPath in assetPaths) { var asset = AssetDatabase.LoadAssetAtPath(assetPath); if (!asset) continue; ScriptableObject newAsset = Object.Instantiate(asset); TraverseObject(newAsset); SaveExportAsset(asset.name, newAsset); } _asset.RefreshDict(); foreach (var kv in _zhStrDict) { if (_asset.ItemDict.ContainsKey(kv.Value)) continue; var item = new MultilingualItem(); item.ID = kv.Value; item.ZH = kv.Key; _asset.Items.Add(item); } // 排序 asset.items 保证id从小到大 _asset.Items = _asset.Items.OrderBy(i => i.ID).ToList(); string filePath = "../Tools/MultilingualTxt.txt"; if (!File.Exists(filePath)) { using (File.Create(filePath)) { } // 立即释放句柄 } using (StreamWriter sw = new StreamWriter(filePath, false, Encoding.UTF8)) { StringBuilder sb = new StringBuilder(); foreach (var item in _asset.Items) { var active = _activeSet.Contains(item.ID) ? 1 : 0; sb.Append($"{item.ID}%$#@!{item.ZH}%$#@!{item.TDZH}%$#@!{item.EN}%$#@!{item.JP}%$#@!{item.KR}%$#@!{active}!@#$%"); } sw.Write(sb.ToString()); } WriteToExcel(); EditorUtility.SetDirty(_asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private void AssetExportToExcelPrefab() { DuplicateRemoval(); _zhStrDict.Clear(); _activeSet.Clear(); foreach (var item in _asset.Items) _zhStrDict[item.ZH] = item.ID; if (_asset.Items.Count != 0) _idIndex = _asset.Items[^1].ID + 1; else _idIndex = 1; // 先处理 Assets/Resources/Prefab 路径下的所有 prefab 并保存 var coms = new List(); var prefabList = new List(); var prefabPath = $"Assets/Resources/Prefab/"; if (Directory.Exists(prefabPath)) { string[] prefabPaths = Directory.GetFiles(prefabPath, "*.prefab", SearchOption.AllDirectories); foreach (var prefabAssetPath in prefabPaths) { var prefab = AssetDatabase.LoadAssetAtPath(prefabAssetPath); if (!prefab) continue; var prefabComs = prefab.GetComponentsInChildren(true).ToList(); if (prefabComs.Count == 0) continue; foreach (var com in prefabComs)coms.Add(com); prefabList.Add(prefab); } } foreach (var com in coms) { if (!Regex.IsMatch(com.text, @"[\u4E00-\u9FFF\u3400-\u4DBFa-zA-Z]")) continue; // 去除首尾空格和换行符 com.text = com.text.Trim().Replace("\r\n", "\n"); var textCom = com.gameObject.GetComponent(); if (!textCom) textCom = com.gameObject.AddComponent(); if (_zhStrDict.ContainsKey(com.text)) { textCom.ID = _zhStrDict[com.text]; } else { textCom.ID = _idIndex; _zhStrDict[com.text] = _idIndex; _idIndex++; } _activeSet.Add(textCom.ID); } foreach (var prefab in prefabList) EditorUtility.SetDirty(prefab); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); _asset.RefreshDict(); foreach (var kv in _zhStrDict) { if (_asset.ItemDict.ContainsKey(kv.Value)) continue; var item = new MultilingualItem(); item.ID = kv.Value; item.ZH = kv.Key; _asset.Items.Add(item); } // 排序 asset.items 保证id从小到大 _asset.Items = _asset.Items.OrderBy(i => i.ID).ToList(); string filePath = "../Tools/MultilingualTxt.txt"; if (!File.Exists(filePath)) { using (File.Create(filePath)) { } // 立即释放句柄 } using (StreamWriter sw = new StreamWriter(filePath, false, Encoding.UTF8)) { StringBuilder sb = new StringBuilder(); foreach (var item in _asset.Items) { var active = _activeSet.Contains(item.ID) ? 1 : 0; sb.Append($"{item.ID}%$#@!{item.ZH}%$#@!{item.TDZH}%$#@!{item.EN}%$#@!{item.JP}%$#@!{item.KR}%$#@!{active}!@#$%"); } sw.Write(sb.ToString()); } WriteToExcel(); EditorUtility.SetDirty(_asset); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private void SaveExportAsset(string name, ScriptableObject target) { if (target == null) return; // 处理目标路径 string targetPath = $"Assets/Resources/Export/{name}.asset"; if (AssetDatabase.LoadAssetAtPath(targetPath) != null) { AssetDatabase.DeleteAsset(targetPath); } // 保存新实例 AssetDatabase.CreateAsset(target, targetPath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private void TraverseObject(object asset) { if (asset == null) return; var fields = asset.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); foreach (var field in fields) { var value = field.GetValue(asset); var attr = field.GetCustomAttribute(); if (attr != null) { if (value is string s) { var str = s.Trim().Replace("\r\n", "\n"); if (string.IsNullOrEmpty(str)) continue; if (_zhStrDict.TryGetValue(str, out var id)) { field.SetValue(asset, id.ToString()); } else { _zhStrDict[str] = _idIndex; field.SetValue(asset, _zhStrDict[str].ToString()); _idIndex++; } _activeSet.Add(_zhStrDict[str]); continue; } if (value is List list) { for (int i = 0; i < list.Count; i++) { var str = list[i].Trim().Replace("\r\n", "\n"); if (string.IsNullOrEmpty(str)) continue; if (_zhStrDict.TryGetValue(str, out var id)) { list[i] = id.ToString(); } else { _zhStrDict[str] = _idIndex; list[i] = _zhStrDict[str].ToString(); _idIndex++; } _activeSet.Add(_zhStrDict[str]); } field.SetValue(asset, list); continue; } } if (value == null) continue; if (value is IEnumerable enumerable) { foreach (object item in enumerable) { TraverseObject(item); // 递归处理集合项 } } // 如果是自定义对象(非基础类型),递归处理 else if (!value.GetType().IsPrimitive) TraverseObject(value); } } public void WriteToExcel() { var pythonScript = $"../Tools/ExportStringToExcel.py"; ProcessStartInfo start = new ProcessStartInfo { FileName = "python", Arguments = $"\"{pythonScript}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardErrorEncoding = Encoding.UTF8 }; using (var process = Process.Start(start)) { string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); // 获取错误信息 process.WaitForExit(); Debug.Log($"Exit Code: {process.ExitCode}"); // 打印退出码 Debug.Log($"Output: {output}"); Debug.Log($"Error: {error}"); // 打印错误信息 } } public void GetExcelData() { var pythonScript = $"../Tools/PrintExcelString.py"; ProcessStartInfo start = new ProcessStartInfo { FileName = "python", Arguments = $"\"{pythonScript}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardErrorEncoding = Encoding.UTF8 }; using (var process = Process.Start(start)) { string output = process.StandardOutput.ReadToEnd(); string error = process.StandardError.ReadToEnd(); // 获取错误信息 process.WaitForExit(); Debug.Log($"Exit Code: {process.ExitCode}"); // 打印退出码 Debug.Log($"Output: {output}"); Debug.Log($"Error: {error}"); // 打印错误信息 } } private void OnBuildChineseTxt() { characterSet.Clear(); AddBasicCharacters(); ExtractChineseFromI2Languages(); ExportCharacterSet(); } private void AddBasicCharacters() { // 添加大写字母 for (char c = 'A'; c <= 'Z'; c++) { characterSet.Add(c); } // 添加小写字母 for (char c = 'a'; c <= 'z'; c++) { characterSet.Add(c); } // 添加数字 for (char c = '0'; c <= '9'; c++) { characterSet.Add(c); } // 添加常用标点符号 string punctuations = ",.!?;:\"'()[]{}+-*/=_<>@#$%^&|\\~|/\~{}[]【】「」『』《》〈〉:;“” ‘’"、,。?!…—--_+-=×÷"; foreach (char c in punctuations) { characterSet.Add(c); } } private void ExtractChineseFromI2Languages() { var path = $"Assets/Resources/Export/Multilingual.asset"; var asset = AssetDatabase.LoadAssetAtPath(path); if (asset == null) { EditorUtility.DisplayDialog("错误", "未找到多语言资源文件,请确认路径是否正确。", "确定"); return; } foreach (var item in asset.Items) { foreach (var c in item.ZH) { if (!IsChinese(c)) continue; characterSet.Add(c); } } } private bool IsChinese(char c) { return (c >= 0x4E00 && c <= 0x9FFF) || // CJK统一汉字 (c >= 0x3400 && c <= 0x4DBF) || // CJK扩展A (c >= 0x20000 && c <= 0x2A6DF); // CJK扩展B } private void ExportCharacterSet() { string path = EditorUtility.SaveFilePanel("保存字符集", "Assets/Fonts", "ChineseCharSet", "txt"); if (!string.IsNullOrEmpty(path)) { File.WriteAllText(path, string.Join("", characterSet.OrderBy(c => c)), Encoding.UTF8); EditorUtility.DisplayDialog("成功", "字符集已导出!", "确定"); } } } }