using System.Collections.Generic;
|
using System.IO;
|
using UnityEditor;
|
using UnityEditor.IMGUI.Controls;
|
using UnityEngine;
|
|
// ======================== 扫描数据模型 ========================
|
|
public class ScanResultItem
|
{
|
public string PrefabPath { get; }
|
public string GameObjectPath { get; }
|
public List<string> MissingLanguages { get; set; } = new List<string>();
|
public TextComponentType ComponentType { get; }
|
public string PrefabGUID { get; }
|
|
public ScanResultItem(string path, string goPath, TextComponentType type, string guid)
|
{
|
PrefabPath = path;
|
GameObjectPath = goPath;
|
ComponentType = type;
|
PrefabGUID = guid;
|
}
|
|
public string GetDisplayName() => $"{GameObjectPath} (缺少: {string.Join(", ", MissingLanguages)}) [{ComponentType}]";
|
}
|
|
public class PrefabScanResult
|
{
|
public string PrefabPath { get; }
|
public string PrefabGUID { get; }
|
public List<ScanResultItem> Items { get; } = new List<ScanResultItem>();
|
|
public PrefabScanResult(string path, string guid)
|
{
|
PrefabPath = path;
|
PrefabGUID = guid;
|
}
|
}
|
|
public class ScanResultSummary
|
{
|
public string ScanDirectory { get; }
|
public int TotalPrefabsScanned { get; set; }
|
public int TotalAdaptersFound { get; private set; }
|
public int AdaptersWithMissingConfig { get; private set; }
|
public List<PrefabScanResult> PrefabResults { get; } = new List<PrefabScanResult>();
|
|
public ScanResultSummary(string dir) => ScanDirectory = dir;
|
|
public void AddResult(ScanResultItem item)
|
{
|
var prefabResult = PrefabResults.Find(p => p.PrefabPath == item.PrefabPath);
|
if (prefabResult == null)
|
{
|
prefabResult = new PrefabScanResult(item.PrefabPath, item.PrefabGUID);
|
PrefabResults.Add(prefabResult);
|
}
|
prefabResult.Items.Add(item);
|
TotalAdaptersFound++;
|
AdaptersWithMissingConfig++;
|
}
|
}
|
|
// ======================== TreeView 实现 ========================
|
|
public class MetadataTreeViewItem : TreeViewItem
|
{
|
public object Metadata { get; }
|
public MetadataTreeViewItem(int id, string name, object meta) : base(id, 0, name) => Metadata = meta;
|
}
|
|
public class ScanResultTreeView : TreeView
|
{
|
private ScanResultSummary m_Summary;
|
|
public ScanResultTreeView(TreeViewState state, ScanResultSummary summary) : base(state)
|
{
|
m_Summary = summary;
|
Reload();
|
ExpandAll();
|
}
|
|
protected override TreeViewItem BuildRoot()
|
{
|
var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };
|
|
// 【关键修复 1】:必须初始化 children 列表。
|
// 否则当扫描结果完美(0个错误)时,root.children 为 null 会导致 Unity 报错
|
root.children = new List<TreeViewItem>();
|
|
if (m_Summary == null || m_Summary.PrefabResults.Count == 0)
|
return root;
|
|
int itemId = 1;
|
foreach (var prefabResult in m_Summary.PrefabResults)
|
{
|
string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
|
var prefabItem = new MetadataTreeViewItem(itemId++, $"{prefabName} ({prefabResult.Items.Count}个问题)", prefabResult);
|
|
foreach (var adapterItem in prefabResult.Items)
|
{
|
var adapterTreeItem = new MetadataTreeViewItem(itemId++, adapterItem.GetDisplayName(), adapterItem);
|
prefabItem.AddChild(adapterTreeItem);
|
}
|
root.AddChild(prefabItem);
|
}
|
|
// 【关键修复 2】:Unity 官方规范要求,手动使用 AddChild 构建树之后,必须调用此方法刷新深度和层级关系
|
SetupDepthsFromParentsAndChildren(root);
|
|
return root;
|
}
|
|
protected override void RowGUI(RowGUIArgs args)
|
{
|
var item = args.item as MetadataTreeViewItem;
|
if (item != null && item.Metadata is PrefabScanResult)
|
GUI.Label(args.rowRect, item.displayName, EditorStyles.boldLabel);
|
else if (item != null && item.Metadata is ScanResultItem adapterItem)
|
GUI.Label(args.rowRect, $"{adapterItem.GameObjectPath} (缺少: {string.Join(", ", adapterItem.MissingLanguages)})");
|
else
|
base.RowGUI(args);
|
}
|
|
protected override void DoubleClickedItem(int id)
|
{
|
var item = FindItem(id, rootItem) as MetadataTreeViewItem;
|
if (item == null) return;
|
|
if (item.Metadata is ScanResultItem adapterItem)
|
PingGameObject(adapterItem.PrefabPath, adapterItem.GameObjectPath);
|
else if (item.Metadata is PrefabScanResult prefabResult)
|
{
|
var obj = AssetDatabase.LoadAssetAtPath<GameObject>(prefabResult.PrefabPath);
|
if (obj != null)
|
{
|
Selection.activeObject = obj;
|
EditorGUIUtility.PingObject(obj);
|
}
|
}
|
}
|
|
private void PingGameObject(string prefabPath, string gameObjectPath)
|
{
|
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
if (prefab == null) return;
|
|
Transform target = prefab.transform.Find(gameObjectPath);
|
if (target != null)
|
{
|
Selection.activeObject = target.gameObject;
|
EditorGUIUtility.PingObject(target.gameObject);
|
}
|
else
|
{
|
Selection.activeObject = prefab;
|
EditorGUIUtility.PingObject(prefab);
|
Debug.LogWarning($"[ScanTool] 找不到路径 '{gameObjectPath}',已选中整个预制体");
|
}
|
}
|
}
|
|
// ======================== 扫描工具主窗口 ========================
|
|
public class TextLanguageAdapterScanTool : EditorWindow
|
{
|
private string m_ScanDirectory = "Assets";
|
private Vector2 m_ScrollPosition;
|
private ScanResultSummary m_ScanResult;
|
private bool m_IsScanning;
|
private float m_ScanProgress;
|
private string m_ScanStatus;
|
|
private ScanResultTreeView m_TreeView;
|
private TreeViewState m_TreeViewState;
|
|
// 批量操作的UI状态
|
private int m_SourceLangIndex = 0;
|
private int m_TargetLangIndex = 0;
|
private bool m_OverwriteExisting = false;
|
|
[MenuItem("程序/TextLanguageAdapter扫描与管理工具")]
|
public static void ShowWindow()
|
{
|
var window = GetWindow<TextLanguageAdapterScanTool>("语言适配器扫描");
|
window.minSize = new Vector2(600f, 500f);
|
}
|
|
private void OnEnable() => TextLanguageAdapterHelper.Initialize();
|
|
private void OnGUI()
|
{
|
DrawHeader();
|
EditorGUILayout.Space(5f);
|
DrawScanSettings();
|
EditorGUILayout.Space(5f);
|
DrawScanButton();
|
EditorGUILayout.Space(5f);
|
DrawResults();
|
EditorGUILayout.Space(5f);
|
DrawBatchOperations();
|
}
|
|
private void DrawHeader()
|
{
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
{
|
EditorGUILayout.LabelField("TextLanguageAdapter 配置缺失扫描与批量操作工具", EditorStyles.boldLabel);
|
EditorGUILayout.HelpBox("扫描指定目录下所有预制体,检测组件的语言配置是否完整,或执行批量语言配置复制。", MessageType.Info);
|
}
|
}
|
|
private void DrawScanSettings()
|
{
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
{
|
EditorGUILayout.LabelField("扫描设置", EditorStyles.boldLabel);
|
|
using (new EditorGUILayout.HorizontalScope())
|
{
|
EditorGUILayout.LabelField("目标目录:", GUILayout.Width(80f));
|
m_ScanDirectory = EditorGUILayout.TextField(m_ScanDirectory);
|
if (GUILayout.Button("选择...", GUILayout.Width(70f)))
|
{
|
string path = EditorUtility.OpenFolderPanel("选择扫描目录", Application.dataPath, "");
|
if (!string.IsNullOrEmpty(path))
|
m_ScanDirectory = path.StartsWith(Application.dataPath) ? "Assets" + path.Substring(Application.dataPath.Length) : path;
|
}
|
}
|
|
EditorGUILayout.Space(5f);
|
EditorGUILayout.LabelField($"预设语言:");
|
|
EditorGUILayout.BeginHorizontal();
|
GUILayout.Space(15f);
|
int displayCount = 0;
|
foreach (var langId in TextLanguageAdapterHelper.PresetLanguageIds)
|
{
|
if (langId == TextLanguageAdapter.DefaultLangId) continue;
|
EditorGUILayout.LabelField(langId, GUILayout.Width(50f));
|
if (++displayCount % 8 == 0)
|
{
|
EditorGUILayout.EndHorizontal();
|
EditorGUILayout.BeginHorizontal();
|
GUILayout.Space(15f);
|
}
|
}
|
EditorGUILayout.EndHorizontal();
|
}
|
}
|
|
private void DrawScanButton()
|
{
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
{
|
using (new EditorGUILayout.HorizontalScope())
|
{
|
using (new EditorGUI.DisabledScope(m_IsScanning || string.IsNullOrEmpty(m_ScanDirectory)))
|
{
|
if (GUILayout.Button("开始扫描", GUILayout.Height(30f))) StartScan();
|
}
|
|
if (m_IsScanning)
|
{
|
EditorGUILayout.LabelField("扫描中...", GUILayout.Width(100f));
|
m_ScanProgress = EditorGUILayout.Slider(m_ScanProgress, 0f, 1f);
|
Repaint();
|
}
|
}
|
if (!string.IsNullOrEmpty(m_ScanStatus)) EditorGUILayout.LabelField(m_ScanStatus);
|
}
|
}
|
|
private void DrawResults()
|
{
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
{
|
EditorGUILayout.LabelField("扫描结果", EditorStyles.boldLabel);
|
|
if (m_ScanResult != null)
|
{
|
EditorGUILayout.LabelField($"预制体总数: {m_ScanResult.TotalPrefabsScanned} | Adapter总数: {m_ScanResult.TotalAdaptersFound} | 缺失配置: {m_ScanResult.AdaptersWithMissingConfig}");
|
EditorGUILayout.Space(5f);
|
|
if (m_TreeView != null)
|
{
|
m_ScrollPosition = EditorGUILayout.BeginScrollView(m_ScrollPosition, GUILayout.MinHeight(150f));
|
var rect = EditorGUILayout.GetControlRect(false, m_TreeView.totalHeight);
|
m_TreeView.OnGUI(rect);
|
EditorGUILayout.EndScrollView();
|
}
|
|
EditorGUILayout.Space(5f);
|
using (new EditorGUILayout.HorizontalScope())
|
{
|
if (GUILayout.Button("展开全部")) m_TreeView?.ExpandAll();
|
if (GUILayout.Button("折叠全部")) m_TreeView?.CollapseAll();
|
}
|
}
|
else
|
{
|
EditorGUILayout.HelpBox("点击「开始扫描」按钮进行扫描", MessageType.None);
|
}
|
}
|
}
|
|
// ======================== 新增:批量操作功能 ========================
|
private void DrawBatchOperations()
|
{
|
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
{
|
EditorGUILayout.LabelField("批量操作 (针对目标目录下的所有预制体上的TextLanguageAdapter组件)", EditorStyles.boldLabel);
|
|
if (TextLanguageAdapterHelper.PresetLanguageIds == null || TextLanguageAdapterHelper.PresetLanguageIds.Length == 0)
|
return;
|
|
using (new EditorGUILayout.HorizontalScope())
|
{
|
EditorGUILayout.LabelField("旧语言:", GUILayout.Width(60f));
|
m_SourceLangIndex = EditorGUILayout.Popup(m_SourceLangIndex, TextLanguageAdapterHelper.PresetLanguageNames);
|
|
GUILayout.Space(20f);
|
|
EditorGUILayout.LabelField("新语言:", GUILayout.Width(60f));
|
m_TargetLangIndex = EditorGUILayout.Popup(m_TargetLangIndex, TextLanguageAdapterHelper.PresetLanguageNames);
|
}
|
|
m_OverwriteExisting = EditorGUILayout.Toggle("覆盖已存在的目标配置", m_OverwriteExisting);
|
|
EditorGUILayout.Space(5f);
|
|
bool isSameLanguage = m_SourceLangIndex == m_TargetLangIndex;
|
using (new EditorGUI.DisabledScope(isSameLanguage || m_IsScanning || string.IsNullOrEmpty(m_ScanDirectory)))
|
{
|
if (GUILayout.Button("批量复制配置", GUILayout.Height(30f)))
|
{
|
string sourceLang = TextLanguageAdapterHelper.PresetLanguageIds[m_SourceLangIndex];
|
string targetLang = TextLanguageAdapterHelper.PresetLanguageIds[m_TargetLangIndex];
|
|
if (EditorUtility.DisplayDialog("高危操作确认",
|
$"此操作将遍历【{m_ScanDirectory}】下所有预制体。\n\n" +
|
$"把它们的 [{sourceLang}] 配置复制并应用到 [{targetLang}] 配置上。\n\n" +
|
$"此操作不可撤销!建议提前使用 Git/SVN 提交代码。\n确定要继续吗?",
|
"确定执行", "取消"))
|
{
|
ExecuteBatchCopy(sourceLang, targetLang);
|
}
|
}
|
}
|
|
if (isSameLanguage)
|
{
|
EditorGUILayout.HelpBox("旧语言与新语言不能相同", MessageType.Warning);
|
}
|
}
|
}
|
|
private void ExecuteBatchCopy(string sourceLang, string targetLang)
|
{
|
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { m_ScanDirectory });
|
if (prefabGuids.Length == 0) return;
|
|
int modifiedPrefabCount = 0;
|
int modifiedAdapterCount = 0;
|
|
try
|
{
|
for (int i = 0; i < prefabGuids.Length; i++)
|
{
|
string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
|
EditorUtility.DisplayProgressBar("批量复制配置", $"处理中 ({i + 1}/{prefabGuids.Length}): {path}", (float)i / prefabGuids.Length);
|
|
// 【核心修改】:直接加载资产内存,不使用 LoadPrefabContents 进行实例化
|
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
bool isModified = false;
|
|
foreach (var adapter in prefabAsset.GetComponentsInChildren<TextLanguageAdapter>(true))
|
{
|
if (adapter.HasConfig(sourceLang))
|
{
|
if (m_OverwriteExisting || !adapter.HasConfig(targetLang))
|
{
|
// 必须标记对象为脏,否则 Unity 不会把资产的修改存盘
|
Undo.RecordObject(adapter, "Batch Copy Language Config");
|
|
var clonedConfig = adapter.GetConfig(sourceLang).Clone();
|
adapter.SetConfig(targetLang, clonedConfig);
|
|
EditorUtility.SetDirty(adapter);
|
isModified = true;
|
modifiedAdapterCount++;
|
}
|
}
|
}
|
|
if (isModified)
|
{
|
modifiedPrefabCount++;
|
}
|
}
|
}
|
finally
|
{
|
EditorUtility.ClearProgressBar();
|
// 统一保存所有标记为 Dirty 的资产修改
|
AssetDatabase.SaveAssets();
|
|
PerformScan();
|
|
EditorUtility.DisplayDialog("批量操作完成",
|
$"批量复制结束!\n\n修改的预制体数量: {modifiedPrefabCount} 个\n更新的适配器配置数量: {modifiedAdapterCount} 个",
|
"确认");
|
}
|
}
|
|
// ======================== 核心扫描逻辑 ========================
|
|
private void StartScan()
|
{
|
m_IsScanning = true;
|
m_ScanProgress = 0f;
|
m_ScanStatus = "准备扫描...";
|
|
EditorApplication.CallbackFunction updateCallback = null;
|
updateCallback = () =>
|
{
|
if (!m_IsScanning)
|
{
|
EditorApplication.update -= updateCallback;
|
return;
|
}
|
PerformScan();
|
m_IsScanning = false;
|
EditorApplication.update -= updateCallback;
|
};
|
EditorApplication.update += updateCallback;
|
}
|
|
private void PerformScan()
|
{
|
m_ScanResult = new ScanResultSummary(m_ScanDirectory);
|
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { m_ScanDirectory });
|
m_ScanResult.TotalPrefabsScanned = prefabGuids.Length;
|
|
if (prefabGuids.Length == 0)
|
{
|
m_ScanStatus = "未找到任何预制体";
|
return;
|
}
|
|
for (int i = 0; i < prefabGuids.Length; i++)
|
{
|
string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
|
ScanPrefab(path, prefabGuids[i]);
|
|
m_ScanProgress = (float)(i + 1) / prefabGuids.Length;
|
m_ScanStatus = $"正在扫描: {Path.GetFileName(path)} ({i + 1}/{prefabGuids.Length})";
|
|
if (i % 10 == 0) Repaint();
|
}
|
|
m_TreeViewState ??= new TreeViewState();
|
m_TreeView = new ScanResultTreeView(m_TreeViewState, m_ScanResult);
|
m_ScanStatus = $"扫描完成! 发现 {m_ScanResult.AdaptersWithMissingConfig} 个缺失配置";
|
Repaint();
|
}
|
|
private void ScanPrefab(string path, string guid)
|
{
|
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
if (prefab == null) return;
|
|
foreach (var adapter in prefab.GetComponentsInChildren<TextLanguageAdapter>(true))
|
{
|
List<string> missing = new List<string>();
|
foreach (var langId in TextLanguageAdapterHelper.PresetLanguageIds)
|
{
|
if (langId == TextLanguageAdapter.DefaultLangId) continue;
|
if (!adapter.HasConfig(langId)) missing.Add(langId);
|
}
|
|
if (missing.Count > 0)
|
{
|
var item = new ScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetTextType, guid)
|
{
|
MissingLanguages = missing
|
};
|
m_ScanResult.AddResult(item);
|
}
|
}
|
}
|
|
private string GetGameObjectPath(GameObject go, GameObject root)
|
{
|
var parts = new List<string>();
|
Transform curr = go.transform;
|
while (curr != null && curr != root.transform)
|
{
|
parts.Insert(0, curr.name);
|
curr = curr.parent;
|
}
|
return string.Join("/", parts);
|
}
|
}
|