Assets/Editor/AssetBundleBrowser/AssetBundleBuildTab.cs
@@ -90,6 +90,12 @@ set { LocalSave.SetString("APKOutPutPath", value); } } public string AndroidExportPath { get { return LocalSave.GetString("AndroidExportPath"); } set { LocalSave.SetString("AndroidExportPath", value); } } string m_Publishers; public string publishers { @@ -230,6 +236,15 @@ var fromVersionConfig = ClientPackage.GetVersionConfig(_publisher, out versionName); var versionConfig = await VersionConfig.GetAsync(); if (fromVersionConfig == null || versionConfig == null) { Debug.LogWarningFormat("SetVersionConfig failed: fromVersionConfig={0}, versionConfig={1}, publisher={2}", fromVersionConfig != null ? "ok" : "null", versionConfig != null ? "ok" : "null", _publisher); return; } VersionConfig.Copy(fromVersionConfig, versionConfig); @@ -516,6 +531,32 @@ #endif GUILayout.EndHorizontal(); #if UNITY_ANDROID EditorGUILayout.Space(); GUILayout.Label("Export Android Project", EditorStyles.boldLabel); GUILayout.BeginHorizontal(); var newExportPath = EditorGUILayout.TextField("Export Path", AndroidExportPath); if (!string.IsNullOrEmpty(newExportPath) && newExportPath != AndroidExportPath) { AndroidExportPath = newExportPath; } if (GUILayout.Button("Browse", GUILayout.MaxWidth(75f))) { BrowseForAndroidExport(); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Export Android Project")) { EditorApplication.delayCall += ExecuteExportAndroidProject; } if (GUILayout.Button("Export Android Dev")) { EditorApplication.delayCall += ExecuteExportAndroidProjectDev; } GUILayout.EndHorizontal(); #endif // GUILayout.Space(20); // if (GUILayout.Button("SwitchVersionConfig")) @@ -966,6 +1007,30 @@ ClientPackage.BuildPublishers(ClientPackage.SDK_PLUGIN_PROJECT, outputPath, ApkOutputPath, publishers, packageIndex, true, false); } private void ExecuteExportAndroidProject() { if (string.IsNullOrEmpty(AndroidExportPath)) { EditorUtility.DisplayDialog("提示", "请先设置 Android Export Path", "确定"); return; } packageIndex++; var assetBundlePath = Application.dataPath.Replace("Assets", m_UserData.m_OutputPath); ClientPackage.ExportAndroidProject(publishers, packageIndex, false, AndroidExportPath, assetBundlePath); } private void ExecuteExportAndroidProjectDev() { if (string.IsNullOrEmpty(AndroidExportPath)) { EditorUtility.DisplayDialog("提示", "请先设置 Android Export Path", "确定"); return; } packageIndex++; var assetBundlePath = Application.dataPath.Replace("Assets", m_UserData.m_OutputPath); ClientPackage.ExportAndroidProject(publishers, packageIndex, true, AndroidExportPath, assetBundlePath); } private void ExecuteBuildClientPackageStandalone() { packageIndex++; @@ -1084,6 +1149,15 @@ } } private void BrowseForAndroidExport() { var newPath = EditorUtility.OpenFolderPanel("Android Export Folder", AndroidExportPath, string.Empty); if (!string.IsNullOrEmpty(newPath)) { AndroidExportPath = newPath; } } private void ResetPathToDefault() { m_UserData.m_UseDefaultPath = true; Assets/Editor/Logo/ryzj/SplashImage.pngAssets/Editor/Logo/test/SplashImage.pngAssets/Editor/Logo/wgyx/SplashImage.pngAssets/Editor/Logo/xssg/SplashImage.pngAssets/Editor/Tool/ClientPackage.cs
@@ -1,4 +1,4 @@ using System.Collections; using System.Collections; using System.Collections.Generic; using UnityEngine; using System; @@ -386,6 +386,286 @@ } public static VersionConfig versionConfig1 = null; /// <summary> /// 导出Android工程给AS打包 /// Unity只负责导出unityLibrary,SDK/Gradle/签名等均由AS工程处理 /// </summary> public static void ExportAndroidProject(string _publisher, int _buildIndex, bool _development, string _outputPath, string _assetBundlePath) { if (string.IsNullOrEmpty(_outputPath) || !Directory.Exists(_outputPath)) { Debug.LogError("导出路径无效,请设置有效的 Android Export Path: " + _outputPath); return; } PreBuild(_publisher, _buildIndex); var versionName = string.Empty; var versionConfig = GetVersionConfig(_publisher, out versionName); PlayerSettings.Android.bundleVersionCode = VersionConfig.GetVersionNumber(versionConfig.version); PlayerSettings.enableInternalProfiler = _development; // 先清理StreamingAssets,再根据assetAccess配置拷贝资源到StreamingAssets var streamingPath = ResourcesPath.Instance.StreamingAssetPath; if (Directory.Exists(streamingPath)) Directory.Delete(streamingPath, true); if (!string.IsNullOrEmpty(_assetBundlePath) && Directory.Exists(_assetBundlePath)) { switch (versionConfig.assetAccess) { case InstalledAsset.NullAsset: CopyNullAssetResources(_assetBundlePath, streamingPath); break; case InstalledAsset.HalfAsset: CopyHalfAssetResources(_assetBundlePath, streamingPath); break; case InstalledAsset.FullAsset: case InstalledAsset.IngoreDownLoad: CopyFullAssetResources(_assetBundlePath, streamingPath); break; } Debug.LogFormat("资源拷贝完成({0}),从 {1} 到 {2}", versionConfig.assetAccess, _assetBundlePath, streamingPath); } else { Debug.LogWarningFormat("AssetBundle路径为空或不存在: {0},导出工程将不包含游戏资源", _assetBundlePath); } // 关键设置:导出为Gradle工程而非直接打APK EditorUserBuildSettings.exportAsGoogleAndroidProject = true; EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle; EditorUserBuildSettings.development = _development; // 导出路径 _outputPath = Path.GetFullPath(_outputPath); string outputDir = Path.Combine(_outputPath, versionName + "_" + versionConfig.clientPackageFlag); if (Directory.Exists(outputDir)) { Directory.Delete(outputDir, true); } Debug.LogFormat("开始导出Android工程,输出路径: {0}", outputDir); BuildPipeline.BuildPlayer(baseLevels, outputDir, BuildTarget.Android, _development ? BuildOptions.AcceptExternalModificationsToPlayer | BuildOptions.Development | BuildOptions.AllowDebugging : BuildOptions.AcceptExternalModificationsToPlayer); Debug.LogFormat("导出Android工程完成!输出路径: {0}", outputDir); // 导出完成后恢复设置 EditorUserBuildSettings.exportAsGoogleAndroidProject = false; EditorUserBuildSettings.development = false; } /// <summary> /// 小包拷贝:仅拷贝builtin资源 /// </summary> static void CopyNullAssetResources(string _assetBundlePath, string _streamingPath) { var files = new List<FileInfo>(); var builtInFiles = new List<FileInfo>(); FileExtersion.GetAllDirectoryFileInfos(_assetBundlePath, files); foreach (var file in files) { if (file.FullName.Contains("builtin")) { builtInFiles.Add(file); } } foreach (var item in builtInFiles) { var extension = Path.GetExtension(item.FullName); if (extension == ".meta") continue; var relativePath = FileExtersion.GetFileRelativePath(_assetBundlePath, item.FullName); var to = StringUtility.Concat(_streamingPath, relativePath); var directory = Path.GetDirectoryName(to); if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); File.Copy(item.FullName, to, true); } } /// <summary> /// 半包拷贝:根据PriorBundleConfig剔除优先级低的hero/maps/audio/uieffect/video资源, /// 以及config和ui资源(取决于includeConfig/includeUI开关) /// </summary> static void CopyHalfAssetResources(string _assetBundlePath, string _streamingPath) { PriorBundleConfig.LazyInit(); var fromFiles = new List<FileInfo>(); FileExtersion.GetAllDirectoryFileInfos(_assetBundlePath, fromFiles); var excludeFileFullNames = new List<string>(); // 剔除优先级低的hero资源 var tempFiles = new List<FileInfo>(); FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/hero"), tempFiles); foreach (var file in tempFiles) { var fileName = Path.GetFileNameWithoutExtension(file.FullName); var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Mob, AssetVersionUtility.DecodeFileName(fileName)); if (prior > AssetPrior) { excludeFileFullNames.Add(file.FullName); } } // 剔除优先级低的maps资源 tempFiles.Clear(); FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/maps"), tempFiles); foreach (var file in tempFiles) { var fileName = Path.GetFileNameWithoutExtension(file.FullName); var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Scene, AssetVersionUtility.DecodeFileName(fileName)); if (prior > AssetPrior) { excludeFileFullNames.Add(file.FullName); } } // 剔除优先级低的audio资源 tempFiles.Clear(); FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/audio"), tempFiles); foreach (var file in tempFiles) { var fileName = Path.GetFileNameWithoutExtension(file.FullName); var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Audio, AssetVersionUtility.DecodeFileName(fileName)); if (prior > AssetPrior) { excludeFileFullNames.Add(file.FullName); } } // 剔除优先级低的uieffect资源 tempFiles.Clear(); FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/uieffect"), tempFiles); foreach (var file in tempFiles) { var fileName = Path.GetFileNameWithoutExtension(file.FullName); var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Effect, AssetVersionUtility.DecodeFileName(fileName)); if (prior > AssetPrior) { excludeFileFullNames.Add(file.FullName); } } // 剔除视频资源 tempFiles.Clear(); var videoPath = StringUtility.Concat(_assetBundlePath, "/video"); if (Directory.Exists(videoPath)) { FileExtersion.GetAllDirectoryFileInfos(videoPath, tempFiles); foreach (var file in tempFiles) { var fileName = Path.GetFileNameWithoutExtension(file.FullName); var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.Video, AssetVersionUtility.DecodeFileName(fileName)); if (prior > AssetPrior) { excludeFileFullNames.Add(file.FullName); } } } // 剔除表资源 if (!includeConfig) { tempFiles.Clear(); FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/config"), tempFiles); foreach (var file in tempFiles) { excludeFileFullNames.Add(file.FullName); } } // 剔除UI资源 if (!includeUI) { tempFiles.Clear(); FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/ui"), tempFiles); foreach (var file in tempFiles) { excludeFileFullNames.Add(file.FullName); } } else { tempFiles.Clear(); FileExtersion.GetAllDirectoryFileInfos(StringUtility.Concat(_assetBundlePath, "/ui"), tempFiles); foreach (var file in tempFiles) { var fileName = Path.GetFileNameWithoutExtension(file.FullName); var prior = PriorBundleConfig.GetAssetPrior(AssetVersion.AssetCategory.UI, fileName); if (prior > AssetPrior) { excludeFileFullNames.Add(file.FullName); } } } // 从文件列表中移除需要排除的文件 for (int i = fromFiles.Count - 1; i >= 0; i--) { if (excludeFileFullNames.Contains(fromFiles[i].FullName)) { fromFiles.RemoveAt(i); } } foreach (var item in fromFiles) { var extension = Path.GetExtension(item.FullName); if (extension == ".meta") continue; var relativePath = FileExtersion.GetFileRelativePath(_assetBundlePath, item.FullName); if (relativePath.StartsWith("patch")) continue; var to = StringUtility.Concat(_streamingPath, relativePath); var directory = Path.GetDirectoryName(to); if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); File.Copy(item.FullName, to, true); } } /// <summary> /// 全包拷贝:拷贝所有资源(排除patch和.meta) /// </summary> static void CopyFullAssetResources(string _assetBundlePath, string _streamingPath) { var files = new List<FileInfo>(); FileExtersion.GetAllDirectoryFileInfos(_assetBundlePath, files); foreach (var file in files) { var extension = Path.GetExtension(file.FullName); if (extension == ".meta") continue; var relativePath = FileExtersion.GetFileRelativePath(_assetBundlePath, file.FullName); if (relativePath.StartsWith("patch")) continue; var to = StringUtility.Concat(_streamingPath, relativePath); var directory = Path.GetDirectoryName(to); if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); File.Copy(file.FullName, to, true); } } public static void BuildIpa(string _sdkPath, string _publisher, int _buildIndex, bool _replace) { PreBuild(_publisher, _buildIndex); Assets/Editor/UIComponent/ImageLanguageAdapterEditor.cs
New file @@ -0,0 +1,217 @@ using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.UI; [CustomEditor(typeof(ImageLanguageAdapter), true)] [CanEditMultipleObjects] public class ImageLanguageAdapterEditor : Editor { private Dictionary<string, bool> m_FoldoutStates = new Dictionary<string, bool>(); private void OnEnable() { ImageLanguageAdapterHelper.Initialize(); if (target is ImageLanguageAdapter adapter) { if (!adapter.HasConfig(ImageLanguageAdapter.DefaultLangId)) { adapter.ReadCurrentToConfig(ImageLanguageAdapter.DefaultLangId); EditorUtility.SetDirty(adapter); } foreach (var langId in adapter.GetConfiguredLanguages()) m_FoldoutStates.TryAdd(langId, false); } } public override void OnInspectorGUI() { var adapter = target as ImageLanguageAdapter; DrawBasicInfo(adapter); EditorGUILayout.Space(); DrawLanguageConfigs(adapter); if (GUI.changed) EditorUtility.SetDirty(target); } private void DrawBasicInfo(ImageLanguageAdapter adapter) { using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { EditorGUILayout.LabelField("基本信息", EditorStyles.boldLabel); EditorGUI.BeginChangeCheck(); Component newTarget = (Component)EditorGUILayout.ObjectField("目标组件", adapter.TargetImageComponent, typeof(Component), true); ImageComponentType newType = (ImageComponentType)EditorGUILayout.EnumPopup("组件类型", adapter.TargetImageType); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(adapter, "Update Basic Info"); if (newTarget != null && newTarget != adapter.TargetImageComponent) { string typeName = newTarget.GetType().Name; if (typeName == "ImageEx") newType = ImageComponentType.ImageEx; else if (newTarget is Image) newType = ImageComponentType.Image; } adapter.TargetImageComponent = newTarget; adapter.TargetImageType = newType; } EditorGUILayout.HelpBox("请手动拖拽要适配的图片组件,并确认组件类型是否正确。", MessageType.Info); } } private void DrawLanguageConfigs(ImageLanguageAdapter adapter) { using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { EditorGUILayout.LabelField("语言排版配置", EditorStyles.boldLabel); EditorGUILayout.HelpBox("default仅用于编辑时恢复初始状态,无对应语言配置时用组件当前状态", MessageType.Info); foreach (var langId in adapter.GetConfiguredLanguages()) DrawLanguageConfigItem(adapter, langId); EditorGUILayout.Space(); DrawAddLanguageConfig(adapter); } } private void DrawLanguageConfigItem(ImageLanguageAdapter adapter, string langId) { m_FoldoutStates.TryAdd(langId, false); int presetIndex = System.Array.IndexOf(ImageLanguageAdapterHelper.PresetLanguageIds, langId); string name = presetIndex >= 0 ? ImageLanguageAdapterHelper.PresetLanguageNames[presetIndex] : ImageLanguageAdapter.GetLanguageShowName(langId); string label = string.IsNullOrEmpty(name) ? langId : $"{langId} ({name})"; EditorGUI.indentLevel++; if (langId == ImageLanguageAdapter.DefaultLangId) GUI.backgroundColor = new Color(1f, 0.9f, 0.7f); m_FoldoutStates[langId] = EditorGUILayout.Foldout(m_FoldoutStates[langId], label, true); GUI.backgroundColor = Color.white; if (m_FoldoutStates[langId]) { EditorGUI.indentLevel++; var config = adapter.LanguageConfigs.Get(langId); if (config != null) { DrawConfigItem(config); EditorGUILayout.Space(); DrawConfigItemActions(adapter, langId); } EditorGUI.indentLevel--; } EditorGUI.indentLevel--; } private void DrawConfigItem(ImageLanguageConfigItem cfg) { EditorGUILayout.LabelField("RectTransform 配置", EditorStyles.miniBoldLabel); EditorGUI.BeginChangeCheck(); cfg.anchoredPosition = EditorGUILayout.Vector2Field("Anchored Position", cfg.anchoredPosition); cfg.sizeDelta = EditorGUILayout.Vector2Field("Size Delta", cfg.sizeDelta); cfg.anchorMin = EditorGUILayout.Vector2Field("Anchor Min", cfg.anchorMin); cfg.anchorMax = EditorGUILayout.Vector2Field("Anchor Max", cfg.anchorMax); cfg.pivot = EditorGUILayout.Vector2Field("Pivot", cfg.pivot); cfg.localScale = EditorGUILayout.Vector3Field("Local Scale", cfg.localScale); cfg.localRotation = EditorGUILayout.Vector3Field("Local Rotation", cfg.localRotation); EditorGUILayout.Space(); EditorGUILayout.LabelField("Image 配置", EditorStyles.miniBoldLabel); cfg.enabled = EditorGUILayout.Toggle("显示", cfg.enabled); cfg.color = EditorGUILayout.ColorField("Color", cfg.color); cfg.type = (Image.Type)EditorGUILayout.EnumPopup("Type", cfg.type); if (cfg.type == Image.Type.Sliced || cfg.type == Image.Type.Tiled) { cfg.fillCenter = EditorGUILayout.Toggle("Fill Center", cfg.fillCenter); } if (cfg.type == Image.Type.Filled) { cfg.fillMethod = (Image.FillMethod)EditorGUILayout.EnumPopup("Fill Method", cfg.fillMethod); cfg.fillAmount = EditorGUILayout.Slider("Fill Amount", cfg.fillAmount, 0f, 1f); cfg.fillOrigin = EditorGUILayout.IntField("Fill Origin", cfg.fillOrigin); } cfg.preserveAspect = EditorGUILayout.Toggle("Preserve Aspect", cfg.preserveAspect); cfg.useSpriteMesh = EditorGUILayout.Toggle("Use Sprite Mesh", cfg.useSpriteMesh); cfg.pixelsPerUnitMultiplier = EditorGUILayout.FloatField("Pixels Per Unit Multiplier", cfg.pixelsPerUnitMultiplier); if (EditorGUI.EndChangeCheck()) EditorUtility.SetDirty(target); } private void DrawConfigItemActions(ImageLanguageAdapter adapter, string langId) { using (new EditorGUILayout.HorizontalScope()) { GUILayout.Space(EditorGUI.indentLevel * 15f); if (GUILayout.Button("从当前读取", GUILayout.Width(80))) { Undo.RecordObject(adapter, "Read Current Config"); adapter.ReadCurrentToConfig(langId); } if (GUILayout.Button("应用", GUILayout.Width(60))) { Undo.RecordObject(adapter, "Apply Config"); adapter.ApplyConfig(langId); } using (new EditorGUI.DisabledScope(langId == ImageLanguageAdapter.DefaultLangId)) { if (GUILayout.Button("删除", GUILayout.Width(50))) { Undo.RecordObject(adapter, "Remove Config"); adapter.RemoveConfig(langId); m_FoldoutStates.Remove(langId); } } } } private void DrawAddLanguageConfig(ImageLanguageAdapter adapter) { EditorGUILayout.BeginHorizontal(); GUILayout.Space(15f); EditorGUILayout.LabelField("添加预设语言:", GUILayout.Width(100)); float viewWidth = EditorGUIUtility.currentViewWidth - 30f; float currentWidth = 115f; float buttonWidth = 64f; for (int i = 0; i < ImageLanguageAdapterHelper.PresetLanguageIds.Length; i++) { string langId = ImageLanguageAdapterHelper.PresetLanguageIds[i]; if (!adapter.HasConfig(langId)) { if (currentWidth + buttonWidth > viewWidth) { EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUILayout.Space(115f); currentWidth = 115f; } if (GUILayout.Button(ImageLanguageAdapterHelper.PresetLanguageNames[i], GUILayout.Width(60))) { Undo.RecordObject(adapter, "Add Language Config"); var newConfig = adapter.HasConfig(ImageLanguageAdapter.DefaultLangId) ? adapter.LanguageConfigs.Get(ImageLanguageAdapter.DefaultLangId).Clone() : new ImageLanguageConfigItem(); adapter.SetConfig(langId, newConfig); m_FoldoutStates[langId] = true; } currentWidth += buttonWidth; } } EditorGUILayout.EndHorizontal(); } } Assets/Editor/UIComponent/ImageLanguageAdapterEditor.cs.meta
New file @@ -0,0 +1,11 @@ fileFormatVersion: 2 guid: 6f2905447a660d641abe91b18a29680a MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: Assets/Editor/UIComponent/ImageLanguageAdapterHelper.cs
New file @@ -0,0 +1,67 @@ using System.Collections.Generic; using System.IO; using LitJson; using UnityEngine; /// <summary> /// 编辑器图片语言配置读取助手 /// 负责统一解析 InitialFunction.txt /// </summary> public static class ImageLanguageAdapterHelper { public static string[] PresetLanguageIds { get; private set; } public static string[] PresetLanguageNames { get; private set; } private static bool s_Initialized; public static void Initialize() { if (s_Initialized) return; var idList = new List<string> { ImageLanguageAdapter.DefaultLangId }; var nameList = new List<string> { "默认" }; string configPath = Path.Combine(Application.dataPath, "ResourcesOut/Config/InitialFunction.txt"); if (File.Exists(configPath)) { try { string[] lines = File.ReadAllLines(configPath); for (int i = 3; i < lines.Length; i++) { string line = lines[i]; if (string.IsNullOrWhiteSpace(line)) continue; int index = line.IndexOf('\t'); if (index == -1) continue; string key = line.Substring(0, index); if (key == "LanguageEx") { string[] fields = line.Split('\t'); if (fields.Length > 1 && !string.IsNullOrEmpty(fields[1])) { var dict = JsonMapper.ToObject<Dictionary<string, string>>(fields[1]); if (dict != null) { foreach (var kvp in dict) { idList.Add(kvp.Key); nameList.Add(kvp.Value); } } } break; } } } catch (System.Exception ex) { Debug.LogWarning($"[ImageLanguageAdapterHelper] 读取配置失败: {ex.Message}"); } } PresetLanguageIds = idList.ToArray(); PresetLanguageNames = nameList.ToArray(); s_Initialized = true; } } Assets/Editor/UIComponent/ImageLanguageAdapterHelper.cs.meta
New file @@ -0,0 +1,11 @@ fileFormatVersion: 2 guid: dd266766e23062e4eb668f7102620a06 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: Assets/Editor/UIComponent/ImageLanguageAdapterScanTool.cs
New file @@ -0,0 +1,594 @@ using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine; public class ImageScanResultItem { public string PrefabPath { get; } public string GameObjectPath { get; } public List<string> MissingLanguages { get; set; } = new List<string>(); public ImageComponentType ComponentType { get; } public string PrefabGUID { get; } public ImageScanResultItem(string path, string goPath, ImageComponentType type, string guid) { PrefabPath = path; GameObjectPath = goPath; ComponentType = type; PrefabGUID = guid; } public string GetDisplayName() => $"{GameObjectPath} (缺少: {string.Join(", ", MissingLanguages)}) [{ComponentType}]"; } public class ImagePrefabScanResult { public string PrefabPath { get; } public string PrefabGUID { get; } public List<ImageScanResultItem> Items { get; } = new List<ImageScanResultItem>(); public ImagePrefabScanResult(string path, string guid) { PrefabPath = path; PrefabGUID = guid; } } public class ImageScanResultSummary { public string ScanDirectory { get; } 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; public void AddResult(ImageScanResultItem item) { var prefabResult = PrefabResults.Find(p => p.PrefabPath == item.PrefabPath); if (prefabResult == null) { prefabResult = new ImagePrefabScanResult(item.PrefabPath, item.PrefabGUID); PrefabResults.Add(prefabResult); PrefabsWithIssueCount++; } prefabResult.Items.Add(item); 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++; } } public enum ImageScanResultFilterMode { 全部, 仅显示有问题, 仅显示无问题 } public class ImageMetadataTreeViewItem : TreeViewItem { public object Metadata { get; } public ImageMetadataTreeViewItem(int id, string name, object meta) : base(id, 0, name) => Metadata = meta; } public class ImageScanResultTreeView : TreeView { private ImageScanResultSummary m_Summary; private ImageScanResultFilterMode m_FilterMode; public ImageScanResultTreeView(TreeViewState state, ImageScanResultSummary summary, ImageScanResultFilterMode filterMode) : base(state) { m_Summary = summary; m_FilterMode = filterMode; Reload(); ExpandAll(); } protected override TreeViewItem BuildRoot() { var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" }; root.children = new List<TreeViewItem>(); if (m_Summary == null) return root; int itemId = 1; 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) { 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); } } 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); return root; } protected override void RowGUI(RowGUIArgs args) { var item = args.item as ImageMetadataTreeViewItem; if (item != null && item.Metadata is ImagePrefabScanResult) GUI.Label(args.rowRect, item.displayName, EditorStyles.boldLabel); else if (item != null && item.Metadata is ImageScanResultItem adapterItem) { 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); } protected override void DoubleClickedItem(int id) { var item = FindItem(id, rootItem) as ImageMetadataTreeViewItem; if (item == null) return; if (item.Metadata is ImageScanResultItem adapterItem) PingGameObject(adapterItem.PrefabPath, adapterItem.GameObjectPath); else if (item.Metadata is ImagePrefabScanResult 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($"[ImageScanTool] 找不到路径 '{gameObjectPath}',已选中整个预制体"); } } } public class ImageLanguageAdapterScanTool : EditorWindow { private string m_ScanDirectory = "Assets"; private Vector2 m_ScrollPosition; private ImageScanResultSummary m_ScanResult; private bool m_IsScanning; private float m_ScanProgress; private string m_ScanStatus; private ImageScanResultTreeView m_TreeView; private TreeViewState m_TreeViewState; private ImageScanResultFilterMode m_ResultFilterMode = ImageScanResultFilterMode.全部; private int m_SourceLangIndex = 0; private int m_TargetLangIndex = 0; private bool m_OverwriteExisting = false; [MenuItem("程序/ImageLanguageAdapter扫描与管理工具")] public static void ShowWindow() { var window = GetWindow<ImageLanguageAdapterScanTool>("图片语言适配器扫描"); window.minSize = new Vector2(600f, 500f); } private void OnEnable() => ImageLanguageAdapterHelper.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("ImageLanguageAdapter 配置缺失扫描与批量操作工具", 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 ImageLanguageAdapterHelper.PresetLanguageIds) { if (langId == ImageLanguageAdapter.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.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) { 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("批量操作 (针对目标目录下的所有预制体上的ImageLanguageAdapter组件)", EditorStyles.boldLabel); if (ImageLanguageAdapterHelper.PresetLanguageIds == null || ImageLanguageAdapterHelper.PresetLanguageIds.Length == 0) return; using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("旧语言:", GUILayout.Width(60f)); m_SourceLangIndex = EditorGUILayout.Popup(m_SourceLangIndex, ImageLanguageAdapterHelper.PresetLanguageNames); GUILayout.Space(20f); EditorGUILayout.LabelField("新语言:", GUILayout.Width(60f)); m_TargetLangIndex = EditorGUILayout.Popup(m_TargetLangIndex, ImageLanguageAdapterHelper.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 = ImageLanguageAdapterHelper.PresetLanguageIds[m_SourceLangIndex]; string targetLang = ImageLanguageAdapterHelper.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); GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(path); bool isModified = false; foreach (var adapter in prefabAsset.GetComponentsInChildren<ImageLanguageAdapter>(true)) { if (adapter.HasConfig(sourceLang)) { if (m_OverwriteExisting || !adapter.HasConfig(targetLang)) { 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(); 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 ImageScanResultSummary(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 ImageScanResultTreeView(m_TreeViewState, m_ScanResult, m_ResultFilterMode); m_ScanStatus = $"扫描完成! 发现 {m_ScanResult.AdaptersWithMissingConfig} 个缺失配置"; Repaint(); } private void ScanPrefab(string path, string guid) { GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path); 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) { List<string> missing = new List<string>(); foreach (var langId in ImageLanguageAdapterHelper.PresetLanguageIds) { if (langId == ImageLanguageAdapter.DefaultLangId) continue; 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) { 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) { 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); } } Assets/Editor/UIComponent/ImageLanguageAdapterScanTool.cs.meta
New file @@ -0,0 +1,11 @@ fileFormatVersion: 2 guid: 7d753da3934048046828299f1ee02998 MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} userData: assetBundleName: assetBundleVariant: Assets/Editor/UIComponent/TextLanguageAdapterEditor.cs
@@ -40,14 +40,30 @@ { EditorGUILayout.LabelField("基本信息", EditorStyles.boldLabel); using (new EditorGUI.DisabledScope(true)) EditorGUI.BeginChangeCheck(); // 允许手动拖拽目标组件和设置类型 Component newTarget = (Component)EditorGUILayout.ObjectField("目标组件", adapter.TargetTextComponent, typeof(Component), true); TextComponentType newType = (TextComponentType)EditorGUILayout.EnumPopup("组件类型", adapter.TargetTextType); if (EditorGUI.EndChangeCheck()) { EditorGUILayout.ObjectField("目标组件", adapter.LanguageConfigs.keys.Count > 0 ? adapter.TargetTextComponent : null, typeof(Component), true); EditorGUILayout.EnumPopup("组件类型", adapter.TargetTextType); Undo.RecordObject(adapter, "Update Basic Info"); // 智能辅助:当用户拖拽新组件时,尝试自动匹配一下类型,但也允许手动覆盖 if (newTarget != null && newTarget != adapter.TargetTextComponent) { string typeName = newTarget.GetType().Name; if (typeName == "GradientText") newType = TextComponentType.GradientText; else if (typeName == "TextEx") newType = TextComponentType.TextEx; else if (newTarget is UnityEngine.UI.Text) newType = TextComponentType.Text; } adapter.TargetTextComponent = newTarget; adapter.TargetTextType = newType; } if (GUILayout.Button("刷新组件检测", GUILayout.Width(120))) adapter.Editor_ForceRefreshDetection(); EditorGUILayout.HelpBox("组件或类型变更后需执行刷新组件检测", MessageType.Info); EditorGUILayout.HelpBox("请手动拖拽要适配的文本组件,并确认组件类型是否正确。", MessageType.Info); } } 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)