// Assets/Editor/BulkTextTool.cs using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; #if TMP_PRESENT using TMPro; #endif public class BulkTextTool : EditorWindow { // 扫描范围 private List targetFolders = new List(); // 空 = 全项目 private bool includeScenes = true; private bool includePrefabs = true; // UGUI Text 选项 private bool applyUiText = true; private bool uiSetFont; private Font uiFont; private bool uiSetFontSize; private int uiFontSize = 24; private bool uiSetColor; private Color uiColor = Color.white; private bool uiSetRichText; private bool uiRichText = true; private bool uiSetRaycast; private bool uiRaycast = false; private bool uiSetAlignment; private TextAnchor uiAlignment = TextAnchor.MiddleCenter; private bool uiSetHorizontalOverflow; private HorizontalWrapMode uiHWrap = HorizontalWrapMode.Wrap; private bool uiSetVerticalOverflow; private VerticalWrapMode uiVWrap = VerticalWrapMode.Truncate; // TMP 选项 #if TMP_PRESENT private bool applyTmpText = true; private bool tmpSetFontAsset; private TMP_FontAsset tmpFontAsset; private bool tmpSetFontSize; private float tmpFontSize = 24f; private bool tmpSetColor; private Color tmpColor = Color.white; private bool tmpSetRaycast; private bool tmpRaycast = false; private bool tmpSetAlignment; private TextAlignmentOptions tmpAlignment = TextAlignmentOptions.Center; private bool tmpSetWrapping; private bool tmpEnableWordWrapping = true; private bool tmpSetOverflow; private TextOverflowModes tmpOverflow = TextOverflowModes.Overflow; #else private const bool applyTmpText = false; // 未检测到 TMP #endif // 执行控制 private bool dryRun = false; // 报表 private int totalAssetsProcessed; private int totalComponentsChanged; private int totalErrors; [MenuItem("Tools/Bulk Text Tool")] public static void Open() => GetWindow("Bulk Text Tool"); private void OnGUI() { EditorGUILayout.LabelField("扫描范围", EditorStyles.boldLabel); includeScenes = EditorGUILayout.Toggle(new GUIContent("包含场景 (.unity)"), includeScenes); includePrefabs = EditorGUILayout.Toggle(new GUIContent("包含预制体 (.prefab)"), includePrefabs); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Use Selected Folders")) { targetFolders = Selection.GetFiltered(SelectionMode.Assets) .Select(AssetDatabase.GetAssetPath) .Select(path => AssetDatabase.IsValidFolder(path) ? path : Path.GetDirectoryName(path).Replace("\\", "/")) .Where(p => !string.IsNullOrEmpty(p)) .Distinct() .ToList(); } if (GUILayout.Button("Clear Folders")) targetFolders.Clear(); } EditorGUILayout.HelpBox( targetFolders.Count == 0 ? "范围:全项目" : $"范围:{string.Join(", ", targetFolders)}", MessageType.Info); EditorGUILayout.Space(); DrawUiTextSection(); EditorGUILayout.Space(); DrawTmpTextSection(); EditorGUILayout.Space(); dryRun = EditorGUILayout.Toggle(new GUIContent("Dry run(仅预览,不保存)"), dryRun); EditorGUILayout.Space(); using (new EditorGUILayout.HorizontalScope()) { GUI.enabled = includeScenes || includePrefabs; if (GUILayout.Button("Run(场景+预制体)")) RunBatch(includeScenes, includePrefabs); GUI.enabled = includeScenes; if (GUILayout.Button("Run(仅场景)")) RunBatch(true, false); GUI.enabled = includePrefabs; if (GUILayout.Button("Run(仅预制体)")) RunBatch(false, true); GUI.enabled = true; } EditorGUILayout.Space(); EditorGUILayout.LabelField("报表", EditorStyles.boldLabel); EditorGUILayout.LabelField($"处理资产数:{totalAssetsProcessed}"); EditorGUILayout.LabelField($"改动组件数:{totalComponentsChanged}"); EditorGUILayout.LabelField($"错误数:{totalErrors}"); } private void DrawUiTextSection() { EditorGUILayout.LabelField("UGUI Text", EditorStyles.boldLabel); applyUiText = EditorGUILayout.Toggle(new GUIContent("应用到 UnityEngine.UI.Text"), applyUiText); using (new EditorGUI.DisabledScope(!applyUiText)) { uiSetFont = EditorGUILayout.Toggle("修改 Font", uiSetFont); if (uiSetFont) uiFont = (Font)EditorGUILayout.ObjectField("Font", uiFont, typeof(Font), false); uiSetFontSize = EditorGUILayout.Toggle("修改 Font Size", uiSetFontSize); if (uiSetFontSize) uiFontSize = EditorGUILayout.IntField("Font Size", uiFontSize); uiSetColor = EditorGUILayout.Toggle("修改 Color", uiSetColor); if (uiSetColor) uiColor = EditorGUILayout.ColorField("Color", uiColor); uiSetRichText = EditorGUILayout.Toggle("修改 Rich Text", uiSetRichText); if (uiSetRichText) uiRichText = EditorGUILayout.Toggle("Rich Text", uiRichText); uiSetRaycast = EditorGUILayout.Toggle("修改 Raycast Target", uiSetRaycast); if (uiSetRaycast) uiRaycast = EditorGUILayout.Toggle("Raycast Target", uiRaycast); uiSetAlignment = EditorGUILayout.Toggle("修改 Alignment", uiSetAlignment); if (uiSetAlignment) uiAlignment = (TextAnchor)EditorGUILayout.EnumPopup("Alignment", uiAlignment); uiSetHorizontalOverflow = EditorGUILayout.Toggle("修改 Horizontal Wrap", uiSetHorizontalOverflow); if (uiSetHorizontalOverflow) uiHWrap = (HorizontalWrapMode)EditorGUILayout.EnumPopup("Horizontal Wrap", uiHWrap); uiSetVerticalOverflow = EditorGUILayout.Toggle("修改 Vertical Overflow", uiSetVerticalOverflow); if (uiSetVerticalOverflow) uiVWrap = (VerticalWrapMode)EditorGUILayout.EnumPopup("Vertical Overflow", uiVWrap); } } private void DrawTmpTextSection() { #if TMP_PRESENT EditorGUILayout.LabelField("TextMeshPro (TMP_Text)", EditorStyles.boldLabel); applyTmpText = EditorGUILayout.Toggle(new GUIContent("应用到 TMPro.TMP_Text"), applyTmpText); using (new EditorGUI.DisabledScope(!applyTmpText)) { tmpSetFontAsset = EditorGUILayout.Toggle("修改 TMP Font Asset", tmpSetFontAsset); if (tmpSetFontAsset) tmpFontAsset = (TMP_FontAsset)EditorGUILayout.ObjectField("TMP Font Asset", tmpFontAsset, typeof(TMP_FontAsset), false); tmpSetFontSize = EditorGUILayout.Toggle("修改 Font Size", tmpSetFontSize); if (tmpSetFontSize) tmpFontSize = EditorGUILayout.FloatField("Font Size", tmpFontSize); tmpSetColor = EditorGUILayout.Toggle("修改 Color", tmpSetColor); if (tmpSetColor) tmpColor = EditorGUILayout.ColorField("Color", tmpColor); tmpSetRaycast = EditorGUILayout.Toggle("修改 Raycast Target", tmpSetRaycast); if (tmpSetRaycast) tmpRaycast = EditorGUILayout.Toggle("Raycast Target", tmpRaycast); tmpSetAlignment = EditorGUILayout.Toggle("修改 Alignment", tmpSetAlignment); if (tmpSetAlignment) tmpAlignment = (TextAlignmentOptions)EditorGUILayout.EnumPopup("Alignment", tmpAlignment); tmpSetWrapping = EditorGUILayout.Toggle("修改 Word Wrapping", tmpSetWrapping); if (tmpSetWrapping) tmpEnableWordWrapping = EditorGUILayout.Toggle("Enable Word Wrapping", tmpEnableWordWrapping); tmpSetOverflow = EditorGUILayout.Toggle("修改 Overflow Mode", tmpSetOverflow); if (tmpSetOverflow) tmpOverflow = (TextOverflowModes)EditorGUILayout.EnumPopup("Overflow Mode", tmpOverflow); } #else EditorGUILayout.LabelField("TextMeshPro (TMP_Text)", EditorStyles.boldLabel); EditorGUILayout.HelpBox("未检测到 TMP。导入 TextMeshPro 后,在 Scripting Define Symbols 定义 TMP_PRESENT 以启用 TMP 设置。", MessageType.Info); #endif } private void RunBatch(bool processScenes, bool processPrefabs) { totalAssetsProcessed = 0; totalComponentsChanged = 0; totalErrors = 0; var guids = CollectAssetGuids(processScenes, processPrefabs); try { AssetDatabase.StartAssetEditing(); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); bool changed = false; if (path.EndsWith(".unity")) { changed = ProcessScene(path); } else if (path.EndsWith(".prefab")) { changed = ProcessPrefab(path); } totalAssetsProcessed++; if (changed && !dryRun) { AssetDatabase.SaveAssets(); } } } finally { AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(); } EditorUtility.DisplayDialog("Bulk Text Tool", $"完成。\n处理资产数:{totalAssetsProcessed}\n改动组件数:{totalComponentsChanged}\n错误数:{totalErrors}", "OK"); } private IEnumerable CollectAssetGuids(bool scenes, bool prefabs) { var filters = new List(); if (scenes) filters.Add("t:scene"); if (prefabs) filters.Add("t:prefab"); var scope = targetFolders.Count == 0 ? null : targetFolders.ToArray(); var set = new HashSet(); foreach (var f in filters) { var guids = scope == null ? AssetDatabase.FindAssets(f) : AssetDatabase.FindAssets(f, scope); foreach (var g in guids) set.Add(g); } return set; } private bool ProcessScene(string path) { bool changedAny = false; var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single); try { foreach (var root in scene.GetRootGameObjects()) { changedAny |= ProcessHierarchy(root); } if (changedAny && !dryRun) { EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); } } catch (System.Exception ex) { totalErrors++; Debug.LogError($"[BulkTextTool] 场景处理错误:{path}\n{ex}"); } return changedAny; } private bool ProcessPrefab(string path) { bool changedAny = false; var prefab = AssetDatabase.LoadAssetAtPath(path); if (prefab == null) return false; var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab); try { changedAny = ProcessHierarchy(instance); if (changedAny && !dryRun) { PrefabUtility.ApplyPrefabInstance(instance, InteractionMode.AutomatedAction); } } catch (System.Exception ex) { totalErrors++; Debug.LogError($"[BulkTextTool] 预制体处理错误:{path}\n{ex}"); } finally { if (instance != null) DestroyImmediate(instance); } return changedAny; } private bool ProcessHierarchy(GameObject root) { bool changed = false; // UGUI Text if (applyUiText) { var texts = root.GetComponentsInChildren(true); foreach (var t in texts) { if (ApplyUiTextChanges(t)) { totalComponentsChanged++; changed = true; if (!dryRun) EditorUtility.SetDirty(t); } } } // TMP Text #if TMP_PRESENT if (applyTmpText) { var tmps = root.GetComponentsInChildren(true); foreach (var tt in tmps) { if (ApplyTmpTextChanges(tt)) { totalComponentsChanged++; changed = true; if (!dryRun) EditorUtility.SetDirty(tt); } } } #endif return changed; } private bool ApplyUiTextChanges(Text t) { bool changed = false; if (uiSetFont && t.font != uiFont) { t.font = uiFont; changed = true; } if (uiSetFontSize && t.fontSize != uiFontSize) { t.fontSize = uiFontSize; changed = true; } if (uiSetColor && t.color != uiColor) { t.color = uiColor; changed = true; } if (uiSetRichText && t.supportRichText != uiRichText) { t.supportRichText = uiRichText; changed = true; } if (uiSetRaycast && t.raycastTarget != uiRaycast) { t.raycastTarget = uiRaycast; changed = true; } if (uiSetAlignment && t.alignment != uiAlignment) { t.alignment = uiAlignment; changed = true; } if (uiSetHorizontalOverflow && t.horizontalOverflow != uiHWrap) { t.horizontalOverflow = uiHWrap; changed = true; } if (uiSetVerticalOverflow && t.verticalOverflow != uiVWrap) { t.verticalOverflow = uiVWrap; changed = true; } return changed; } #if TMP_PRESENT private bool ApplyTmpTextChanges(TMP_Text tt) { bool changed = false; if (tmpSetFontAsset && tt.font != tmpFontAsset) { tt.font = tmpFontAsset; changed = true; } if (tmpSetFontSize && Mathf.Abs(tt.fontSize - tmpFontSize) > 0.001f) { tt.fontSize = tmpFontSize; changed = true; } if (tmpSetColor && tt.color != tmpColor) { tt.color = tmpColor; changed = true; } if (tmpSetRaycast && tt.raycastTarget != tmpRaycast) { tt.raycastTarget = tmpRaycast; changed = true; } if (tmpSetAlignment && tt.alignment != tmpAlignment) { tt.alignment = tmpAlignment; changed = true; } if (tmpSetWrapping && tt.enableWordWrapping != tmpEnableWordWrapping) { tt.enableWordWrapping = tmpEnableWordWrapping; changed = true; } if (tmpSetOverflow && tt.overflowMode != tmpOverflow) { tt.overflowMode = tmpOverflow; changed = true; } return changed; } #endif }