三国卡牌客户端基础资源仓库
lcy
2026-05-11 f661375604d48478ea2b0716d11c9a31c336f013
592 多语言适配 提交主干

1.Text和Image的适配器扫描工具支持查看显示的模式,方便看哪些预制体上有适配器
2.如果现在一个预制体上挂了多个适配器,一部分适配器缺配置,另一部分不缺,也能扫描出来
2个文件已修改
292 ■■■■ 已修改文件
Assets/Editor/UIComponent/ImageLanguageAdapterScanTool.cs 142 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/TextLanguageAdapterScanTool.cs 150 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Assets/Editor/UIComponent/ImageLanguageAdapterScanTool.cs
@@ -4,8 +4,6 @@
using UnityEditor.IMGUI.Controls;
using UnityEngine;
// ======================== 扫描数据模型 ========================
public class ImageScanResultItem
{
    public string PrefabPath { get; }
@@ -44,7 +42,10 @@
    public int TotalPrefabsScanned { get; set; }
    public int TotalAdaptersFound { get; set; }
    public int AdaptersWithMissingConfig { get; private set; }
    public int PrefabsWithIssueCount { get; private set; }
    public int PrefabsWithoutIssueCount { get; private set; }
    public List<ImagePrefabScanResult> PrefabResults { get; } = new List<ImagePrefabScanResult>();
    public List<ImagePrefabScanResult> PrefabResultsWithoutIssue { get; } = new List<ImagePrefabScanResult>();
    public ImageScanResultSummary(string dir) => ScanDirectory = dir;
@@ -55,13 +56,31 @@
        {
            prefabResult = new ImagePrefabScanResult(item.PrefabPath, item.PrefabGUID);
            PrefabResults.Add(prefabResult);
            PrefabsWithIssueCount++;
        }
        prefabResult.Items.Add(item);
        AdaptersWithMissingConfig++;
        if (item.MissingLanguages.Count > 0)
        {
            AdaptersWithMissingConfig++;
        }
    }
    public void AddPrefabWithoutIssue(string path, string guid, List<ImageScanResultItem> items)
    {
        var prefabResult = new ImagePrefabScanResult(path, guid);
        prefabResult.Items.AddRange(items);
        PrefabResultsWithoutIssue.Add(prefabResult);
        PrefabsWithoutIssueCount++;
    }
}
// ======================== TreeView 实现 ========================
public enum ImageScanResultFilterMode
{
    全部,
    仅显示有问题,
    仅显示无问题
}
public class ImageMetadataTreeViewItem : TreeViewItem
{
@@ -72,10 +91,12 @@
public class ImageScanResultTreeView : TreeView
{
    private ImageScanResultSummary m_Summary;
    private ImageScanResultFilterMode m_FilterMode;
    public ImageScanResultTreeView(TreeViewState state, ImageScanResultSummary summary) : base(state)
    public ImageScanResultTreeView(TreeViewState state, ImageScanResultSummary summary, ImageScanResultFilterMode filterMode) : base(state)
    {
        m_Summary = summary;
        m_FilterMode = filterMode;
        Reload();
        ExpandAll();
    }
@@ -86,21 +107,60 @@
        root.children = new List<TreeViewItem>();
        if (m_Summary == null || m_Summary.PrefabResults.Count == 0)
        if (m_Summary == null)
            return root;
        int itemId = 1;
        foreach (var prefabResult in m_Summary.PrefabResults)
        {
            string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
            var prefabItem = new ImageMetadataTreeViewItem(itemId++, $"{prefabName} ({prefabResult.Items.Count}个问题)", prefabResult);
            foreach (var adapterItem in prefabResult.Items)
        bool showWithIssue = m_FilterMode == ImageScanResultFilterMode.全部 ||
                            m_FilterMode == ImageScanResultFilterMode.仅显示有问题;
        bool showWithoutIssue = m_FilterMode == ImageScanResultFilterMode.全部 ||
                               m_FilterMode == ImageScanResultFilterMode.仅显示无问题;
        if (showWithIssue)
        {
            foreach (var prefabResult in m_Summary.PrefabResults)
            {
                var adapterTreeItem = new ImageMetadataTreeViewItem(itemId++, adapterItem.GetDisplayName(), adapterItem);
                prefabItem.AddChild(adapterTreeItem);
                string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
                int issueCount = 0;
                foreach (var item in prefabResult.Items)
                {
                    if (item.MissingLanguages != null && item.MissingLanguages.Count > 0)
                        issueCount++;
                }
                var prefabItem = new ImageMetadataTreeViewItem(itemId++, $"{prefabName} ({issueCount}个问题)", prefabResult);
                foreach (var adapterItem in prefabResult.Items)
                {
                    string displayName;
                    if (adapterItem.MissingLanguages.Count > 0)
                        displayName = adapterItem.GetDisplayName();
                    else
                        displayName = $"{adapterItem.GameObjectPath} (配置完整)";
                    var adapterTreeItem = new ImageMetadataTreeViewItem(itemId++, displayName, adapterItem);
                    prefabItem.AddChild(adapterTreeItem);
                }
                root.AddChild(prefabItem);
            }
            root.AddChild(prefabItem);
        }
        if (showWithoutIssue)
        {
            foreach (var prefabResult in m_Summary.PrefabResultsWithoutIssue)
            {
                string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
                var prefabItem = new ImageMetadataTreeViewItem(itemId++, $"{prefabName} (无问题)", prefabResult);
                foreach (var adapterItem in prefabResult.Items)
                {
                    var displayName = $"{adapterItem.GameObjectPath} (配置完整)";
                    var adapterTreeItem = new ImageMetadataTreeViewItem(itemId++, displayName, adapterItem);
                    prefabItem.AddChild(adapterTreeItem);
                }
                root.AddChild(prefabItem);
            }
        }
        SetupDepthsFromParentsAndChildren(root);
@@ -114,7 +174,12 @@
        if (item != null && item.Metadata is ImagePrefabScanResult)
            GUI.Label(args.rowRect, item.displayName, EditorStyles.boldLabel);
        else if (item != null && item.Metadata is ImageScanResultItem adapterItem)
            GUI.Label(args.rowRect, $"{adapterItem.GameObjectPath} (缺少: {string.Join(", ", adapterItem.MissingLanguages)})");
        {
            if (adapterItem.MissingLanguages.Count > 0)
                GUI.Label(args.rowRect, $"{adapterItem.GameObjectPath} (缺少: {string.Join(", ", adapterItem.MissingLanguages)})");
            else
                GUI.Label(args.rowRect, item.displayName);
        }
        else
            base.RowGUI(args);
    }
@@ -157,8 +222,6 @@
    }
}
// ======================== 扫描工具主窗口 ========================
public class ImageLanguageAdapterScanTool : EditorWindow
{
    private string m_ScanDirectory = "Assets";
@@ -170,6 +233,7 @@
    private ImageScanResultTreeView m_TreeView;
    private TreeViewState m_TreeViewState;
    private ImageScanResultFilterMode m_ResultFilterMode = ImageScanResultFilterMode.全部;
    private int m_SourceLangIndex = 0;
    private int m_TargetLangIndex = 0;
@@ -275,7 +339,20 @@
            if (m_ScanResult != null)
            {
                EditorGUILayout.LabelField($"预制体总数: {m_ScanResult.TotalPrefabsScanned} | Adapter总数: {m_ScanResult.TotalAdaptersFound} | 缺失配置: {m_ScanResult.AdaptersWithMissingConfig}");
                EditorGUILayout.LabelField($"预制体总数: {m_ScanResult.TotalPrefabsScanned} | 有Adapter的预制体: {m_ScanResult.PrefabsWithIssueCount + m_ScanResult.PrefabsWithoutIssueCount} | Adapter总数: {m_ScanResult.TotalAdaptersFound} | 缺失配置: {m_ScanResult.AdaptersWithMissingConfig} | 有问题预制体: {m_ScanResult.PrefabsWithIssueCount} | 无问题预制体: {m_ScanResult.PrefabsWithoutIssueCount}");
                EditorGUILayout.Space(5f);
                using (new EditorGUILayout.HorizontalScope())
                {
                    EditorGUILayout.LabelField("显示模式:", GUILayout.Width(60f));
                    EditorGUI.BeginChangeCheck();
                    m_ResultFilterMode = (ImageScanResultFilterMode)EditorGUILayout.EnumPopup(m_ResultFilterMode);
                    if (EditorGUI.EndChangeCheck() && m_TreeView != null)
                    {
                        m_TreeViewState ??= new TreeViewState();
                        m_TreeView = new ImageScanResultTreeView(m_TreeViewState, m_ScanResult, m_ResultFilterMode);
                    }
                }
                EditorGUILayout.Space(5f);
                if (m_TreeView != null)
@@ -450,7 +527,7 @@
        }
        m_TreeViewState ??= new TreeViewState();
        m_TreeView = new ImageScanResultTreeView(m_TreeViewState, m_ScanResult);
        m_TreeView = new ImageScanResultTreeView(m_TreeViewState, m_ScanResult, m_ResultFilterMode);
        m_ScanStatus = $"扫描完成! 发现 {m_ScanResult.AdaptersWithMissingConfig} 个缺失配置";
        Repaint();
    }
@@ -461,8 +538,12 @@
        if (prefab == null) return;
        var adapters = prefab.GetComponentsInChildren<ImageLanguageAdapter>(true);
        if (adapters.Length == 0) return;
        m_ScanResult.TotalAdaptersFound += adapters.Length;
        bool hasIssue = false;
        List<ImageScanResultItem> allItems = new List<ImageScanResultItem>();
        foreach (var adapter in adapters)
        {
@@ -473,15 +554,30 @@
                if (!adapter.HasConfig(langId)) missing.Add(langId);
            }
            var item = new ImageScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetImageType, guid)
            {
                MissingLanguages = missing
            };
            allItems.Add(item);
            if (missing.Count > 0)
            {
                var item = new ImageScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetImageType, guid)
                {
                    MissingLanguages = missing
                };
                hasIssue = true;
            }
        }
        if (hasIssue)
        {
            foreach (var item in allItems)
            {
                m_ScanResult.AddResult(item);
            }
        }
        else
        {
            m_ScanResult.AddPrefabWithoutIssue(path, guid, allItems);
        }
    }
    private string GetGameObjectPath(GameObject go, GameObject root)
Assets/Editor/UIComponent/TextLanguageAdapterScanTool.cs
@@ -44,7 +44,10 @@
    public int TotalPrefabsScanned { get; set; }
    public int TotalAdaptersFound { get; set; } 
    public int AdaptersWithMissingConfig { get; private set; }
    public int PrefabsWithIssueCount { get; private set; }
    public int PrefabsWithoutIssueCount { get; private set; }
    public List<PrefabScanResult> PrefabResults { get; } = new List<PrefabScanResult>();
    public List<PrefabScanResult> PrefabResultsWithoutIssue { get; } = new List<PrefabScanResult>();
    public ScanResultSummary(string dir) => ScanDirectory = dir;
@@ -55,13 +58,33 @@
        {
            prefabResult = new PrefabScanResult(item.PrefabPath, item.PrefabGUID);
            PrefabResults.Add(prefabResult);
            PrefabsWithIssueCount++;
        }
        prefabResult.Items.Add(item);
        AdaptersWithMissingConfig++;
        if (item.MissingLanguages.Count > 0)
        {
            AdaptersWithMissingConfig++;
        }
    }
    public void AddPrefabWithoutIssue(string path, string guid, List<ScanResultItem> items)
    {
        var prefabResult = new PrefabScanResult(path, guid);
        prefabResult.Items.AddRange(items);
        PrefabResultsWithoutIssue.Add(prefabResult);
        PrefabsWithoutIssueCount++;
    }
}
// ======================== TreeView 实现 ========================
public enum ScanResultFilterMode
{
    全部,
    仅显示有问题,
    仅显示无问题
}
public class MetadataTreeViewItem : TreeViewItem
{
@@ -72,10 +95,12 @@
public class ScanResultTreeView : TreeView
{
    private ScanResultSummary m_Summary;
    private ScanResultFilterMode m_FilterMode;
    public ScanResultTreeView(TreeViewState state, ScanResultSummary summary) : base(state)
    public ScanResultTreeView(TreeViewState state, ScanResultSummary summary, ScanResultFilterMode filterMode) : base(state)
    {
        m_Summary = summary;
        m_FilterMode = filterMode;
        Reload();
        ExpandAll();
    }
@@ -84,28 +109,64 @@
    {
        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)
        if (m_Summary == null)
            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)
        bool showWithIssue = m_FilterMode == ScanResultFilterMode.全部 ||
                            m_FilterMode == ScanResultFilterMode.仅显示有问题;
        bool showWithoutIssue = m_FilterMode == ScanResultFilterMode.全部 ||
                               m_FilterMode == ScanResultFilterMode.仅显示无问题;
        if (showWithIssue)
        {
            foreach (var prefabResult in m_Summary.PrefabResults)
            {
                var adapterTreeItem = new MetadataTreeViewItem(itemId++, adapterItem.GetDisplayName(), adapterItem);
                prefabItem.AddChild(adapterTreeItem);
                string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
                int issueCount = 0;
                foreach (var item in prefabResult.Items)
                {
                    if (item.MissingLanguages != null && item.MissingLanguages.Count > 0)
                        issueCount++;
                }
                var prefabItem = new MetadataTreeViewItem(itemId++, $"{prefabName} ({issueCount}个问题)", prefabResult);
                foreach (var adapterItem in prefabResult.Items)
                {
                    string displayName;
                    if (adapterItem.MissingLanguages.Count > 0)
                        displayName = adapterItem.GetDisplayName();
                    else
                        displayName = $"{adapterItem.GameObjectPath} (配置完整)";
                    var adapterTreeItem = new MetadataTreeViewItem(itemId++, displayName, adapterItem);
                    prefabItem.AddChild(adapterTreeItem);
                }
                root.AddChild(prefabItem);
            }
            root.AddChild(prefabItem);
        }
        // 【关键修复 2】:Unity 官方规范要求,手动使用 AddChild 构建树之后,必须调用此方法刷新深度和层级关系
        if (showWithoutIssue)
        {
            foreach (var prefabResult in m_Summary.PrefabResultsWithoutIssue)
            {
                string prefabName = Path.GetFileNameWithoutExtension(prefabResult.PrefabPath);
                var prefabItem = new MetadataTreeViewItem(itemId++, $"{prefabName} (无问题)", prefabResult);
                foreach (var adapterItem in prefabResult.Items)
                {
                    var displayName = $"{adapterItem.GameObjectPath} (配置完整)";
                    var adapterTreeItem = new MetadataTreeViewItem(itemId++, displayName, adapterItem);
                    prefabItem.AddChild(adapterTreeItem);
                }
                root.AddChild(prefabItem);
            }
        }
        SetupDepthsFromParentsAndChildren(root);
        return root;
@@ -117,7 +178,12 @@
        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)})");
        {
            if (adapterItem.MissingLanguages.Count > 0)
                GUI.Label(args.rowRect, $"{adapterItem.GameObjectPath} (缺少: {string.Join(", ", adapterItem.MissingLanguages)})");
            else
                GUI.Label(args.rowRect, item.displayName);
        }
        else
            base.RowGUI(args);
    }
@@ -173,8 +239,8 @@
    
    private ScanResultTreeView m_TreeView;
    private TreeViewState m_TreeViewState;
    private ScanResultFilterMode m_ResultFilterMode = ScanResultFilterMode.全部;
    // 批量操作的UI状态
    private int m_SourceLangIndex = 0;
    private int m_TargetLangIndex = 0;
    private bool m_OverwriteExisting = false;
@@ -279,7 +345,20 @@
            if (m_ScanResult != null)
            {
                EditorGUILayout.LabelField($"预制体总数: {m_ScanResult.TotalPrefabsScanned} | Adapter总数: {m_ScanResult.TotalAdaptersFound} | 缺失配置: {m_ScanResult.AdaptersWithMissingConfig}");
                EditorGUILayout.LabelField($"预制体总数: {m_ScanResult.TotalPrefabsScanned} | 有Adapter的预制体: {m_ScanResult.PrefabsWithIssueCount + m_ScanResult.PrefabsWithoutIssueCount} | Adapter总数: {m_ScanResult.TotalAdaptersFound} | 缺失配置: {m_ScanResult.AdaptersWithMissingConfig} | 有问题预制体: {m_ScanResult.PrefabsWithIssueCount} | 无问题预制体: {m_ScanResult.PrefabsWithoutIssueCount}");
                EditorGUILayout.Space(5f);
                using (new EditorGUILayout.HorizontalScope())
                {
                    EditorGUILayout.LabelField("显示模式:", GUILayout.Width(60f));
                    EditorGUI.BeginChangeCheck();
                    m_ResultFilterMode = (ScanResultFilterMode)EditorGUILayout.EnumPopup(m_ResultFilterMode);
                    if (EditorGUI.EndChangeCheck() && m_TreeView != null)
                    {
                        m_TreeViewState ??= new TreeViewState();
                        m_TreeView = new ScanResultTreeView(m_TreeViewState, m_ScanResult, m_ResultFilterMode);
                    }
                }
                EditorGUILayout.Space(5f);
                if (m_TreeView != null)
@@ -304,7 +383,6 @@
        }
    }
    // ======================== 新增:批量操作功能 ========================
    private void DrawBatchOperations()
    {
        using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
@@ -370,7 +448,6 @@
                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;
@@ -380,7 +457,6 @@
                    {
                        if (m_OverwriteExisting || !adapter.HasConfig(targetLang))
                        {
                            // 必须标记对象为脏,否则 Unity 不会把资产的修改存盘
                            Undo.RecordObject(adapter, "Batch Copy Language Config");
                            
                            var clonedConfig = adapter.GetConfig(sourceLang).Clone();
@@ -402,7 +478,6 @@
        finally
        {
            EditorUtility.ClearProgressBar();
            // 统一保存所有标记为 Dirty 的资产修改
            AssetDatabase.SaveAssets();
            
            PerformScan();
@@ -412,8 +487,6 @@
                "确认");
        }
    }
    // ======================== 核心扫描逻辑 ========================
    private void StartScan()
    {
@@ -460,7 +533,7 @@
        }
        m_TreeViewState ??= new TreeViewState();
        m_TreeView = new ScanResultTreeView(m_TreeViewState, m_ScanResult);
        m_TreeView = new ScanResultTreeView(m_TreeViewState, m_ScanResult, m_ResultFilterMode);
        m_ScanStatus = $"扫描完成! 发现 {m_ScanResult.AdaptersWithMissingConfig} 个缺失配置";
        Repaint();
    }
@@ -471,8 +544,12 @@
        if (prefab == null) return;
        var adapters = prefab.GetComponentsInChildren<TextLanguageAdapter>(true);
        if (adapters.Length == 0) return;
        m_ScanResult.TotalAdaptersFound += adapters.Length;
        bool hasIssue = false;
        List<ScanResultItem> allItems = new List<ScanResultItem>();
        foreach (var adapter in adapters)
        {
@@ -483,15 +560,30 @@
                if (!adapter.HasConfig(langId)) missing.Add(langId);
            }
            var item = new ScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetTextType, guid)
            {
                MissingLanguages = missing
            };
            allItems.Add(item);
            if (missing.Count > 0)
            {
                var item = new ScanResultItem(path, GetGameObjectPath(adapter.gameObject, prefab), adapter.TargetTextType, guid)
                {
                    MissingLanguages = missing
                };
                hasIssue = true;
            }
        }
        if (hasIssue)
        {
            foreach (var item in allItems)
            {
                m_ScanResult.AddResult(item);
            }
        }
        else
        {
            m_ScanResult.AddPrefabWithoutIssue(path, guid, allItems);
        }
    }
    private string GetGameObjectPath(GameObject go, GameObject root)