diff --git a/Assets/Art_SubModule.meta b/Assets/Art_SubModule.meta
new file mode 100644
index 0000000..683e487
--- /dev/null
+++ b/Assets/Art_SubModule.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 56ee0b40ae212264598820ef1d8b3532
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Editor.meta b/Assets/Editor.meta
new file mode 100644
index 0000000..0522e7a
--- /dev/null
+++ b/Assets/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ef92907a06b50994a8029a98c05989f2
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Editor/Art_Tools/ArtResourceConfigEditor.cs b/Assets/Editor/Art_Tools/ArtResourceConfigEditor.cs
new file mode 100644
index 0000000..8bfd172
--- /dev/null
+++ b/Assets/Editor/Art_Tools/ArtResourceConfigEditor.cs
@@ -0,0 +1,1646 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+using System.Linq;
+using System.IO;
+using ArtResource;
+using Spine.Unity;
+using UnityEngine.U2D;
+
+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 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();
+ }
+
+ private void OnEnable()
+ {
+ 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] = true;
+ }
+
+ // 绘制文件夹
+ EditorGUILayout.BeginHorizontal();
+ folderFoldouts[folderName] = EditorGUILayout.Foldout(
+ folderFoldouts[folderName],
+ $"📁 {folderName} ({folderGroup.Count()})",
+ true,
+ EditorStyles.foldoutHeader);
+ 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 "根目录";
+ }
+
+ #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("📁 批量导入Sprite", GUILayout.Height(25)))
+ {
+ BatchImportSprites();
+ }
+ 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)
+ {
+ if (!EditorUtility.DisplayDialog("切换配置表",
+ "当前有未保存的更改,切换将丢失这些更改。\n确定要切换吗?",
+ "切换", "取消"))
+ {
+ return;
+ }
+ }
+
+ 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,
+ SpineAsset = item.SpineAsset,
+ SpineAnimName = item.SpineAnimName
+ };
+ tempItemsList.Add(copy);
+ }
+ }
+
+ 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;
+ }
+
+ int startId = tempItemsList.Count > 0 ? tempItemsList.Max(i => i.Id) + 1 : 1;
+ int importCount = 0;
+
+ foreach (string guid in guids)
+ {
+ string assetPath = AssetDatabase.GUIDToAssetPath(guid);
+ Sprite sprite = AssetDatabase.LoadAssetAtPath(assetPath);
+
+ if (sprite != null)
+ {
+ var newItem = new ArtItemData
+ {
+ Id = startId + importCount,
+ Name = sprite.name,
+ Desc = "",
+ Sprite = sprite
+ };
+
+ tempItemsList.Add(newItem);
+ importCount++;
+ }
+ }
+
+ if (importCount > 0)
+ {
+ hasUnsavedChanges = true;
+ EditorUtility.DisplayDialog("导入成功",
+ $"成功导入 {importCount} 个Sprite资源\n记得点击保存按钮!",
+ "确定");
+ }
+ }
+
+ private void SaveCurrentTable()
+ {
+ if (selectedTable == null)
+ {
+ EditorUtility.DisplayDialog("保存失败", "没有选中的配置表", "确定");
+ return;
+ }
+
+ // 检查表格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,
+ SpineAsset = tempItem.SpineAsset,
+ SpineAnimName = tempItem.SpineAnimName
+ };
+ selectedTable.Items.Add(newItem);
+ }
+
+ // 保存SO
+ EditorUtility.SetDirty(selectedTable);
+ AssetDatabase.SaveAssets();
+
+ // 同步JSON
+ SyncToJson(selectedTable);
+
+ // 更新manifest文件
+ UpdateManifest();
+
+ hasUnsavedChanges = false;
+
+ Debug.Log($"配置表保存成功: {selectedTable.TableName}");
+ EditorUtility.DisplayDialog("保存成功", $"配置表「{selectedTable.TableName}」已保存\n共 {selectedTable.Items.Count} 项", "确定");
+ }
+
+ #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_SO/art_table_manifest.json";
+
+ try
+ {
+ // 查找所有ArtTableSO文件
+ string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { SO_ROOT_PATH });
+ List tablePaths = new List();
+
+ foreach (string guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ if (!string.IsNullOrEmpty(path))
+ {
+ tablePaths.Add(path);
+ }
+ }
+
+ // 排序路径
+ 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 guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ var table = AssetDatabase.LoadAssetAtPath(path);
+ if (table != null)
+ {
+ validTableIds.Add(table.TableId);
+ }
+ }
+
+ // 过滤掉已删除表的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} 个表");
+ }
+ catch (System.Exception ex)
+ {
+ Debug.LogError($"[ArtResourceConfigEditor] 更新Manifest文件失败: {ex.Message}");
+ }
+ }
+
+ [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
+ }
+}
diff --git a/Assets/Editor/Art_Tools/ArtResourceLoadTest.cs b/Assets/Editor/Art_Tools/ArtResourceLoadTest.cs
new file mode 100644
index 0000000..e40ee0f
--- /dev/null
+++ b/Assets/Editor/Art_Tools/ArtResourceLoadTest.cs
@@ -0,0 +1,226 @@
+using UnityEngine;
+using UnityEditor;
+using ArtResource;
+using System.Diagnostics;
+using Debug = UnityEngine.Debug;
+
+namespace CrazyMaple.Editor
+{
+ ///
+ /// 测试工具:验证SO加载时是否会连带加载引用资源
+ ///
+ /// 测试方法:
+ /// 1. 在加载SO前后检查内存中的资源数量
+ /// 2. 分别测试带引用和不带引用的SO
+ /// 3. 分析加载耗时差异
+ ///
+ public class ArtResourceLoadTest : EditorWindow
+ {
+ [MenuItem("美术工具/测试SO加载行为")]
+ public static void ShowWindow()
+ {
+ GetWindow("SO加载测试");
+ }
+
+ private void OnGUI()
+ {
+ GUILayout.Label("SO加载行为测试", EditorStyles.boldLabel);
+ GUILayout.Space(10);
+
+ if (GUILayout.Button("测试1: 检查SO序列化大小", GUILayout.Height(40)))
+ {
+ TestSerializationSize();
+ }
+
+ GUILayout.Space(5);
+
+ if (GUILayout.Button("测试2: 对比加载时间(带引用 vs 纯路径)", GUILayout.Height(40)))
+ {
+ TestLoadingTime();
+ }
+
+ GUILayout.Space(5);
+
+ if (GUILayout.Button("测试3: 检查内存占用(加载前后)", GUILayout.Height(40)))
+ {
+ TestMemoryUsage();
+ }
+
+ GUILayout.Space(10);
+ EditorGUILayout.HelpBox(
+ "这些测试将帮助确定:\n" +
+ "1. SO是否会连带加载引用资源\n" +
+ "2. 引用字段对加载速度的影响\n" +
+ "3. 内存占用差异",
+ MessageType.Info);
+ }
+
+ ///
+ /// 测试1: 检查SO序列化后的文件大小
+ ///
+ private void TestSerializationSize()
+ {
+ Debug.Log("========== 测试1: SO序列化大小 ==========");
+
+ string[] guids = AssetDatabase.FindAssets("t:ArtTableSO");
+
+ if (guids.Length == 0)
+ {
+ Debug.LogWarning("未找到任何ArtTableSO");
+ return;
+ }
+
+ foreach (var guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ var table = AssetDatabase.LoadAssetAtPath(path);
+
+ if (table != null)
+ {
+ // 获取文件大小
+ var fileInfo = new System.IO.FileInfo(path);
+ long fileSize = fileInfo.Length;
+
+ // 统计引用数量
+ int spriteRefCount = 0;
+ int spineRefCount = 0;
+ int pathCount = 0;
+
+ foreach (var item in table.Items)
+ {
+ if (item.Sprite != null) spriteRefCount++;
+ if (item.SpineAsset != null) spineRefCount++;
+ if (!string.IsNullOrEmpty(item.SpritePath) || !string.IsNullOrEmpty(item.SpineAssetPath))
+ pathCount++;
+ }
+
+ Debug.Log($"SO: {table.TableName}\n" +
+ $" 文件大小: {fileSize / 1024f:F2} KB\n" +
+ $" 资源项数: {table.Items.Count}\n" +
+ $" Sprite引用: {spriteRefCount}\n" +
+ $" Spine引用: {spineRefCount}\n" +
+ $" 路径字段: {pathCount}");
+ }
+ }
+
+ Debug.Log("========== 测试1 完成 ==========");
+ EditorUtility.DisplayDialog("测试完成", "请查看Console日志了解SO文件大小和引用情况", "确定");
+ }
+
+ ///
+ /// 测试2: 对比加载时间
+ ///
+ private void TestLoadingTime()
+ {
+ Debug.Log("========== 测试2: 加载时间对比 ==========");
+
+ string[] guids = AssetDatabase.FindAssets("t:ArtTableSO");
+
+ if (guids.Length == 0)
+ {
+ Debug.LogWarning("未找到任何ArtTableSO");
+ return;
+ }
+
+ foreach (var guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+
+ // 卸载以确保测试准确
+ Resources.UnloadUnusedAssets();
+ System.GC.Collect();
+
+ // 测试加载时间
+ var sw = Stopwatch.StartNew();
+ var table = AssetDatabase.LoadAssetAtPath(path);
+ sw.Stop();
+
+ if (table != null)
+ {
+ // 统计引用情况
+ int refCount = 0;
+ foreach (var item in table.Items)
+ {
+ if (item.Sprite != null || item.SpineAsset != null)
+ refCount++;
+ }
+
+ Debug.Log($"SO: {table.TableName}\n" +
+ $" 加载耗时: {sw.Elapsed.TotalMilliseconds:F2} ms\n" +
+ $" 资源项数: {table.Items.Count}\n" +
+ $" 含引用项: {refCount}\n" +
+ $" 平均耗时: {sw.Elapsed.TotalMilliseconds / table.Items.Count:F3} ms/项");
+
+ // 尝试访问一个引用,看看是否触发额外加载
+ if (table.Items.Count > 0 && table.Items[0].Sprite != null)
+ {
+ var sw2 = Stopwatch.StartNew();
+ var sprite = table.Items[0].Sprite; // 访问引用
+ var name = sprite.name; // 访问属性
+ sw2.Stop();
+ Debug.Log($" 访问第1个Sprite引用: {sw2.Elapsed.TotalMilliseconds:F2} ms");
+ }
+ }
+ }
+
+ Debug.Log("========== 测试2 完成 ==========");
+ EditorUtility.DisplayDialog("测试完成", "请查看Console日志了解加载时间差异", "确定");
+ }
+
+ ///
+ /// 测试3: 检查内存占用
+ ///
+ private void TestMemoryUsage()
+ {
+ Debug.Log("========== 测试3: 内存占用测试 ==========");
+
+ // 清理内存
+ Resources.UnloadUnusedAssets();
+ System.GC.Collect();
+
+ long memBefore = System.GC.GetTotalMemory(true);
+ int textureBefore = Resources.FindObjectsOfTypeAll().Length;
+ int spriteBefore = Resources.FindObjectsOfTypeAll().Length;
+
+ Debug.Log($"加载前:\n" +
+ $" 托管内存: {memBefore / 1024f / 1024f:F2} MB\n" +
+ $" Texture2D数量: {textureBefore}\n" +
+ $" Sprite数量: {spriteBefore}");
+
+ // 加载所有SO
+ string[] guids = AssetDatabase.FindAssets("t:ArtTableSO");
+ var tables = new System.Collections.Generic.List();
+
+ foreach (var guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ var table = AssetDatabase.LoadAssetAtPath(path);
+ if (table != null)
+ {
+ tables.Add(table);
+ }
+ }
+
+ long memAfter = System.GC.GetTotalMemory(false);
+ int textureAfter = Resources.FindObjectsOfTypeAll().Length;
+ int spriteAfter = Resources.FindObjectsOfTypeAll().Length;
+
+ Debug.Log($"加载后:\n" +
+ $" 托管内存: {memAfter / 1024f / 1024f:F2} MB (+{(memAfter - memBefore) / 1024f / 1024f:F2} MB)\n" +
+ $" Texture2D数量: {textureAfter} (+{textureAfter - textureBefore})\n" +
+ $" Sprite数量: {spriteAfter} (+{spriteAfter - spriteBefore})");
+
+ Debug.Log($"共加载 {tables.Count} 个SO");
+
+ Debug.Log("========== 测试3 完成 ==========");
+
+ EditorUtility.DisplayDialog(
+ "测试完成",
+ $"内存增加: {(memAfter - memBefore) / 1024f / 1024f:F2} MB\n" +
+ $"Texture2D增加: {textureAfter - textureBefore}\n" +
+ $"Sprite增加: {spriteAfter - spriteBefore}\n\n" +
+ $"如果纹理/Sprite数量大幅增加,说明确实连带加载了引用资源",
+ "确定");
+ }
+ }
+}
diff --git a/Assets/Editor/Art_Tools/ArtResourcePathFiller.cs b/Assets/Editor/Art_Tools/ArtResourcePathFiller.cs
new file mode 100644
index 0000000..c948dc4
--- /dev/null
+++ b/Assets/Editor/Art_Tools/ArtResourcePathFiller.cs
@@ -0,0 +1,296 @@
+#if UNITY_EDITOR
+using UnityEngine;
+using UnityEditor;
+using ArtResource;
+using System.Linq;
+
+namespace ArtTools
+{
+ ///
+ /// 美术资源路径自动填充工具
+ ///
+ /// 功能:
+ /// 1. 在保存SO时自动填充SpritePath和SpineAssetPath
+ /// 2. 提供手动批量更新所有SO的工具
+ ///
+ /// 使用方法:
+ /// - 自动:在ArtTableSO的OnValidate或保存时调用FillResourcePaths
+ /// - 手动:菜单 -> 美术工具 -> 更新所有美术资源路径
+ ///
+ public static class ArtResourcePathFiller
+ {
+ ///
+ /// 自动填充单个SO的资源路径
+ /// 应该在ArtTableSO保存时调用
+ ///
+ public static void FillResourcePaths(ArtTableSO table)
+ {
+ if (table == null || table.Items == null)
+ return;
+
+ bool hasChanges = false;
+
+ foreach (var item in table.Items)
+ {
+ // 填充Sprite路径
+ if (item.Sprite != null)
+ {
+ string spritePath = AssetDatabase.GetAssetPath(item.Sprite);
+ if (!string.IsNullOrEmpty(spritePath) && item.SpritePath != spritePath)
+ {
+ item.SpritePath = spritePath;
+ hasChanges = true;
+ }
+ }
+ else
+ {
+ // 引用为空时,清空路径
+ if (!string.IsNullOrEmpty(item.SpritePath))
+ {
+ item.SpritePath = "";
+ hasChanges = true;
+ }
+ }
+
+ // 填充Spine路径
+ if (item.SpineAsset != null)
+ {
+ string spinePath = AssetDatabase.GetAssetPath(item.SpineAsset);
+ if (!string.IsNullOrEmpty(spinePath) && item.SpineAssetPath != spinePath)
+ {
+ item.SpineAssetPath = spinePath;
+ hasChanges = true;
+ }
+ }
+ else
+ {
+ // 引用为空时,清空路径
+ if (!string.IsNullOrEmpty(item.SpineAssetPath))
+ {
+ item.SpineAssetPath = "";
+ hasChanges = true;
+ }
+ }
+ }
+
+ if (hasChanges)
+ {
+ EditorUtility.SetDirty(table);
+ Debug.Log($"[ArtResourcePathFiller] 已更新资源路径: {table.TableName} ({table.TableId})");
+ }
+ }
+
+ ///
+ /// 手动批量更新所有ArtTableSO的资源路径
+ ///
+ [MenuItem("美术工具/更新所有美术资源路径")]
+ public static void UpdateAllArtTablePaths()
+ {
+ // 查找所有ArtTableSO
+ string[] guids = AssetDatabase.FindAssets("t:ArtTableSO");
+
+ if (guids.Length == 0)
+ {
+ Debug.LogWarning("[ArtResourcePathFiller] 未找到任何ArtTableSO文件");
+ return;
+ }
+
+ int updatedCount = 0;
+ int totalCount = guids.Length;
+
+ EditorUtility.DisplayProgressBar("更新美术资源路径", "正在扫描...", 0);
+
+ for (int i = 0; i < guids.Length; i++)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guids[i]);
+ var table = AssetDatabase.LoadAssetAtPath(path);
+
+ if (table != null)
+ {
+ EditorUtility.DisplayProgressBar(
+ "更新美术资源路径",
+ $"处理: {table.TableName} ({i + 1}/{totalCount})",
+ (float)i / totalCount
+ );
+
+ FillResourcePaths(table);
+ updatedCount++;
+ }
+ }
+
+ EditorUtility.ClearProgressBar();
+
+ // 保存所有修改
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+
+ Debug.Log($"[ArtResourcePathFiller] ✓ 批量更新完成: 处理了 {updatedCount} 个ArtTableSO");
+ EditorUtility.DisplayDialog(
+ "更新完成",
+ $"已更新 {updatedCount} 个美术资源表的路径信息\n\n现在可以在Runtime模式下正常加载资源了",
+ "确定"
+ );
+ }
+
+ ///
+ /// 验证所有SO的路径完整性
+ ///
+ [MenuItem("美术工具/验证美术资源路径完整性")]
+ public static void ValidateAllPaths()
+ {
+ string[] guids = AssetDatabase.FindAssets("t:ArtTableSO");
+
+ if (guids.Length == 0)
+ {
+ Debug.LogWarning("[ArtResourcePathFiller] 未找到任何ArtTableSO文件");
+ return;
+ }
+
+ int missingPathCount = 0;
+ int totalItemCount = 0;
+
+ foreach (string guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ var table = AssetDatabase.LoadAssetAtPath(path);
+
+ if (table == null || table.Items == null)
+ continue;
+
+ foreach (var item in table.Items)
+ {
+ totalItemCount++;
+
+ // 检查Sprite
+ if (item.Sprite != null && string.IsNullOrEmpty(item.SpritePath))
+ {
+ Debug.LogWarning($"[{table.TableName}] 资源项 '{item.Name}' 有Sprite引用但缺少路径");
+ missingPathCount++;
+ }
+
+ // 检查Spine
+ if (item.SpineAsset != null && string.IsNullOrEmpty(item.SpineAssetPath))
+ {
+ Debug.LogWarning($"[{table.TableName}] 资源项 '{item.Name}' 有Spine引用但缺少路径");
+ missingPathCount++;
+ }
+ }
+ }
+
+ if (missingPathCount == 0)
+ {
+ Debug.Log($"[ArtResourcePathFiller] ✓ 验证通过: 所有 {totalItemCount} 个资源项的路径都完整");
+ EditorUtility.DisplayDialog(
+ "验证通过",
+ $"所有 {totalItemCount} 个资源项的路径都完整\n\n可以正常使用Runtime模式",
+ "确定"
+ );
+ }
+ else
+ {
+ Debug.LogError($"[ArtResourcePathFiller] ✗ 发现 {missingPathCount} 个缺少路径的资源项");
+ EditorUtility.DisplayDialog(
+ "发现问题",
+ $"发现 {missingPathCount} 个缺少路径的资源项\n\n建议运行【美术工具 -> 更新所有美术资源路径】修复",
+ "确定"
+ );
+ }
+ }
+ }
+
+ ///
+ /// ArtTableSO的自定义Inspector
+ /// 在Inspector底部添加"更新路径"按钮
+ ///
+ [CustomEditor(typeof(ArtTableSO))]
+ public class ArtTableSOInspector : Editor
+ {
+ public override void OnInspectorGUI()
+ {
+ // 绘制默认Inspector
+ DrawDefaultInspector();
+
+ EditorGUILayout.Space(10);
+ EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
+ EditorGUILayout.Space(5);
+
+ var table = target as ArtTableSO;
+ if (table == null)
+ return;
+
+ // 统计信息
+ int spriteCount = table.Items.Count(item => item.Sprite != null);
+ int spineCount = table.Items.Count(item => item.SpineAsset != null);
+ int missingPathCount = table.Items.Count(item =>
+ (item.Sprite != null && string.IsNullOrEmpty(item.SpritePath)) ||
+ (item.SpineAsset != null && string.IsNullOrEmpty(item.SpineAssetPath))
+ );
+
+ EditorGUILayout.BeginVertical(EditorStyles.helpBox);
+ EditorGUILayout.LabelField("资源路径状态", EditorStyles.boldLabel);
+ EditorGUILayout.LabelField($"Sprite数量: {spriteCount}");
+ EditorGUILayout.LabelField($"Spine数量: {spineCount}");
+
+ if (missingPathCount > 0)
+ {
+ var oldColor = GUI.contentColor;
+ GUI.contentColor = Color.red;
+ EditorGUILayout.LabelField($"⚠️ 缺少路径: {missingPathCount}");
+ GUI.contentColor = oldColor;
+ }
+ else if (spriteCount > 0 || spineCount > 0)
+ {
+ var oldColor = GUI.contentColor;
+ GUI.contentColor = Color.green;
+ EditorGUILayout.LabelField("✓ 路径完整");
+ GUI.contentColor = oldColor;
+ }
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.Space(5);
+
+ // 更新路径按钮
+ if (GUILayout.Button("🔄 更新此表的资源路径", GUILayout.Height(30)))
+ {
+ ArtResourcePathFiller.FillResourcePaths(table);
+ EditorUtility.SetDirty(table);
+ AssetDatabase.SaveAssets();
+ EditorUtility.DisplayDialog("更新完成", $"已更新 {table.TableName} 的资源路径", "确定");
+ }
+
+ EditorGUILayout.Space(5);
+ EditorGUILayout.HelpBox(
+ "提示:保存SO时会自动更新路径,手动点击按钮可强制更新",
+ MessageType.Info
+ );
+ }
+ }
+
+ ///
+ /// 资源保存时自动填充路径
+ ///
+ public class ArtTableSOAssetPostprocessor : AssetPostprocessor
+ {
+ static void OnPostprocessAllAssets(
+ string[] importedAssets,
+ string[] deletedAssets,
+ string[] movedAssets,
+ string[] movedFromAssetPaths)
+ {
+ foreach (string assetPath in importedAssets)
+ {
+ // 只处理ArtTableSO
+ if (!assetPath.EndsWith(".asset"))
+ continue;
+
+ var table = AssetDatabase.LoadAssetAtPath(assetPath);
+ if (table != null)
+ {
+ ArtResourcePathFiller.FillResourcePaths(table);
+ }
+ }
+ }
+ }
+}
+#endif
diff --git a/Assets/Editor/Art_Tools/LoadMethodComparisonTest.cs b/Assets/Editor/Art_Tools/LoadMethodComparisonTest.cs
new file mode 100644
index 0000000..3d98f14
--- /dev/null
+++ b/Assets/Editor/Art_Tools/LoadMethodComparisonTest.cs
@@ -0,0 +1,131 @@
+using UnityEngine;
+using UnityEditor;
+using ArtResource;
+using System.Diagnostics;
+using GameFramework.Resource;
+using UnityGameFramework.Runtime;
+using Debug = UnityEngine.Debug;
+
+namespace CrazyMaple.Editor
+{
+ ///
+ /// 对比测试:AssetDatabase vs GameEntry.Resource.LoadAsset
+ ///
+ public class LoadMethodComparisonTest : EditorWindow
+ {
+ [MenuItem("美术工具/对比加载方式性能")]
+ public static void ShowWindow()
+ {
+ GetWindow("加载方式对比");
+ }
+
+ private void OnGUI()
+ {
+ GUILayout.Label("对比测试", EditorStyles.boldLabel);
+ GUILayout.Space(10);
+
+ EditorGUILayout.HelpBox(
+ "对比两种加载方式的性能差异:\n" +
+ "1. AssetDatabase.LoadAssetAtPath(Editor专用)\n" +
+ "2. GameEntry.Resource.LoadAsset(框架统一接口)",
+ MessageType.Info);
+
+ GUILayout.Space(10);
+
+ if (GUILayout.Button("运行对比测试", GUILayout.Height(40)))
+ {
+ RunComparisonTest();
+ }
+ }
+
+ private void RunComparisonTest()
+ {
+ Debug.Log("========== 加载方式性能对比 ==========");
+
+ string[] guids = AssetDatabase.FindAssets("t:ArtTableSO");
+
+ if (guids.Length == 0)
+ {
+ Debug.LogWarning("未找到任何ArtTableSO");
+ return;
+ }
+
+ // 清理缓存
+ Resources.UnloadUnusedAssets();
+ System.GC.Collect();
+
+ foreach (var guid in guids)
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+
+ Debug.Log($"\n--- 测试文件: {path} ---");
+
+ // 方法1: AssetDatabase(Editor专用,最直接)
+ Resources.UnloadUnusedAssets();
+ var sw1 = Stopwatch.StartNew();
+ var table1 = AssetDatabase.LoadAssetAtPath(path);
+ sw1.Stop();
+ Debug.Log($"[AssetDatabase] 加载耗时: {sw1.Elapsed.TotalMilliseconds:F2} ms");
+
+ // 方法2: GameEntry.Resource(框架接口,通过EditorResourceComponent)
+ Resources.UnloadUnusedAssets();
+
+ // 检查 GameEntry 是否已初始化
+ if (GameEntry.Resource == null)
+ {
+ Debug.LogWarning("[GameEntry.Resource] GameEntry未初始化,请先运行游戏或在Play模式下测试");
+ Debug.LogWarning("跳过GameEntry.Resource测试,只显示AssetDatabase结果");
+ continue;
+ }
+
+ var sw2 = Stopwatch.StartNew();
+
+ bool loadCompleted = false;
+ ArtTableSO table2 = null;
+
+ var callbacks = new LoadAssetCallbacks(
+ (assetName, asset, duration, userData) =>
+ {
+ sw2.Stop();
+ table2 = asset as ArtTableSO;
+ loadCompleted = true;
+ Debug.Log($"[GameEntry.Resource] 加载耗时: {sw2.Elapsed.TotalMilliseconds:F2} ms");
+ Debug.Log($" - Framework报告的duration: {duration:F4} 秒 ({duration * 1000:F2} ms)");
+ },
+ (assetName, status, errorMessage, userData) =>
+ {
+ sw2.Stop();
+ loadCompleted = true;
+ Debug.LogError($"[GameEntry.Resource] 加载失败: {errorMessage}");
+ }
+ );
+
+ GameEntry.Resource.LoadAsset(path, typeof(ArtTableSO), callbacks);
+
+ // 等待异步加载完成(Editor模式下实际是同步的,但需要等待回调)
+ int timeout = 0;
+ while (!loadCompleted && timeout < 1000)
+ {
+ System.Threading.Thread.Sleep(1);
+ timeout++;
+ }
+
+ if (!loadCompleted)
+ {
+ Debug.LogError("[GameEntry.Resource] 加载超时!");
+ }
+
+ // 对比
+ if (table1 != null && table2 != null)
+ {
+ float ratio = (float)sw2.Elapsed.TotalMilliseconds / (float)sw1.Elapsed.TotalMilliseconds;
+ Debug.Log($"性能差异: GameEntry.Resource 比 AssetDatabase 慢 {ratio:F1}x");
+ }
+ }
+
+ Debug.Log("\n========== 测试完成 ==========");
+ Debug.Log("结论: 如果GameEntry.Resource明显更慢,说明EditorResourceComponent有额外开销");
+ Debug.Log("建议: 真机/打包后测试,Runtime的AssetBundleResourceComponent性能可能完全不同");
+ }
+ }
+}
diff --git a/GitToolLog/提交并推送_20260120_172348.log b/GitToolLog/提交并推送_20260120_172348.log
new file mode 100644
index 0000000..8abbbb5
--- /dev/null
+++ b/GitToolLog/提交并推送_20260120_172348.log
@@ -0,0 +1,83 @@
+================================================================================
+操作类型: 提交并推送
+开始时间: 2026-01-20 17:23:48
+项目路径: E:/WorkSpace/MeowmentArt
+================================================================================
+
+[2026-01-20 17:23:48] [INFO] 日志: E:\WorkSpace\MeowmentArt\GitToolLog\提交并推送_20260120_172348.log
+[2026-01-20 17:23:48] [INFO] 提交信息: 美术仓库初始化
+[2026-01-20 17:23:48] [INFO] ============================================================
+[2026-01-20 17:23:48] [INFO] 开始提交Art_SubModule
+[2026-01-20 17:23:48] [INFO] ============================================================
+[2026-01-20 17:23:48] [INFO]
+步骤1: 检查改动
+[2026-01-20 17:23:48] [INFO] 执行: git status --short
+[2026-01-20 17:23:48] [INFO] D README.md
+ D README.md.meta
+ D "\346\265\213\350\257\2251.txt"
+ D "\346\265\213\350\257\2251.txt.meta"
+?? HeadResource.prefab
+?? HeadResource.prefab.meta
+?? Scripts.meta
+?? Scripts/
+?? UISprites.meta
+
+[2026-01-20 17:23:48] [INFO] 改动:
+ D README.md
+ D README.md.meta
+ D "\346\265\213\350\257\2251.txt"
+ D "\346\265\213\350\257\2251.txt.meta"
+?? HeadResource.prefab
+?? HeadResource.prefab.meta
+?? Scripts.meta
+?? Scripts/
+?? UISprites.meta
+
+[2026-01-20 17:23:48] [INFO]
+步骤2: 添加改动
+[2026-01-20 17:23:48] [INFO] 执行: git add .
+[2026-01-20 17:23:48] [WARNING] warning: in the working copy of 'HeadResource.prefab', LF will be replaced by CRLF the next time Git touches it
+warning: in the working copy of 'HeadResource.prefab.meta', LF will be replaced by CRLF the next time Git touches it
+warning: in the working copy of 'Scripts.meta', LF will be replaced by CRLF the next time Git touches it
+warning: in the working copy of 'Scripts/HeadResource.cs.meta', LF will be replaced by CRLF the next time Git touches it
+warning: in the working copy of 'UISprites.meta', LF will be replaced by CRLF the next time Git touches it
+
+[2026-01-20 17:23:48] [INFO]
+步骤3: 提交
+[2026-01-20 17:23:48] [INFO] 执行: git commit -m "美术仓库初始化"
+[2026-01-20 17:23:48] [INFO] [detached HEAD dcd133c] 美术仓库初始化
+ 8 files changed, 75 insertions(+), 4 deletions(-)
+ create mode 100644 HeadResource.prefab
+ rename README.md.meta => HeadResource.prefab.meta (62%)
+ delete mode 100644 README.md
+ rename "\346\265\213\350\257\2251.txt.meta" => Scripts.meta (57%)
+ create mode 100644 Scripts/HeadResource.cs
+ create mode 100644 Scripts/HeadResource.cs.meta
+ create mode 100644 UISprites.meta
+ delete mode 100644 "\346\265\213\350\257\2251.txt"
+
+[2026-01-20 17:23:48] [INFO]
+步骤4: Push前Pull
+[2026-01-20 17:23:48] [INFO] 执行: git pull
+[2026-01-20 17:23:49] [WARNING] You are not currently on a branch.
+Please specify which branch you want to merge with.
+See git-pull(1) for details.
+
+ git pull
+
+
+[2026-01-20 17:23:49] [ERROR] Pull失败-存在冲突!
+[2026-01-20 17:23:49] [ERROR] 解决方法:
+1.打开Art_SubModule目录
+2.解决冲突
+3.git add .
+4.git commit
+5.重新提交
+
+================================================================================
+结束时间: 2026-01-20 17:24:05
+执行结果: 失败
+================================================================================
+
+!!! 操作失败 !!!
+日志文件: E:\WorkSpace\MeowmentArt\GitToolLog\提交并推送_20260120_172348.log
diff --git a/GitToolLog/更新_20260120_171217.log b/GitToolLog/更新_20260120_171217.log
new file mode 100644
index 0000000..fb426a0
--- /dev/null
+++ b/GitToolLog/更新_20260120_171217.log
@@ -0,0 +1,27 @@
+================================================================================
+操作类型: 更新
+开始时间: 2026-01-20 17:12:17
+项目路径: E:/WorkSpace/MeowmentArt
+================================================================================
+
+[2026-01-20 17:12:17] [INFO] 日志: E:\WorkSpace\MeowmentArt\GitToolLog\更新_20260120_171217.log
+[2026-01-20 17:12:17] [INFO] ============================================================
+[2026-01-20 17:12:17] [INFO] 开始更新子模块
+[2026-01-20 17:12:17] [INFO] ============================================================
+[2026-01-20 17:12:17] [INFO] 执行: git submodule update --init --recursive --remote --merge
+[2026-01-20 17:12:19] [INFO] Updating 54a32bf..10480ba
+Fast-forward
+ README.md.meta | 7 +++++++
+ "\346\265\213\350\257\2251.txt.meta" | 7 +++++++
+ 2 files changed, 14 insertions(+)
+ create mode 100644 README.md.meta
+ create mode 100644 "\346\265\213\350\257\2251.txt.meta"
+Submodule path 'Assets/Art_SubModule': merged in '10480ba573cacd75c1496737614d2d0d26147533'
+
+[2026-01-20 17:12:19] [INFO]
+更新完成!
+
+================================================================================
+结束时间: 2026-01-20 17:12:20
+执行结果: 成功
+================================================================================
diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset
index c2fae0a..303fc1b 100644
--- a/ProjectSettings/ProjectSettings.asset
+++ b/ProjectSettings/ProjectSettings.asset
@@ -287,7 +287,99 @@ PlayerSettings:
AndroidValidateAppBundleSize: 1
AndroidAppBundleSizeToValidate: 150
m_BuildTargetIcons: []
- m_BuildTargetPlatformIcons: []
+ m_BuildTargetPlatformIcons:
+ - m_BuildTarget: Android
+ m_Icons:
+ - m_Textures: []
+ m_Width: 432
+ m_Height: 432
+ m_Kind: 2
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 324
+ m_Height: 324
+ m_Kind: 2
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 216
+ m_Height: 216
+ m_Kind: 2
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 162
+ m_Height: 162
+ m_Kind: 2
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 108
+ m_Height: 108
+ m_Kind: 2
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 81
+ m_Height: 81
+ m_Kind: 2
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 192
+ m_Height: 192
+ m_Kind: 1
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 144
+ m_Height: 144
+ m_Kind: 1
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 96
+ m_Height: 96
+ m_Kind: 1
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 72
+ m_Height: 72
+ m_Kind: 1
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 48
+ m_Height: 48
+ m_Kind: 1
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 36
+ m_Height: 36
+ m_Kind: 1
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 192
+ m_Height: 192
+ m_Kind: 0
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 144
+ m_Height: 144
+ m_Kind: 0
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 96
+ m_Height: 96
+ m_Kind: 0
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 72
+ m_Height: 72
+ m_Kind: 0
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 48
+ m_Height: 48
+ m_Kind: 0
+ m_SubKind:
+ - m_Textures: []
+ m_Width: 36
+ m_Height: 36
+ m_Kind: 0
+ m_SubKind:
m_BuildTargetBatching:
- m_BuildTarget: Standalone
m_StaticBatching: 1
diff --git a/artist_config.json b/artist_config.json
new file mode 100644
index 0000000..e09af50
--- /dev/null
+++ b/artist_config.json
@@ -0,0 +1,3 @@
+{
+ "project_path": "E:/WorkSpace/MeowmentArt"
+}
\ No newline at end of file
diff --git a/美术Git提交工具.exe b/美术Git提交工具.exe
new file mode 100644
index 0000000..9c06d17
Binary files /dev/null and b/美术Git提交工具.exe differ
diff --git a/美术Git操作指南.md b/美术Git操作指南.md
new file mode 100644
index 0000000..d77e0c3
--- /dev/null
+++ b/美术Git操作指南.md
@@ -0,0 +1,352 @@
+# 美术Git操作指南
+
+> 💡 **提示**:命令行操作最准确可靠。SourceTree因版本不同界面可能有差异,如遇找不到功能的情况请使用命令行。
+
+---
+
+## 📋 目录
+1. [首次拉取项目](#首次拉取项目)
+2. [日常提交流程](#日常提交流程)
+3. [避免冲突](#避免冲突)
+4. [解决冲突](#解决冲突)
+5. [自动同步SubModule](#自动同步submodule)
+
+---
+
+## 首次拉取项目
+
+### 命令行方式 ✅
+
+```powershell
+# 1. 克隆项目(自动包含SubModule)
+git clone --recursive git@gitea.bywaystudios.com:zhanghongbo/MeowmentArt.git
+
+# 2. 进入项目
+cd MeowmentArt
+
+# 3. 配置自动同步SubModule
+git config submodule.recurse true
+
+# 4. 配置用户信息
+git config user.name "LiBoyang"
+git config user.email "zhangsan@company.com"
+
+# 完成!可以用Unity打开项目了
+```
+
+### SourceTree方式
+
+```
+1. File → Clone / New
+
+2. 填写信息:
+ Source URL: git@gitea.bywaystudios.com:你的用户名/MeowmentArt.git
+ Destination Path: E:\WorkSpace\MeowmentArt
+ ✓ Recurse submodules ← 勾选这个!
+
+3. 点击 Clone
+
+4. 克隆完成后,Tools → Options → Git
+ 找到 Submodules 部分
+ ✓ 勾选 Update submodules when pulling
+```
+
+---
+
+## 日常提交流程
+
+### 命令行方式 ✅
+
+```powershell
+# === 第一步:在SubModule内提交(必须先做) ===
+cd Assets/Art_SubModule
+
+# 查看修改了哪些文件
+git status
+
+# 添加文件(可以选择性添加)
+git add Characters/hero.png # 添加单个文件
+git add Characters/ # 添加整个文件夹
+git add . # 添加所有修改
+
+# 提交前先拉取更新(避免冲突)
+git pull
+
+# 提交
+git commit -m "添加英雄角色贴图"
+
+# 推送到远程
+git push
+
+
+# === 第二步:回到主项目提交(记录SubModule更新) ===
+cd ../..
+
+# 查看状态(会看到SubModule有变化)
+git status
+
+# 添加SubModule的变化
+git add Assets/Art_SubModule
+
+# 提交主项目
+git commit -m "更新美术资源"
+
+# 推送
+git push
+```
+
+### SourceTree方式
+
+```
+=== 第一步:提交SubModule ===
+
+1. 在左侧 SUBMODULES 区域双击 "Assets/Art_SubModule"
+ (会打开新窗口显示SubModule)
+
+2. 在SubModule窗口:
+ - 点击 File Status
+ - 勾选要提交的文件
+ - 点击 Stage Selected(暂存所选)
+ - 底部输入提交信息:"添加英雄角色贴图"
+ - 点击 Commit
+ - 点击 Push
+
+=== 第二步:提交主项目 ===
+
+3. 回到主项目窗口:
+ - File Status 会显示 "Assets/Art_SubModule" 有变化
+ - 勾选这个变化
+ - 点击 Stage Selected
+ - 输入提交信息:"更新美术资源"
+ - 点击 Commit
+ - 点击 Push
+```
+
+**⚠️ 重要提示:**
+- 必须先push SubModule,再push主项目
+- 两个都要提交,缺一不可
+
+---
+
+## 避免冲突
+
+### 规则1:提交前先拉取 ⭐ 最重要
+
+```powershell
+# 养成习惯:每次提交前先执行
+cd Assets/Art_SubModule
+git pull # 先拉取别人的更新
+
+# 如果有冲突,现在解决比稍后解决简单
+# 然后再提交你的修改
+git add .
+git commit -m "..."
+git push
+```
+
+### 规则2:明确分工
+
+```
+建议分工方式:
+美术A → Characters/ 文件夹
+美术B → UI/ 文件夹
+美术C → Effects/ 文件夹
+
+各自只修改自己负责的文件夹,不会冲突!
+```
+
+### 规则3:提交前沟通
+
+```
+在团队群里:
+"我正在修改 characters/hero.png,大家别动这个文件"
+"收到!"
+
+→ 避免多人同时修改同一文件
+```
+
+### 规则4:经常提交
+
+```
+✅ 好习惯:完成一个小功能就提交
+❌ 坏习惯:攒几天的修改一次性提交
+
+经常提交 = 减少冲突范围 = 更容易解决
+```
+
+---
+
+## 解决冲突
+
+### 场景1:push时提示冲突
+
+#### 命令行方式 ✅
+
+```powershell
+cd Assets/Art_SubModule
+
+# 推送失败
+git push
+# ❌ error: failed to push some refs
+
+# 原因:远程有新提交
+# 解决方法:
+
+# 1. 先拉取远程更新
+git pull
+
+# 2. 如果自动合并成功
+# Merge made by... ← 看到这个说明成功
+git push # 再次推送即可
+
+# 3. 如果提示冲突
+# CONFLICT (content): Merge conflict in xxx.png
+# Auto-merging xxx failed
+
+# 4. 查看哪些文件冲突
+git status
+# both modified: Characters/hero.png ← 冲突的文件
+
+# 5A. 如果是二进制文件(图片、模型等)
+# 选择保留谁的版本:
+
+# 保留自己的版本
+git checkout --ours Characters/hero.png
+git add Characters/hero.png
+
+# 或者保留别人的版本
+git checkout --theirs Characters/hero.png
+git add Characters/hero.png
+
+# 5B. 如果是文本文件(配置文件等)
+# 打开文件手动编辑,删除冲突标记:
+# <<<<<<< HEAD
+# 你的内容
+# =======
+# 别人的内容
+# >>>>>>> xxx
+# 保留需要的内容,删除标记
+
+# 6. 标记冲突已解决
+git add .
+
+# 7. 完成合并
+git commit -m "解决冲突"
+
+# 8. 推送
+git push
+```
+
+#### SourceTree方式
+
+```
+1. Push失败后会提示需要Pull
+
+2. 点击 Pull 按钮
+
+3. 如果有冲突,会显示:
+ ⚠️ Conflicted files:
+ - Characters/hero.png
+
+4. 右键冲突文件:
+
+ 对于图片/模型等二进制文件:
+ - Resolve Conflicts → Use Mine (保留你的)
+ - Resolve Conflicts → Use Theirs (保留别人的)
+
+ 对于文本文件:
+ - Resolve Conflicts → Launch External Merge Tool
+ - 或直接编辑文件
+
+5. 解决后:
+ - 右键文件 → Mark Resolved
+ - 点击 Commit
+ - 输入 "解决冲突"
+ - Push
+```
+
+### 场景2:主项目SubModule引用冲突
+
+#### 命令行方式 ✅
+
+```powershell
+cd MeowmentArt # 主项目根目录
+
+git pull
+# CONFLICT (submodule): Merge conflict in Assets/Art_SubModule
+
+# 解决方法:
+
+# 1. 进入SubModule拉取最新
+cd Assets/Art_SubModule
+git pull
+cd ../..
+
+# 2. 标记已解决
+git add Assets/Art_SubModule
+
+# 3. 完成合并
+git commit -m "同步SubModule更新"
+
+# 4. 推送
+git push
+```
+
+---
+
+## 自动同步SubModule
+
+### 问题:pull时SubModule没更新
+
+**原因**:默认情况下 `git pull` 不会自动更新SubModule
+
+**解决方法**:
+
+#### 命令行方式 ✅
+
+```powershell
+cd MeowmentArt # 主项目根目录
+
+# 一次性配置,永久生效
+git config submodule.recurse true
+
+# 配置后,git pull 会自动同步SubModule
+git pull # 会自动更新SubModule了!
+
+# 验证配置
+git config --get submodule.recurse
+# 输出: true = 配置成功
+```
+
+#### SourceTree方式
+
+```
+方式1:全局设置
+1. Tools → Options → Git
+2. 找到 Submodules 部分
+3. ✓ 勾选 Update submodules when pulling
+
+方式2:Pull时手动勾选
+1. 每次点击 Pull 时
+2. 在弹出对话框中
+3. ✓ 勾选 Update submodules
+4. 点击 OK
+```
+
+### 手动更新SubModule到最新版本
+
+有时需要把SubModule更新到远程最新版本:
+
+```powershell
+# 更新SubModule到远程最新
+git submodule update --remote
+
+# 或在SourceTree中:
+# Repository → Update Submodules...
+```
+
+
+
+---
+
+**遇到问题随时联系客户端程序!**