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... +``` + + + +--- + +**遇到问题随时联系客户端程序!**