376 lines
14 KiB
C#
376 lines
14 KiB
C#
// 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<string> targetFolders = new List<string>(); // 空 = 全项目
|
||
|
|
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<BulkTextTool>("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<Object>(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<string> CollectAssetGuids(bool scenes, bool prefabs)
|
||
|
|
{
|
||
|
|
var filters = new List<string>();
|
||
|
|
if (scenes) filters.Add("t:scene");
|
||
|
|
if (prefabs) filters.Add("t:prefab");
|
||
|
|
|
||
|
|
var scope = targetFolders.Count == 0 ? null : targetFolders.ToArray();
|
||
|
|
var set = new HashSet<string>();
|
||
|
|
|
||
|
|
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<GameObject>(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<Text>(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<TMP_Text>(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
|
||
|
|
}
|