using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.Linq; using System.IO; using ArtResource; using Spine.Unity; using UnityEngine.U2D; using Thrift; using Thrift.Protocol; using Thrift.Transport; using Thrift.Transport.Client; namespace EditorArt_Tools { /// /// 美术资源配置编辑器 /// 提供三栏布局:配置表预览、Item导航、详细编辑 /// public class ArtResourceConfigEditor : EditorWindow { private const string SO_ROOT_PATH = "Assets/Art_SubModule/Art_SO"; private const string JSON_ROOT_PATH = "Assets/Art_SubModule/Art_Json"; private const string BYTES_ROOT_PATH = "Assets/Art_SubModule/Art_Bytes"; private const string FOLDER_FOLDOUT_PREFS_KEY_PREFIX = "ArtResourceConfigEditor.FolderFoldout."; // ===== 数据 ===== private List allTables = new List(); private ArtTableSO selectedTable; private ArtItemData selectedItem; private Dictionary folderFoldouts = new Dictionary(); private Dictionary itemFoldouts = new Dictionary(); // 暂存区(用于编辑但未保存的数据)- 完全独立的数据副本 private List tempItemsList = new List(); private new bool hasUnsavedChanges = false; // ID编辑临时缓存 private Dictionary editingIdStrings = new Dictionary(); private Dictionary originalIds = new Dictionary(); // 用于第二栏跳转到第三栏 private int scrollToItemId = -1; // 当前正在播放Spine动画的Item ID private int currentPlayingSpineItemId = -1; // ===== UI滚动 ===== private Vector2 scrollTableList; private Vector2 scrollItemNav; private Vector2 scrollEditArea; // ===== Spine预览 ===== private object spinePreviewInstance; private System.Type spinePreviewType; // ===== Sprite预览缓存 ===== private Dictionary spriteAtlasCache = new Dictionary(); private struct SpriteAtlasInfo { public SpriteAtlas atlas; public string atlasPath; } [MenuItem("美术工具/美术资源配置")] public static void ShowWindow() { var window = GetWindow("美术资源配置"); window.minSize = new Vector2(1600, 800); window.Show(); } [MenuItem("美术工具/路径管理/修复丢失的引用")] public static void FixMissingReferences() { FixAllMissingReferences(); } [MenuItem("美术工具/路径管理/清理SO丢失文件")] public static void CleanupMissingItems() { CleanupAllMissingItems(); } private void OnEnable() { LoadFolderFoldoutStates(); RefreshTableList(); InitializeSpinePreview(); AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; } private void OnDisable() { CleanupSpinePreview(); AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; // 检查是否有未保存的更改 if (hasUnsavedChanges) { if (EditorUtility.DisplayDialog("未保存的更改", "您有未保存的更改,是否保存?", "保存", "放弃")) { SaveCurrentTable(); } } } private void OnDestroy() { CleanupSpinePreview(); } #region Spine预览初始化和清理 private void InitializeSpinePreview() { try { var assembly = typeof(Spine.Unity.Editor.SkeletonDataAssetInspector).Assembly; spinePreviewType = assembly.GetType("Spine.Unity.Editor.SkeletonInspectorPreview"); if (spinePreviewType != null) { spinePreviewInstance = System.Activator.CreateInstance(spinePreviewType); EditorApplication.update -= HandleSpinePreviewUpdate; EditorApplication.update += HandleSpinePreviewUpdate; } } catch (System.Exception e) { Debug.LogWarning($"初始化Spine预览失败: {e.Message}"); } } private void CleanupSpinePreview() { EditorApplication.update -= HandleSpinePreviewUpdate; if (spinePreviewInstance != null && spinePreviewType != null) { try { var clearMethod = spinePreviewType.GetMethod("Clear"); if (clearMethod != null) { clearMethod.Invoke(spinePreviewInstance, null); } var cleanupMethod = spinePreviewType.GetMethod("Cleanup"); if (cleanupMethod != null) { cleanupMethod.Invoke(spinePreviewInstance, null); } } catch (System.Exception e) { Debug.LogWarning($"清理Spine预览时出错: {e.Message}"); } spinePreviewInstance = null; } } private void HandleSpinePreviewUpdate() { if (spinePreviewInstance != null && spinePreviewType != null) { var updateMethod = spinePreviewType.GetMethod("HandleEditorUpdate"); if (updateMethod != null) { updateMethod.Invoke(spinePreviewInstance, null); } } } private void OnBeforeAssemblyReload() { CleanupSpinePreview(); } #endregion #region 主界面绘制 private void OnGUI() { DrawToolbar(); EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal(); // 第一栏:配置表预览(分级显示) DrawTableListPanel(); // 第二栏:Item导航 DrawItemNavigatorPanel(); // 第三栏:详细编辑 DrawEditPanel(); EditorGUILayout.EndHorizontal(); } private void DrawToolbar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(60))) { RefreshTableList(); } if (GUILayout.Button("新建配置表", EditorStyles.toolbarButton, GUILayout.Width(100))) { CreateNewTable(); } GUILayout.FlexibleSpace(); // 未保存提示 if (hasUnsavedChanges) { GUI.color = Color.yellow; GUILayout.Label("● 有未保存的更改", EditorStyles.boldLabel); GUI.color = Color.white; } if (selectedTable != null) { EditorGUILayout.LabelField($"当前表: {selectedTable.TableName}", EditorStyles.boldLabel); if (GUILayout.Button("保存", EditorStyles.toolbarButton, GUILayout.Width(60))) { SaveCurrentTable(); } GUI.backgroundColor = Color.red; if (GUILayout.Button("删除配置表", EditorStyles.toolbarButton, GUILayout.Width(80))) { DeleteCurrentTable(); } GUI.backgroundColor = Color.white; } EditorGUILayout.EndHorizontal(); } #endregion #region 第一栏:配置表预览(分级显示) private void DrawTableListPanel() { EditorGUILayout.BeginVertical("box", GUILayout.Width(320)); EditorGUILayout.LabelField("配置表列表", EditorStyles.boldLabel); scrollTableList = EditorGUILayout.BeginScrollView(scrollTableList); if (allTables != null && allTables.Count > 0) { DrawFolderHierarchy(); } else { EditorGUILayout.HelpBox("暂无配置表\n点击上方「新建配置表」创建", MessageType.Info); } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } private void DrawFolderHierarchy() { // 按文件夹分组 var tablesByFolder = allTables .Select(table => new { Table = table, Path = AssetDatabase.GetAssetPath(table), Folder = GetRelativeFolder(AssetDatabase.GetAssetPath(table)) }) .GroupBy(x => x.Folder) .OrderBy(g => g.Key); foreach (var folderGroup in tablesByFolder) { string folderName = folderGroup.Key; if (!folderFoldouts.ContainsKey(folderName)) { folderFoldouts[folderName] = GetFolderFoldoutState(folderName); } // 绘制文件夹 EditorGUILayout.BeginHorizontal(); bool previousFoldoutState = folderFoldouts[folderName]; bool newFoldoutState = EditorGUILayout.Foldout( previousFoldoutState, $"📁 {folderName} ({folderGroup.Count()})", true, EditorStyles.foldoutHeader); if (newFoldoutState != previousFoldoutState) { folderFoldouts[folderName] = newFoldoutState; SaveFolderFoldoutState(folderName, newFoldoutState); } EditorGUILayout.EndHorizontal(); // 绘制该文件夹下的配置表 if (folderFoldouts[folderName]) { EditorGUI.indentLevel++; foreach (var item in folderGroup) { DrawTableButton(item.Table); } EditorGUI.indentLevel--; } EditorGUILayout.Space(3); } } private void DrawTableButton(ArtTableSO table) { bool isSelected = table == selectedTable; Color originalBg = GUI.backgroundColor; if (isSelected) { GUI.backgroundColor = new Color(0.3f, 0.6f, 1f); } EditorGUILayout.BeginVertical("box"); if (GUILayout.Button($"📄 {table.TableName}", GUILayout.Height(30))) { SelectTable(table); } GUI.backgroundColor = originalBg; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"ID: {table.TableId}", EditorStyles.miniLabel, GUILayout.Width(80)); EditorGUILayout.LabelField($"资源数: {table.Items.Count}", EditorStyles.miniLabel); EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } private string GetRelativeFolder(string assetPath) { string relativePath = assetPath.Replace(SO_ROOT_PATH + "/", ""); int lastSlash = relativePath.LastIndexOf('/'); if (lastSlash >= 0) { return relativePath.Substring(0, lastSlash); } return "根目录"; } private void LoadFolderFoldoutStates() { folderFoldouts.Clear(); } private bool GetFolderFoldoutState(string folderName) { return EditorPrefs.GetBool(GetFolderFoldoutPrefsKey(folderName), false); } private void SaveFolderFoldoutState(string folderName, bool isExpanded) { EditorPrefs.SetBool(GetFolderFoldoutPrefsKey(folderName), isExpanded); } private string GetFolderFoldoutPrefsKey(string folderName) { return FOLDER_FOLDOUT_PREFS_KEY_PREFIX + folderName; } #endregion #region 第二栏:Item导航 private void DrawItemNavigatorPanel() { EditorGUILayout.BeginVertical("box", GUILayout.Width(280)); if (selectedTable == null) { EditorGUILayout.HelpBox("请从左侧选择一个配置表", MessageType.Info); EditorGUILayout.EndVertical(); return; } EditorGUILayout.LabelField($"{selectedTable.TableName} - 资源项", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("+ 添加新项", GUILayout.Height(25))) { AddNewItem(); } if (GUILayout.Button("📁 按文件夹导入", GUILayout.Height(25))) { BatchImportSprites(); } if (GUILayout.Button("🖼 批量导入图片", GUILayout.Height(25))) { BatchImportSpritesByFiles(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); scrollItemNav = EditorGUILayout.BeginScrollView(scrollItemNav); if (tempItemsList.Count == 0) { EditorGUILayout.HelpBox("暂无资源项\n点击上方「+ 添加新项」创建", MessageType.Info); } else { for (int i = 0; i < tempItemsList.Count; i++) { var item = tempItemsList[i]; DrawItemNavigatorButton(item, i); } } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } private void DrawItemNavigatorButton(ArtItemData item, int index) { bool isSelected = item == selectedItem; Color originalBg = GUI.backgroundColor; if (isSelected) { GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f); } EditorGUILayout.BeginVertical("box"); EditorGUILayout.BeginHorizontal(); // 小缩略图 if (item.Sprite != null) { Rect previewRect = GUILayoutUtility.GetRect(40, 40, GUILayout.ExpandWidth(false)); EditorGUI.DrawRect(previewRect, new Color(0.2f, 0.2f, 0.2f)); DrawSpritePreview(previewRect, item.Sprite); } else if (item.SpineAsset != null) { Rect previewRect = GUILayoutUtility.GetRect(40, 40, GUILayout.ExpandWidth(false)); EditorGUI.DrawRect(previewRect, new Color(0.2f, 0.2f, 0.2f)); GUI.Label(previewRect, "🦴", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 24 }); } else { GUILayout.Space(40); } EditorGUILayout.BeginVertical(); if (GUILayout.Button($"{index + 1}. {item.Name}", GUILayout.Height(40))) { SelectItem(item); } EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); // 显示基本信息 EditorGUILayout.LabelField($"ID: {item.Id}", EditorStyles.miniLabel); if (!string.IsNullOrEmpty(item.Desc)) { EditorGUILayout.LabelField($"描述: {item.Desc}", EditorStyles.miniLabel); } EditorGUILayout.EndVertical(); GUI.backgroundColor = originalBg; } #endregion #region 第三栏:详细编辑(显示所有Item) private void DrawEditPanel() { EditorGUILayout.BeginVertical("box", GUILayout.ExpandWidth(true)); if (selectedTable == null) { EditorGUILayout.HelpBox("请从左侧选择一个配置表", MessageType.Info); EditorGUILayout.EndVertical(); return; } // 表格信息编辑区域 EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("配置表信息", EditorStyles.boldLabel); EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("表格ID", GUILayout.Width(80)); selectedTable.TableId = EditorGUILayout.IntField(selectedTable.TableId); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("表格名称", GUILayout.Width(80)); selectedTable.TableName = EditorGUILayout.TextField(selectedTable.TableName); EditorGUILayout.EndHorizontal(); if (EditorGUI.EndChangeCheck()) { MarkAsChanged(); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(5); scrollEditArea = EditorGUILayout.BeginScrollView(scrollEditArea); if (tempItemsList.Count == 0) { EditorGUILayout.HelpBox("暂无资源项\n点击中间栏上方「+ 添加新项」创建", MessageType.Info); } else { // 显示所有Item for (int i = 0; i < tempItemsList.Count; i++) { var item = tempItemsList[i]; // 如果需要跳转到这个Item if (scrollToItemId == item.Id) { // 展开这个Item itemFoldouts[item.Id] = true; // 计算滚动位置:前面的item都是折叠状态,高度约60像素 float collapsedItemHeight = 44f; // 折叠状态的实际高度(只有标题行) scrollEditArea.y = Mathf.Max(0, i * collapsedItemHeight); // 重置跳转标记 scrollToItemId = -1; // 让GUI重绘以应用折叠状态 Repaint(); } DrawItemEditSection(item, i); EditorGUILayout.Space(10); } } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } private void DrawItemEditSection(ArtItemData item, int index) { // 整个Item的大面板 bool isSelected = item == selectedItem; Color originalBg = GUI.backgroundColor; if (isSelected) { GUI.backgroundColor = new Color(0.4f, 0.7f, 1f, 0.3f); } EditorGUILayout.BeginVertical("box"); GUI.backgroundColor = originalBg; // 标题行 EditorGUILayout.BeginHorizontal(); string title = $"{index + 1}. {item.Name} (ID: {item.Id})"; if (!itemFoldouts.ContainsKey(item.Id)) { itemFoldouts[item.Id] = false; } itemFoldouts[item.Id] = EditorGUILayout.Foldout( itemFoldouts[item.Id], title, true, EditorStyles.foldoutHeader); GUILayout.FlexibleSpace(); GUI.backgroundColor = Color.red; if (GUILayout.Button("删除", GUILayout.Width(60), GUILayout.Height(20))) { if (EditorUtility.DisplayDialog("确认删除", $"确定要删除资源项「{item.Name}」吗?\n此操作不可恢复!", "删除", "取消")) { DeleteItem(item); } } GUI.backgroundColor = Color.white; EditorGUILayout.EndHorizontal(); // 展开内容 if (itemFoldouts[item.Id]) { EditorGUI.indentLevel++; // 基本信息 DrawBasicInfoSection(item); EditorGUILayout.Space(5); // Sprite资源 DrawSpriteSection(item); EditorGUILayout.Space(5); // Spine资源 DrawSpineSection(item); EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); } private void DrawBasicInfoSection(ArtItemData editingData) { EditorGUILayout.BeginVertical("box"); EditorGUI.BeginChangeCheck(); EditorGUILayout.LabelField("资源ID", EditorStyles.boldLabel); // 初始化编辑字符串 int itemHashCode = editingData.GetHashCode(); if (!editingIdStrings.ContainsKey(itemHashCode)) { editingIdStrings[itemHashCode] = editingData.Id.ToString(); originalIds[itemHashCode] = editingData.Id; } // 使用TextField进行编辑 GUI.SetNextControlName($"IdField_{itemHashCode}"); string newIdString = EditorGUILayout.TextField(editingIdStrings[itemHashCode]); // 如果字符串改变,更新缓存 if (newIdString != editingIdStrings[itemHashCode]) { editingIdStrings[itemHashCode] = newIdString; } // 检查是否失去焦点 string currentFocus = GUI.GetNameOfFocusedControl(); if (currentFocus != $"IdField_{itemHashCode}" && editingIdStrings.ContainsKey(itemHashCode)) { // 尝试解析ID if (int.TryParse(editingIdStrings[itemHashCode], out int newId)) { // 检查是否有重复ID(排除自己) bool hasDuplicate = tempItemsList.Any(item => item != editingData && item.Id == newId); if (hasDuplicate) { // 有重复,恢复原值并提示 EditorUtility.DisplayDialog("ID重复", $"ID {newId} 已被其他资源项使用,请使用不同的ID。", "确定"); editingIdStrings[itemHashCode] = originalIds[itemHashCode].ToString(); editingData.Id = originalIds[itemHashCode]; } else if (newId != editingData.Id) { // 没有重复,更新ID int oldId = editingData.Id; editingData.Id = newId; originalIds[itemHashCode] = newId; // 更新itemFoldouts字典的key if (itemFoldouts.ContainsKey(oldId)) { bool wasFoldout = itemFoldouts[oldId]; itemFoldouts.Remove(oldId); itemFoldouts[newId] = wasFoldout; } } } else { // 解析失败,恢复原值 editingIdStrings[itemHashCode] = originalIds[itemHashCode].ToString(); editingData.Id = originalIds[itemHashCode]; } } EditorGUILayout.Space(5); EditorGUILayout.LabelField("资源名称", EditorStyles.boldLabel); editingData.Name = EditorGUILayout.TextField(editingData.Name); EditorGUILayout.Space(5); EditorGUILayout.LabelField("资源描述", EditorStyles.boldLabel); editingData.Desc = EditorGUILayout.TextArea(editingData.Desc, GUILayout.Height(60)); if (EditorGUI.EndChangeCheck()) { MarkAsChanged(); } EditorGUILayout.EndVertical(); } private void DrawSpriteSection(ArtItemData editingData) { EditorGUILayout.LabelField("Sprite 图片资源", EditorStyles.boldLabel); EditorGUILayout.BeginVertical("box"); EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Sprite引用", GUILayout.Width(80)); var newSprite = (Sprite)EditorGUILayout.ObjectField( editingData.Sprite, typeof(Sprite), false); EditorGUILayout.EndHorizontal(); if (EditorGUI.EndChangeCheck()) { editingData.Sprite = newSprite; MarkAsChanged(); } // 预览 if (editingData.Sprite != null) { EditorGUILayout.Space(10); EditorGUILayout.LabelField("预览", EditorStyles.boldLabel); Rect previewRect = GUILayoutUtility.GetRect(150, 150, GUILayout.Width(150), GUILayout.Height(150)); EditorGUI.DrawRect(previewRect, new Color(0.2f, 0.2f, 0.2f)); DrawSpritePreview(previewRect, editingData.Sprite); // 显示信息 var atlas = GetSpriteAtlas(editingData.Sprite); if (atlas.HasValue) { EditorGUILayout.LabelField($"图集: {atlas.Value.atlas.name}"); } EditorGUILayout.LabelField($"尺寸: {editingData.Sprite.rect.width} x {editingData.Sprite.rect.height}"); } EditorGUILayout.EndVertical(); } private void DrawSpineSection(ArtItemData editingData) { EditorGUILayout.LabelField("Spine 骨骼资源", EditorStyles.boldLabel); EditorGUILayout.BeginVertical("box"); EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("SkeletonDataAsset引用", GUILayout.Width(150)); var newSpineAsset = (SkeletonDataAsset)EditorGUILayout.ObjectField( editingData.SpineAsset, typeof(SkeletonDataAsset), false); EditorGUILayout.EndHorizontal(); bool spineAssetChanged = EditorGUI.EndChangeCheck(); if (spineAssetChanged) { editingData.SpineAsset = newSpineAsset; // 自动选择第一个动画 if (newSpineAsset != null) { var animations = GetSpineAnimations(newSpineAsset); if (animations != null && animations.Length > 0) { editingData.SpineAnimName = animations[0]; // 暂停其他正在播放的动画 StopAllSpineAnimations(); currentPlayingSpineItemId = editingData.Id; } } MarkAsChanged(); } if (editingData.SpineAsset != null) { EditorGUILayout.Space(5); // 获取所有动画名称 var animations = GetSpineAnimations(editingData.SpineAsset); if (animations != null && animations.Length > 0) { EditorGUI.BeginChangeCheck(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("动画名称", GUILayout.Width(80)); int currentIndex = System.Array.IndexOf(animations, editingData.SpineAnimName); if (currentIndex < 0) currentIndex = 0; int newIndex = EditorGUILayout.Popup(currentIndex, animations); EditorGUILayout.EndHorizontal(); if (EditorGUI.EndChangeCheck()) { editingData.SpineAnimName = animations[newIndex]; MarkAsChanged(); // 切换动画时自动播放 StopAllSpineAnimations(); currentPlayingSpineItemId = editingData.Id; PlaySpineAnimation(editingData.SpineAsset, editingData.SpineAnimName); } } else { EditorGUILayout.HelpBox("该Spine资源没有动画", MessageType.Warning); } // 预览 EditorGUILayout.Space(10); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("预览", EditorStyles.boldLabel); bool isPlaying = (currentPlayingSpineItemId == editingData.Id); GUI.backgroundColor = isPlaying ? Color.green : Color.white; if (GUILayout.Button(isPlaying ? "⏸ 暂停" : "▶ 播放", GUILayout.Width(80))) { if (isPlaying) { currentPlayingSpineItemId = -1; StopSpineAnimation(); } else { StopAllSpineAnimations(); currentPlayingSpineItemId = editingData.Id; PlaySpineAnimation(editingData.SpineAsset, editingData.SpineAnimName); } } GUI.backgroundColor = Color.white; EditorGUILayout.EndHorizontal(); Rect previewRect = GUILayoutUtility.GetRect(150, 150, GUILayout.Width(150), GUILayout.Height(150)); DrawSpinePreview(previewRect, editingData.SpineAsset, editingData.SpineAnimName, isPlaying); } EditorGUILayout.EndVertical(); } #endregion #region 数据操作 // 找到所有的SO文件展示在列表中 private void RefreshTableList() { allTables.Clear(); // 搜索所有ScriptableObject,然后手动过滤ArtTableSO类型 string[] allSOGuids = AssetDatabase.FindAssets("t:ScriptableObject"); int checkedCount = 0; foreach (string guid in allSOGuids) { string path = AssetDatabase.GUIDToAssetPath(guid); // 只检查在SO_ROOT_PATH路径下的文件 if (path.StartsWith(SO_ROOT_PATH)) { checkedCount++; var asset = AssetDatabase.LoadAssetAtPath(path); // 检查是否是ArtTableSO类型 if (asset is ArtTableSO table) { allTables.Add(table); // Debug.Log($"[ArtResourceConfig] 找到配置表: {table.TableName} - {path}"); } } } allTables = allTables.OrderBy(t => t.TableName).ToList(); // Debug.Log($"[ArtResourceConfig] 在 {SO_ROOT_PATH} 下检查了 {checkedCount} 个SO文件,找到 {allTables.Count} 个配置表"); // 清除缓存 tempItemsList.Clear(); hasUnsavedChanges = false; Repaint(); } private void SelectTable(ArtTableSO table) { // 如果有未保存的更改,提示用户 if (hasUnsavedChanges && selectedTable != null && selectedTable != table) { // 0=保存并切换, 1=取消, 2=不保存直接切换 int result = EditorUtility.DisplayDialogComplex("未保存的更改", $"当前配置表「{selectedTable.TableName}」有未保存的更改。", "保存并切换", "取消", "不保存,直接切换"); if (result == 0) { // 保存当前表后再切换 SaveCurrentTable(); // 如果保存失败(仍有未保存标记),中止切换 if (hasUnsavedChanges) return; } else if (result == 1) { // 取消切换 return; } // result == 2: 不保存,继续切换 } selectedTable = table; selectedItem = null; hasUnsavedChanges = false; itemFoldouts.Clear(); // 加载数据到暂存区 LoadToTempData(); } private void LoadToTempData() { tempItemsList.Clear(); if (selectedTable == null) return; // 创建完全独立的副本 foreach (var item in selectedTable.Items) { var copy = new ArtItemData { Id = item.Id, Name = item.Name, Desc = item.Desc, Sprite = item.Sprite, SpritePath = item.SpritePath, SpineAsset = item.SpineAsset, SpineAssetPath = item.SpineAssetPath, SpineAnimName = item.SpineAnimName }; tempItemsList.Add(copy); } } private string GetSpriteReferencePath(ArtItemData item) { if (item == null) { return string.Empty; } if (item.Sprite != null) { return AssetDatabase.GetAssetPath(item.Sprite); } return item.SpritePath ?? string.Empty; } private void SelectItem(ArtItemData item) { selectedItem = item; // 清除当前焦点,避免显示旧的编辑内容 GUI.FocusControl(null); GUIUtility.keyboardControl = 0; EditorGUIUtility.editingTextField = false; // 折叠所有其他项,只展开当前项 foreach (var tempItem in tempItemsList) { itemFoldouts[tempItem.Id] = (tempItem == item); } // 设置跳转标记,让第三栏滚动到这个Item scrollToItemId = item.Id; // 如果有Spine资源,自动播放动画 if (item.SpineAsset != null && !string.IsNullOrEmpty(item.SpineAnimName)) { StopAllSpineAnimations(); currentPlayingSpineItemId = item.Id; PlaySpineAnimation(item.SpineAsset, item.SpineAnimName); } else { // 如果没有Spine资源,停止所有播放 StopAllSpineAnimations(); } Repaint(); } private void MarkAsChanged() { hasUnsavedChanges = true; } private void CreateNewTable() { string path = EditorUtility.SaveFilePanelInProject( "创建新配置表", "NewArtTable", "asset", "请选择配置表保存位置", SO_ROOT_PATH); if (string.IsNullOrEmpty(path)) { return; } // 检查是否直接放在Art_SO根目录 string relativePath = path.Replace(SO_ROOT_PATH + "/", ""); if (!relativePath.Contains("/")) { EditorUtility.DisplayDialog("路径错误", "配置表不能直接放在 Art_SO 根目录下!\n请在 Art_SO 下创建子文件夹,然后将配置表放在子文件夹中。", "确定"); return; } var newTable = ScriptableObject.CreateInstance(); newTable.TableName = Path.GetFileNameWithoutExtension(path); newTable.TableId = GenerateUniqueTableId(); newTable.Items = new List(); AssetDatabase.CreateAsset(newTable, path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); // 同步创建JSON文件 SyncToJson(newTable); // 更新manifest文件 UpdateManifest(); RefreshTableList(); SelectTable(newTable); Debug.Log($"创建配置表成功: {path}"); } private int GenerateUniqueTableId() { if (allTables.Count == 0) return 1; return allTables.Max(t => t.TableId) + 1; } private void AddNewItem() { if (selectedTable == null) return; int newId = tempItemsList.Count > 0 ? tempItemsList.Max(i => i.Id) + 1 : 1; var newItem = new ArtItemData { Id = newId, Name = $"新资源项_{newId}", Desc = "" }; tempItemsList.Add(newItem); hasUnsavedChanges = true; SelectItem(newItem); } private void DeleteItem(ArtItemData item) { if (item == null) return; tempItemsList.Remove(item); hasUnsavedChanges = true; if (selectedItem == item) { selectedItem = tempItemsList.FirstOrDefault(); } Repaint(); } private void DeleteCurrentTable() { if (selectedTable == null) return; if (!EditorUtility.DisplayDialog("确认删除", $"确定要删除配置表「{selectedTable.TableName}」吗?\n这将同时删除SO文件和对应的JSON文件!\n此操作不可恢复!", "删除", "取消")) { return; } string tableName = selectedTable.TableName; string soPath = AssetDatabase.GetAssetPath(selectedTable); // 计算JSON路径 string relativePath = soPath.Replace(SO_ROOT_PATH, "").Replace(".asset", ".json"); string jsonPath = JSON_ROOT_PATH + relativePath; // 删除SO文件 AssetDatabase.DeleteAsset(soPath); // 删除JSON文件(如果存在) if (File.Exists(jsonPath)) { File.Delete(jsonPath); // 同时删除.meta文件 if (File.Exists(jsonPath + ".meta")) { File.Delete(jsonPath + ".meta"); } } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); // 更新manifest文件 UpdateManifest(); Debug.Log($"已删除配置表: {tableName}"); Debug.Log($"SO路径: {soPath}"); if (File.Exists(jsonPath)) { Debug.Log($"JSON路径: {jsonPath}"); } // 清空选择 selectedTable = null; selectedItem = null; tempItemsList.Clear(); hasUnsavedChanges = false; // 刷新列表 RefreshTableList(); EditorUtility.DisplayDialog("删除成功", $"配置表「{tableName}」已删除", "确定"); } private void BatchImportSprites() { if (selectedTable == null) { EditorUtility.DisplayDialog("错误", "请先选择一个配置表", "确定"); return; } string folderPath = EditorUtility.OpenFolderPanel("选择Sprite文件夹", "Assets", ""); if (string.IsNullOrEmpty(folderPath)) { return; } // 转换为相对路径 if (!folderPath.StartsWith(Application.dataPath)) { EditorUtility.DisplayDialog("错误", "请选择项目内的文件夹", "确定"); return; } string relativePath = "Assets" + folderPath.Substring(Application.dataPath.Length); // 查找所有Sprite string[] guids = AssetDatabase.FindAssets("t:Sprite", new[] { relativePath }); if (guids.Length == 0) { EditorUtility.DisplayDialog("提示", "该文件夹中没有找到Sprite资源", "确定"); return; } var existingSpritePaths = new HashSet( tempItemsList .Select(GetSpriteReferencePath) .Where(path => !string.IsNullOrEmpty(path))); var spritesToImport = new List(); var spritePathsToImport = new List(); int skippedExistingCount = 0; foreach (string guid in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); if (existingSpritePaths.Contains(assetPath)) { skippedExistingCount++; continue; } Sprite sprite = AssetDatabase.LoadAssetAtPath(assetPath); if (sprite == null) { continue; } spritesToImport.Add(sprite); spritePathsToImport.Add(assetPath); existingSpritePaths.Add(assetPath); } if (spritesToImport.Count == 0) { EditorUtility.DisplayDialog("提示", skippedExistingCount > 0 ? "该文件夹中的Sprite已全部存在于当前表格中,已自动跳过。" : "该文件夹中没有可导入的Sprite资源。", "确定"); return; } if (spritesToImport.Count > 200) { bool shouldContinue = EditorUtility.DisplayDialog( "批量导入确认", $"要导入的图片数量为 {spritesToImport.Count} 个,超过200个,是否继续?", "继续", "取消"); if (!shouldContinue) { return; } } int startId = tempItemsList.Count > 0 ? tempItemsList.Max(i => i.Id) + 1 : 1; int importCount = 0; for (int i = 0; i < spritesToImport.Count; i++) { Sprite sprite = spritesToImport[i]; string assetPath = spritePathsToImport[i]; var newItem = new ArtItemData { Id = startId + importCount, Name = sprite.name, Desc = "", Sprite = sprite, SpritePath = assetPath }; tempItemsList.Add(newItem); importCount++; } if (importCount > 0) { hasUnsavedChanges = true; EditorUtility.DisplayDialog("导入成功", $"成功导入 {importCount} 个Sprite资源\n跳过已存在引用 {skippedExistingCount} 个\n记得点击保存按钮!", "确定"); } } private void BatchImportSpritesByFiles() { if (selectedTable == null) { EditorUtility.DisplayDialog("错误", "请先选择一个配置表", "确定"); return; } BatchImportByDragWindow.Show(this); } /// /// 通过文件路径列表导入Sprite(供拖拽导入窗口调用) /// public void ImportSpritesByAssetPaths(string[] assetPaths) { if (selectedTable == null || assetPaths == null || assetPaths.Length == 0) return; var existingSpritePaths = new HashSet( tempItemsList .Select(GetSpriteReferencePath) .Where(path => !string.IsNullOrEmpty(path))); var spritesToImport = new List(); var spritePathsToImport = new List(); int skippedExistingCount = 0; int notSpriteCount = 0; foreach (string assetPath in assetPaths) { if (existingSpritePaths.Contains(assetPath)) { skippedExistingCount++; continue; } // 尝试加载为Sprite Sprite sprite = AssetDatabase.LoadAssetAtPath(assetPath); if (sprite == null) { notSpriteCount++; continue; } spritesToImport.Add(sprite); spritePathsToImport.Add(assetPath); existingSpritePaths.Add(assetPath); } if (spritesToImport.Count == 0) { string msg = "没有可导入的Sprite资源。"; if (notSpriteCount > 0) msg += $"\n{notSpriteCount} 个文件不是Sprite类型(请检查图片导入设置)。"; if (skippedExistingCount > 0) msg += $"\n{skippedExistingCount} 个已存在于当前表格中,已跳过。"; EditorUtility.DisplayDialog("提示", msg, "确定"); return; } int startId = tempItemsList.Count > 0 ? tempItemsList.Max(i => i.Id) + 1 : 1; int importCount = 0; for (int i = 0; i < spritesToImport.Count; i++) { Sprite sprite = spritesToImport[i]; string assetPath = spritePathsToImport[i]; var newItem = new ArtItemData { Id = startId + importCount, Name = sprite.name, Desc = "", Sprite = sprite, SpritePath = assetPath }; tempItemsList.Add(newItem); importCount++; } if (importCount > 0) { hasUnsavedChanges = true; string msg = $"成功导入 {importCount} 个Sprite资源"; if (skippedExistingCount > 0) msg += $"\n跳过已存在引用 {skippedExistingCount} 个"; if (notSpriteCount > 0) msg += $"\n跳过非Sprite文件 {notSpriteCount} 个"; msg += "\n记得点击保存按钮!"; EditorUtility.DisplayDialog("导入成功", msg, "确定"); Repaint(); } } private void SaveCurrentTable() { if (selectedTable == null) { EditorUtility.DisplayDialog("保存失败", "没有选中的配置表", "确定"); return; } selectedTable.TableName = (selectedTable.TableName ?? string.Empty).Trim(); if (string.IsNullOrEmpty(selectedTable.TableName)) { EditorUtility.DisplayDialog("保存失败", "表格名称不能为空。", "确定"); return; } if (selectedTable.TableName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) { EditorUtility.DisplayDialog("保存失败", "表格名称包含非法文件名字符,请修改后重试。", "确定"); return; } string originalSoPath = AssetDatabase.GetAssetPath(selectedTable); // 检查表格ID和名称的唯一性 foreach (var table in allTables) { if (table == selectedTable) continue; if (table.TableId == selectedTable.TableId) { EditorUtility.DisplayDialog("保存失败", $"表格ID {selectedTable.TableId} 已被表格「{table.TableName}」使用!\n请使用不同的ID。", "确定"); return; } if (table.TableName == selectedTable.TableName) { EditorUtility.DisplayDialog("保存失败", $"表格名称「{selectedTable.TableName}」已被使用!\n请使用不同的名称。", "确定"); return; } } // 检查资源项名称的唯一性 var nameGroups = tempItemsList.GroupBy(item => item.Name).Where(g => g.Count() > 1).ToList(); if (nameGroups.Any()) { string duplicateNames = string.Join(", ", nameGroups.Select(g => $"'{g.Key}'")); EditorUtility.DisplayDialog("保存失败", $"资源项名称重复!\n以下名称出现了多次: {duplicateNames}\n请确保每个资源项的名称唯一。", "确定"); return; } // 检查资源项ID的唯一性 var idGroups = tempItemsList.GroupBy(item => item.Id).Where(g => g.Count() > 1).ToList(); if (idGroups.Any()) { string duplicateIds = string.Join(", ", idGroups.Select(g => g.Key.ToString())); EditorUtility.DisplayDialog("保存失败", $"资源项ID重复!\n以下ID出现了多次: {duplicateIds}\n请确保每个资源项的ID唯一。", "确定"); return; } // 检查是否有资源项既没有Sprite又没有Spine var emptyItems = tempItemsList.Where(item => item.Sprite == null && item.SpineAsset == null).ToList(); if (emptyItems.Any()) { string emptyItemNames = string.Join(", ", emptyItems.Select(item => $"'{item.Name}' (ID: {item.Id})")); EditorUtility.DisplayDialog("保存失败", $"以下资源项既没有Sprite也没有Spine资源!\n{emptyItemNames}\n请至少为每个资源项配置一种资源。", "确定"); return; } // 清空原始列表 selectedTable.Items.Clear(); // 将暂存区的数据复制到原始数据,同时更新路径 foreach (var tempItem in tempItemsList) { var newItem = new ArtItemData { Id = tempItem.Id, Name = tempItem.Name, Desc = tempItem.Desc, Sprite = tempItem.Sprite, SpritePath = tempItem.Sprite != null ? AssetDatabase.GetAssetPath(tempItem.Sprite) : "", SpineAsset = tempItem.SpineAsset, SpineAssetPath = tempItem.SpineAsset != null ? AssetDatabase.GetAssetPath(tempItem.SpineAsset) : "", SpineAnimName = tempItem.SpineAnimName }; selectedTable.Items.Add(newItem); } selectedTable.name = selectedTable.TableName; if (!TrySyncTableAssetNames(selectedTable, originalSoPath)) { return; } // 保存SO EditorUtility.SetDirty(selectedTable); AssetDatabase.SaveAssets(); // 同步JSON SyncToJson(selectedTable); // 同步Thrift Bytes(新增) SyncToThriftBytes(selectedTable); // 更新manifest文件 UpdateManifest(); // 生成合并的Bytes文件 GenerateMergedThriftBytes(); hasUnsavedChanges = false; var savedTable = selectedTable; RefreshTableList(); SelectTable(savedTable); Debug.Log($"配置表保存成功: {selectedTable.TableName}"); EditorUtility.DisplayDialog("保存成功", $"配置表「{selectedTable.TableName}」已保存\n共 {selectedTable.Items.Count} 项", "确定"); } private bool TrySyncTableAssetNames(ArtTableSO table, string originalSoPath) { if (table == null || string.IsNullOrEmpty(originalSoPath)) { return false; } string originalAssetName = Path.GetFileNameWithoutExtension(originalSoPath); if (string.Equals(originalAssetName, table.TableName, System.StringComparison.Ordinal)) { return true; } string originalJsonPath = GetJsonPathFromSoPath(originalSoPath); string renameError = AssetDatabase.RenameAsset(originalSoPath, table.TableName); if (!string.IsNullOrEmpty(renameError)) { EditorUtility.DisplayDialog("保存失败", $"重命名配置表文件失败:{renameError}", "确定"); return false; } string newSoPath = AssetDatabase.GetAssetPath(table); string newJsonPath = GetJsonPathFromSoPath(newSoPath); if (!string.Equals(originalJsonPath, newJsonPath, System.StringComparison.OrdinalIgnoreCase) && AssetDatabase.LoadAssetAtPath(originalJsonPath) != null) { AssetDatabase.DeleteAsset(originalJsonPath); } return true; } private string GetJsonPathFromSoPath(string soPath) { return soPath.Replace(SO_ROOT_PATH, JSON_ROOT_PATH).Replace(".asset", ".json"); } #endregion #region JSON同步 private void SyncToJson(ArtTableSO table) { string soPath = AssetDatabase.GetAssetPath(table); string relativePath = soPath.Replace(SO_ROOT_PATH, "").Replace(".asset", ".json"); string jsonPath = JSON_ROOT_PATH + relativePath; // 确保目录存在 string directory = Path.GetDirectoryName(jsonPath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // 创建JSON数据 var jsonData = new ArtTableJsonData { TableId = table.TableId, TableName = table.TableName, Items = table.Items.Select(item => new ArtItemJsonData { Id = item.Id, Name = item.Name, Desc = item.Desc, SpritePath = item.Sprite != null ? AssetDatabase.GetAssetPath(item.Sprite) : "", SpineAssetPath = item.SpineAsset != null ? AssetDatabase.GetAssetPath(item.SpineAsset) : "", SpineAnimName = item.SpineAnimName }).ToList() }; string json = JsonUtility.ToJson(jsonData, true); File.WriteAllText(jsonPath, json); AssetDatabase.Refresh(); Debug.Log($"JSON同步成功: {jsonPath}"); } private void UpdateManifest() { const string MANIFEST_PATH = "Assets/Art_SubModule/Art_Json/art_table_manifest.json"; try { // 查找所有JSON文件(从JSON目录扫描) string[] jsonFiles = Directory.GetFiles(JSON_ROOT_PATH, "*.json", SearchOption.AllDirectories); List tablePaths = new List(); foreach (string fullPath in jsonFiles) { string relativePath = fullPath.Replace("\\", "/").Replace(Application.dataPath.Replace("/Assets", "").Replace("\\", "/") + "/", ""); // 排除manifest文件本身 if (relativePath.EndsWith("art_table_manifest.json")) continue; tablePaths.Add(relativePath); } // 排序路径 tablePaths.Sort(); // 创建或更新manifest ArtTableManifest manifest; // 如果文件存在,读取现有的预加载配置 if (File.Exists(MANIFEST_PATH)) { string existingJson = File.ReadAllText(MANIFEST_PATH); manifest = JsonUtility.FromJson(existingJson); // 确保字段不为null if (manifest == null) { manifest = new ArtTableManifest(); } if (manifest.preloadTableIds == null) { manifest.preloadTableIds = new int[0]; } } else { // 创建新的manifest manifest = new ArtTableManifest { preloadTableIds = new int[0] }; } // 更新路径列表 manifest.tablePaths = tablePaths.ToArray(); // 清理预加载配置:移除已删除表的ID // 获取所有当前存在的表的ID List validTableIds = new List(); foreach (string jsonPath in tablePaths) { try { string jsonContent = File.ReadAllText(jsonPath); var jsonData = JsonUtility.FromJson(jsonContent); if (jsonData != null) { validTableIds.Add(jsonData.TableId); } } catch { // 忽略无法解析的文件 } } // 过滤掉已删除表的ID if (manifest.preloadTableIds != null && manifest.preloadTableIds.Length > 0) { var cleanedPreloadIds = manifest.preloadTableIds.Where(id => validTableIds.Contains(id)).ToArray(); int removedCount = manifest.preloadTableIds.Length - cleanedPreloadIds.Length; manifest.preloadTableIds = cleanedPreloadIds; if (removedCount > 0) { Debug.Log($"[ArtResourceConfigEditor] 从预加载配置中移除了 {removedCount} 个已删除表的ID"); } } // 序列化为JSON string json = JsonUtility.ToJson(manifest, true); // 确保目录存在 string directory = Path.GetDirectoryName(MANIFEST_PATH); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // 写入文件 File.WriteAllText(MANIFEST_PATH, json); AssetDatabase.ImportAsset(MANIFEST_PATH); Debug.Log($"[ArtResourceConfigEditor] Manifest文件已更新: {MANIFEST_PATH}, 共 {tablePaths.Count} 个JSON表"); } catch (System.Exception ex) { Debug.LogError($"[ArtResourceConfigEditor] 更新Manifest文件失败: {ex.Message}"); } } /// /// 将单个表同步到Thrift Bytes格式(单表文件,暂不使用) /// private void SyncToThriftBytes(ArtTableSO table) { // 注意:这个方法生成单表bytes,但我们实际使用的是GenerateMergedThriftBytes() // 保留此方法以防将来需要单表加载 Debug.Log($"[ArtResourceConfigEditor] 跳过单表Bytes生成: {table.TableName}(使用合并模式)"); } /// /// 生成合并的Thrift Bytes文件(所有表合并) /// 参考 ConfigManager 的加载方式 /// private void GenerateMergedThriftBytes() { try { var startTime = System.Diagnostics.Stopwatch.StartNew(); // 确保输出目录存在 if (!Directory.Exists(BYTES_ROOT_PATH)) { Directory.CreateDirectory(BYTES_ROOT_PATH); } string outputPath = Path.Combine(BYTES_ROOT_PATH, "ArtResourceConfig.bytes"); // 加载所有SO表 string[] allSOGuids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { SO_ROOT_PATH }); List allSOTables = new List(); foreach (string guid in allSOGuids) { string path = AssetDatabase.GUIDToAssetPath(guid); ArtTableSO table = AssetDatabase.LoadAssetAtPath(path); if (table != null) { allSOTables.Add(table); } } // 创建Thrift数据结构 var thriftConfig = new Byway.Thrift.Data.ArtResourceConfig { Tables = new List(), Version = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds(), GenerateTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; // 转换所有表 foreach (var soTable in allSOTables) { var thriftTable = new Byway.Thrift.Data.ArtTable { TableId = soTable.TableId, TableName = soTable.TableName, Items = new List() }; foreach (var soItem in soTable.Items) { var thriftItem = new Byway.Thrift.Data.ArtItem { Id = soItem.Id, Name = soItem.Name ?? "", Desc = soItem.Desc ?? "", SpritePath = soItem.Sprite != null ? AssetDatabase.GetAssetPath(soItem.Sprite) : "", SpineAssetPath = soItem.SpineAsset != null ? AssetDatabase.GetAssetPath(soItem.SpineAsset) : "", SpineAnimName = soItem.SpineAnimName ?? "" }; thriftTable.Items.Add(thriftItem); } thriftConfig.Tables.Add(thriftTable); } // 加载预加载配置(从manifest读取) const string MANIFEST_PATH = "Assets/Art_SubModule/Art_Json/art_table_manifest.json"; if (File.Exists(MANIFEST_PATH)) { string manifestJson = File.ReadAllText(MANIFEST_PATH); var manifest = JsonUtility.FromJson(manifestJson); if (manifest != null && manifest.preloadTableIds != null) { thriftConfig.PreloadTableIds = manifest.preloadTableIds.ToList(); } } // 序列化到bytes byte[] bytesData; using (var memoryStream = new MemoryStream()) { using (var transport = new TStreamTransport(null, memoryStream, new TConfiguration())) { using (var protocol = new TBinaryProtocol(transport)) { thriftConfig.WriteAsync(protocol, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); } } bytesData = memoryStream.ToArray(); } // 写入文件 File.WriteAllBytes(outputPath, bytesData); AssetDatabase.ImportAsset(outputPath); startTime.Stop(); Debug.Log($"[ArtResourceConfigEditor] ✅ Thrift Bytes生成成功!\n" + $" 路径: {outputPath}\n" + $" 表数量: {thriftConfig.Tables.Count}\n" + $" 文件大小: {bytesData.Length / 1024f:F2} KB\n" + $" 耗时: {startTime.ElapsedMilliseconds} ms"); } catch (System.Exception ex) { Debug.LogError($"[ArtResourceConfigEditor] ❌ 生成Thrift Bytes失败: {ex.Message}\n{ex.StackTrace}"); } } /// /// 静态版本:生成合并的Thrift Bytes文件(供外部工具调用) /// public static void GenerateMergedThriftBytesStatic() { try { var startTime = System.Diagnostics.Stopwatch.StartNew(); // 确保输出目录存在 const string BYTES_ROOT_PATH = "Assets/Art_SubModule/Art_Bytes"; if (!Directory.Exists(BYTES_ROOT_PATH)) { Directory.CreateDirectory(BYTES_ROOT_PATH); } string outputPath = Path.Combine(BYTES_ROOT_PATH, "ArtResourceConfig.bytes"); // 加载所有SO表 const string SO_ROOT_PATH = "Assets/Art_SubModule/Art_SO"; string[] allSOGuids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { SO_ROOT_PATH }); List allSOTables = new List(); foreach (string guid in allSOGuids) { string path = AssetDatabase.GUIDToAssetPath(guid); ArtTableSO table = AssetDatabase.LoadAssetAtPath(path); if (table != null) { allSOTables.Add(table); } } // 创建Thrift数据结构 var thriftConfig = new Byway.Thrift.Data.ArtResourceConfig { Tables = new List(), Version = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds(), GenerateTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; // 转换所有表 foreach (var soTable in allSOTables) { var thriftTable = new Byway.Thrift.Data.ArtTable { TableId = soTable.TableId, TableName = soTable.TableName, Items = new List() }; foreach (var soItem in soTable.Items) { var thriftItem = new Byway.Thrift.Data.ArtItem { Id = soItem.Id, Name = soItem.Name ?? "", Desc = soItem.Desc ?? "", SpritePath = soItem.Sprite != null ? AssetDatabase.GetAssetPath(soItem.Sprite) : "", SpineAssetPath = soItem.SpineAsset != null ? AssetDatabase.GetAssetPath(soItem.SpineAsset) : "", SpineAnimName = soItem.SpineAnimName ?? "" }; thriftTable.Items.Add(thriftItem); } thriftConfig.Tables.Add(thriftTable); } // 加载预加载配置(从manifest读取) const string MANIFEST_PATH = "Assets/Art_SubModule/Art_Json/art_table_manifest.json"; if (File.Exists(MANIFEST_PATH)) { string manifestJson = File.ReadAllText(MANIFEST_PATH); var manifest = JsonUtility.FromJson(manifestJson); if (manifest != null && manifest.preloadTableIds != null) { thriftConfig.PreloadTableIds = manifest.preloadTableIds.ToList(); } } // 序列化到bytes byte[] bytesData; using (var memoryStream = new MemoryStream()) { using (var transport = new TStreamTransport(null, memoryStream, new TConfiguration())) { using (var protocol = new TBinaryProtocol(transport)) { thriftConfig.WriteAsync(protocol, System.Threading.CancellationToken.None).GetAwaiter().GetResult(); } } bytesData = memoryStream.ToArray(); } // 写入文件 File.WriteAllBytes(outputPath, bytesData); AssetDatabase.ImportAsset(outputPath); startTime.Stop(); Debug.Log($"[ArtResourceConfigEditor] ✅ Thrift Bytes生成成功!\n" + $" 路径: {outputPath}\n" + $" 表数量: {thriftConfig.Tables.Count}\n" + $" 文件大小: {bytesData.Length / 1024f:F2} KB\n" + $" 耗时: {startTime.ElapsedMilliseconds} ms"); } catch (System.Exception ex) { Debug.LogError($"[ArtResourceConfigEditor] ❌ 生成Thrift Bytes失败: {ex.Message}\n{ex.StackTrace}"); } } [System.Serializable] public class ArtTableManifest { public string[] tablePaths; public int[] preloadTableIds; } [System.Serializable] public class ArtTableJsonData { public int TableId; public string TableName; public List Items; } [System.Serializable] public class ArtItemJsonData { public int Id; public string Name; public string Desc; public string SpritePath; public string SpineAssetPath; public string SpineAnimName; } #endregion #region 预览功能 private void DrawSpritePreview(Rect rect, Sprite sprite) { if (sprite == null || sprite.texture == null) return; Rect texCoords = sprite.textureRect; Texture2D tex = sprite.texture; Rect normalizedCoords = new Rect( texCoords.x / tex.width, texCoords.y / tex.height, texCoords.width / tex.width, texCoords.height / tex.height ); // 计算适配矩形(保持长宽比) float aspect = texCoords.width / texCoords.height; Rect drawRect = rect; if (aspect > 1f) { float height = drawRect.width / aspect; drawRect.y += (drawRect.height - height) * 0.5f; drawRect.height = height; } else { float width = drawRect.height * aspect; drawRect.x += (drawRect.width - width) * 0.5f; drawRect.width = width; } GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true); } private string[] GetSpineAnimations(SkeletonDataAsset spineAsset) { if (spineAsset == null || spineAsset.GetSkeletonData(false) == null) return new string[0]; var skeletonData = spineAsset.GetSkeletonData(false); var animations = skeletonData.Animations; if (animations == null || animations.Count == 0) return new string[0]; string[] animNames = new string[animations.Count]; for (int i = 0; i < animations.Count; i++) { animNames[i] = animations.Items[i].Name; } return animNames; } private void PlaySpineAnimation(SkeletonDataAsset spineAsset, string animName) { if (spineAsset == null || spinePreviewInstance == null || spinePreviewType == null) return; try { // 先清理旧的预览 var clearMethod = spinePreviewType.GetMethod("Clear"); if (clearMethod != null) { clearMethod.Invoke(spinePreviewInstance, null); } // 初始化预览(传入Repaint回调、SkeletonDataAsset和空字符串) var initMethod = spinePreviewType.GetMethod("Initialize", new System.Type[] { typeof(System.Action), typeof(SkeletonDataAsset), typeof(string) }); if (initMethod != null) { initMethod.Invoke(spinePreviewInstance, new object[] { new System.Action(Repaint), spineAsset, "" }); // 延迟设置动画,确保skeleton已完全初始化 if (!string.IsNullOrEmpty(animName)) { EditorApplication.delayCall += () => { try { if (spinePreviewInstance != null && spinePreviewType != null) { var skeletonAnimField = spinePreviewType.GetField("skeletonAnimation", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (skeletonAnimField != null) { var skeletonAnim = skeletonAnimField.GetValue(spinePreviewInstance) as SkeletonAnimation; if (skeletonAnim != null && skeletonAnim.valid && skeletonAnim.AnimationState != null) { skeletonAnim.AnimationState.SetAnimation(0, animName, true); } } } } catch { } }; } } Repaint(); } catch (System.Exception e) { Debug.LogWarning($"播放Spine动画失败: {e.Message}"); } } private void StopSpineAnimation() { if (spinePreviewInstance == null || spinePreviewType == null) return; try { var clearMethod = spinePreviewType.GetMethod("Clear"); if (clearMethod != null) { clearMethod.Invoke(spinePreviewInstance, null); } Repaint(); } catch (System.Exception e) { Debug.LogWarning($"停止Spine动画失败: {e.Message}"); } } private void StopAllSpineAnimations() { currentPlayingSpineItemId = -1; StopSpineAnimation(); } private void DrawSpinePreview(Rect rect, SkeletonDataAsset spineAsset, string animName, bool isPlaying) { if (spineAsset == null || spinePreviewInstance == null || spinePreviewType == null) { EditorGUI.HelpBox(rect, "Spine预览不可用", MessageType.Info); return; } try { // 只有在播放状态时才绘制预览 if (isPlaying) { // 检查预览是否有效 var isValidProperty = spinePreviewType.GetProperty("IsValid"); bool isValid = isValidProperty != null && (bool)isValidProperty.GetValue(spinePreviewInstance, null); if (isValid) { // 绘制预览 - 使用HandleInteractivePreviewGUI方法 var handleMethod = spinePreviewType.GetMethod("HandleInteractivePreviewGUI"); if (handleMethod != null) { handleMethod.Invoke(spinePreviewInstance, new object[] { rect, EditorStyles.helpBox }); } } else { EditorGUI.DrawRect(rect, new Color(0.2f, 0.2f, 0.2f)); GUI.Label(rect, "Spine预览初始化中...", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, normal = new GUIStyleState { textColor = Color.gray } }); } } else { // 不播放时显示静态提示 EditorGUI.DrawRect(rect, new Color(0.2f, 0.2f, 0.2f)); GUI.Label(rect, "点击播放按钮查看动画", new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, normal = new GUIStyleState { textColor = Color.gray } }); } } catch (System.Exception e) { EditorGUI.HelpBox(rect, $"Spine预览错误: {e.Message}", MessageType.Warning); } } private SpriteAtlasInfo? GetSpriteAtlas(Sprite sprite) { if (sprite == null) return null; if (spriteAtlasCache.TryGetValue(sprite, out var cached)) { return cached; } string[] atlasGuids = AssetDatabase.FindAssets("t:SpriteAtlas"); foreach (string guid in atlasGuids) { string atlasPath = AssetDatabase.GUIDToAssetPath(guid); SpriteAtlas atlas = AssetDatabase.LoadAssetAtPath(atlasPath); if (atlas != null && atlas.CanBindTo(sprite)) { var info = new SpriteAtlasInfo { atlas = atlas, atlasPath = atlasPath }; spriteAtlasCache[sprite] = info; return info; } } spriteAtlasCache[sprite] = null; return null; } #endregion #region 修复丢失引用和清理功能 /// /// 修复所有配置表中丢失的引用 /// 按照路径来找到引用(与"更新所有美术资源路径"相反) /// private static void FixAllMissingReferences() { if (!EditorUtility.DisplayDialog("修复丢失的引用", "此操作将遍历所有配置表,根据路径信息修复丢失的引用。\n" + "适用于跨项目拉取代码后引用丢失但路径正确的情况。\n\n" + "确定要继续吗?", "确定", "取消")) { return; } try { EditorUtility.DisplayProgressBar("修复丢失的引用", "正在查找配置表...", 0f); // 查找所有ArtTableSO文件 string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { SO_ROOT_PATH }); if (guids.Length == 0) { EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("提示", "未找到任何配置表。", "确定"); return; } int totalFixed = 0; int totalTables = 0; List errorMessages = new List(); List processedTables = new List(); for (int i = 0; i < guids.Length; i++) { string path = AssetDatabase.GUIDToAssetPath(guids[i]); ArtTableSO table = AssetDatabase.LoadAssetAtPath(path); if (table == null) continue; EditorUtility.DisplayProgressBar("修复丢失的引用", $"正在处理: {table.TableName} ({i + 1}/{guids.Length})", (float)i / guids.Length); int fixedInTable = FixTableReferences(table, errorMessages); if (fixedInTable > 0) { totalFixed += fixedInTable; totalTables++; processedTables.Add($"{table.TableName}: {fixedInTable}个"); // 保存修改 EditorUtility.SetDirty(table); } } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); EditorUtility.ClearProgressBar(); // 显示结果 string message = $"修复完成!\n\n" + $"处理的配置表: {guids.Length}个\n" + $"修复引用的配置表: {totalTables}个\n" + $"总共修复的引用: {totalFixed}个"; if (processedTables.Count > 0) { message += "\n\n修复详情:\n" + string.Join("\n", processedTables); } if (errorMessages.Count > 0) { message += "\n\n⚠️ 以下问题需要注意:\n" + string.Join("\n", errorMessages); } EditorUtility.DisplayDialog(totalFixed > 0 ? "修复成功" : "无需修复", message, "确定"); Debug.Log($"[修复丢失引用] {message}"); } catch (System.Exception ex) { EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("错误", $"修复过程中出现错误:\n{ex.Message}", "确定"); Debug.LogError($"[修复丢失引用] 错误: {ex.Message}\n{ex.StackTrace}"); } } /// /// 修复单个配置表的引用 /// private static int FixTableReferences(ArtTableSO table, List errorMessages) { int fixedCount = 0; foreach (var item in table.Items) { // 修复Sprite引用 if (item.Sprite == null && !string.IsNullOrEmpty(item.SpritePath)) { Sprite sprite = AssetDatabase.LoadAssetAtPath(item.SpritePath); if (sprite != null) { item.Sprite = sprite; fixedCount++; Debug.Log($"[修复引用] {table.TableName}/{item.Name}: 修复了Sprite引用 ({item.SpritePath})"); } else { string error = $"[{table.TableName}/{item.Name}] 找不到Sprite: {item.SpritePath}"; errorMessages.Add(error); Debug.LogWarning(error); } } // 修复SpineAsset引用 if (item.SpineAsset == null && !string.IsNullOrEmpty(item.SpineAssetPath)) { SkeletonDataAsset spineAsset = AssetDatabase.LoadAssetAtPath(item.SpineAssetPath); if (spineAsset != null) { item.SpineAsset = spineAsset; fixedCount++; Debug.Log($"[修复引用] {table.TableName}/{item.Name}: 修复了SpineAsset引用 ({item.SpineAssetPath})"); } else { string error = $"[{table.TableName}/{item.Name}] 找不到SpineAsset: {item.SpineAssetPath}"; errorMessages.Add(error); Debug.LogWarning(error); } } // 检查既没有引用又没有路径的情况 if (item.Sprite == null && string.IsNullOrEmpty(item.SpritePath) && item.SpineAsset == null && string.IsNullOrEmpty(item.SpineAssetPath)) { string error = $"[{table.TableName}/{item.Name}] 既没有引用也没有路径,需要手动配置"; errorMessages.Add(error); Debug.LogWarning(error); } } return fixedCount; } /// /// 清理所有配置表中引用丢失的Item /// private static void CleanupAllMissingItems() { if (!EditorUtility.DisplayDialog("清理丢失文件", "此操作将删除所有配置表中引用丢失的资源项。\n" + "同时会同步更新对应的JSON文件。\n\n" + "⚠️ 此操作不可恢复!\n\n" + "确定要继续吗?", "确定", "取消")) { return; } try { EditorUtility.DisplayProgressBar("清理丢失文件", "正在查找配置表...", 0f); // 查找所有ArtTableSO文件 string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { SO_ROOT_PATH }); if (guids.Length == 0) { EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("提示", "未找到任何配置表。", "确定"); return; } int totalRemoved = 0; int totalTables = 0; List cleanedTables = new List(); for (int i = 0; i < guids.Length; i++) { string path = AssetDatabase.GUIDToAssetPath(guids[i]); ArtTableSO table = AssetDatabase.LoadAssetAtPath(path); if (table == null) continue; EditorUtility.DisplayProgressBar("清理丢失文件", $"正在处理: {table.TableName} ({i + 1}/{guids.Length})", (float)i / guids.Length); int removedInTable = CleanupTableMissingItems(table); if (removedInTable > 0) { totalRemoved += removedInTable; totalTables++; cleanedTables.Add($"{table.TableName}: 移除{removedInTable}个"); // 保存修改 EditorUtility.SetDirty(table); // 同步到JSON SyncTableToJson(table); } } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); EditorUtility.ClearProgressBar(); // 显示结果 string message = $"清理完成!\n\n" + $"处理的配置表: {guids.Length}个\n" + $"清理的配置表: {totalTables}个\n" + $"总共移除的资源项: {totalRemoved}个"; if (cleanedTables.Count > 0) { message += "\n\n清理详情:\n" + string.Join("\n", cleanedTables); } EditorUtility.DisplayDialog(totalRemoved > 0 ? "清理成功" : "无需清理", message, "确定"); Debug.Log($"[清理丢失文件] {message}"); } catch (System.Exception ex) { EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("错误", $"清理过程中出现错误:\n{ex.Message}", "确定"); Debug.LogError($"[清理丢失文件] 错误: {ex.Message}\n{ex.StackTrace}"); } } /// /// 清理单个配置表中引用丢失的Item /// private static int CleanupTableMissingItems(ArtTableSO table) { List itemsToRemove = new List(); foreach (var item in table.Items) { bool hasMissingReference = false; string missingInfo = ""; // 检查Sprite引用是否丢失 if (!string.IsNullOrEmpty(item.SpritePath) && item.Sprite == null) { hasMissingReference = true; missingInfo += $" Sprite({item.SpritePath})"; } // 检查SpineAsset引用是否丢失 if (!string.IsNullOrEmpty(item.SpineAssetPath) && item.SpineAsset == null) { hasMissingReference = true; missingInfo += $" SpineAsset({item.SpineAssetPath})"; } if (hasMissingReference) { itemsToRemove.Add(item); Debug.Log($"[清理丢失文件] {table.TableName}/{item.Name}: 标记为删除 - 丢失:{missingInfo}"); } } // 移除标记的Item foreach (var item in itemsToRemove) { table.Items.Remove(item); } return itemsToRemove.Count; } /// /// 同步单个配置表到JSON(静态版本) /// private static void SyncTableToJson(ArtTableSO table) { string soPath = AssetDatabase.GetAssetPath(table); string relativePath = soPath.Replace(SO_ROOT_PATH, "").Replace(".asset", ".json"); string jsonPath = JSON_ROOT_PATH + relativePath; // 确保目录存在 string directory = Path.GetDirectoryName(jsonPath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // 创建JSON数据 var jsonData = new ArtTableJsonData { TableId = table.TableId, TableName = table.TableName, Items = table.Items.Select(item => new ArtItemJsonData { Id = item.Id, Name = item.Name, Desc = item.Desc, SpritePath = item.Sprite != null ? AssetDatabase.GetAssetPath(item.Sprite) : "", SpineAssetPath = item.SpineAsset != null ? AssetDatabase.GetAssetPath(item.SpineAsset) : "", SpineAnimName = item.SpineAnimName }).ToList() }; string json = JsonUtility.ToJson(jsonData, true); File.WriteAllText(jsonPath, json); AssetDatabase.Refresh(); Debug.Log($"[同步JSON] {jsonPath}"); } #endregion } /// /// 批量导入图片的拖拽窗口 /// 支持从Project窗口拖拽多个png图片进来 /// public class BatchImportByDragWindow : EditorWindow { private ArtResourceConfigEditor parentEditor; private List draggedObjects = new List(); private Vector2 scrollPos; public static void Show(ArtResourceConfigEditor editor) { var window = GetWindow("批量导入图片"); window.parentEditor = editor; window.draggedObjects.Clear(); window.minSize = new Vector2(400, 500); window.ShowUtility(); } private void OnGUI() { EditorGUILayout.LabelField("批量导入图片", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "从 Project 窗口中将 png 图片拖拽到下方区域,\n支持多选拖入。只有 Sprite 类型的图片会被导入。", MessageType.Info); EditorGUILayout.Space(5); // 拖拽区域 Rect dropArea = GUILayoutUtility.GetRect(0, 100, GUILayout.ExpandWidth(true)); GUI.Box(dropArea, "🖼 将图片拖拽到这里", new GUIStyle(GUI.skin.box) { alignment = TextAnchor.MiddleCenter, fontSize = 16, fontStyle = FontStyle.Bold }); HandleDragAndDrop(dropArea); EditorGUILayout.Space(5); // 已添加的列表 EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"已选择: {draggedObjects.Count} 个文件", EditorStyles.boldLabel); if (draggedObjects.Count > 0) { if (GUILayout.Button("清空", GUILayout.Width(60))) { draggedObjects.Clear(); } } EditorGUILayout.EndHorizontal(); scrollPos = EditorGUILayout.BeginScrollView(scrollPos); for (int i = draggedObjects.Count - 1; i >= 0; i--) { EditorGUILayout.BeginHorizontal("box"); EditorGUILayout.ObjectField(draggedObjects[i], typeof(Texture2D), false); if (GUILayout.Button("✕", GUILayout.Width(25))) { draggedObjects.RemoveAt(i); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); EditorGUILayout.Space(10); // 导入按钮 EditorGUI.BeginDisabledGroup(draggedObjects.Count == 0 || parentEditor == null); GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f); if (GUILayout.Button($"确认导入 ({draggedObjects.Count} 个)", GUILayout.Height(35))) { DoImport(); } GUI.backgroundColor = Color.white; EditorGUI.EndDisabledGroup(); } private void HandleDragAndDrop(Rect dropArea) { Event evt = Event.current; switch (evt.type) { case EventType.DragUpdated: case EventType.DragPerform: if (!dropArea.Contains(evt.mousePosition)) return; DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (evt.type == EventType.DragPerform) { DragAndDrop.AcceptDrag(); var existingPaths = new HashSet( draggedObjects.Select(o => AssetDatabase.GetAssetPath(o))); foreach (var obj in DragAndDrop.objectReferences) { string path = AssetDatabase.GetAssetPath(obj); if (string.IsNullOrEmpty(path)) continue; // 如果拖入的是文件夹,递归查找png if (AssetDatabase.IsValidFolder(path)) { string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { path }); foreach (string guid in guids) { string texPath = AssetDatabase.GUIDToAssetPath(guid); if (!texPath.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase)) continue; if (existingPaths.Contains(texPath)) continue; var texObj = AssetDatabase.LoadAssetAtPath(texPath); if (texObj != null) { draggedObjects.Add(texObj); existingPaths.Add(texPath); } } } else { // 只接受png文件 if (!path.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase)) continue; if (existingPaths.Contains(path)) continue; draggedObjects.Add(obj); existingPaths.Add(path); } } } evt.Use(); break; } } private void DoImport() { var assetPaths = draggedObjects .Select(o => AssetDatabase.GetAssetPath(o)) .Where(p => !string.IsNullOrEmpty(p)) .ToArray(); parentEditor.ImportSpritesByAssetPaths(assetPaths); Close(); } } }