Art_SubModule/Editor/Art_Tools/ArtResourceConfigEditor.cs
2026-02-04 16:43:33 +08:00

2237 lines
82 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using ArtResource;
using Spine.Unity;
using UnityEngine.U2D;
using Thrift;
using Thrift.Protocol;
using Thrift.Transport;
using Thrift.Transport.Client;
namespace EditorArt_Tools
{
/// <summary>
/// 美术资源配置编辑器
/// 提供三栏布局配置表预览、Item导航、详细编辑
/// </summary>
public class ArtResourceConfigEditor : EditorWindow
{
private const string SO_ROOT_PATH = "Assets/Art_SubModule/Art_SO";
private const string JSON_ROOT_PATH = "Assets/Art_SubModule/Art_Json";
private const string BYTES_ROOT_PATH = "Assets/Art_SubModule/Art_Bytes";
// ===== 数据 =====
private List<ArtTableSO> allTables = new List<ArtTableSO>();
private ArtTableSO selectedTable;
private ArtItemData selectedItem;
private Dictionary<string, bool> folderFoldouts = new Dictionary<string, bool>();
private Dictionary<int, bool> itemFoldouts = new Dictionary<int, bool>();
// 暂存区(用于编辑但未保存的数据)- 完全独立的数据副本
private List<ArtItemData> tempItemsList = new List<ArtItemData>();
private new bool hasUnsavedChanges = false;
// ID编辑临时缓存
private Dictionary<int, string> editingIdStrings = new Dictionary<int, string>();
private Dictionary<int, int> originalIds = new Dictionary<int, int>();
// 用于第二栏跳转到第三栏
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<Sprite, SpriteAtlasInfo?> spriteAtlasCache = new Dictionary<Sprite, SpriteAtlasInfo?>();
private struct SpriteAtlasInfo
{
public SpriteAtlas atlas;
public string atlasPath;
}
[MenuItem("美术工具/美术资源配置")]
public static void ShowWindow()
{
var window = GetWindow<ArtResourceConfigEditor>("美术资源配置");
window.minSize = new Vector2(1600, 800);
window.Show();
}
[MenuItem("美术工具/路径管理/修复丢失的引用")]
public static void FixMissingReferences()
{
FixAllMissingReferences();
}
[MenuItem("美术工具/路径管理/清理SO丢失文件")]
public static void CleanupMissingItems()
{
CleanupAllMissingItems();
}
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<UnityEngine.Object>(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<ArtTableSO>();
newTable.TableName = Path.GetFileNameWithoutExtension(path);
newTable.TableId = GenerateUniqueTableId();
newTable.Items = new List<ArtItemData>();
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<Sprite>(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,
SpritePath = tempItem.Sprite != null ? AssetDatabase.GetAssetPath(tempItem.Sprite) : "",
SpineAsset = tempItem.SpineAsset,
SpineAssetPath = tempItem.SpineAsset != null ? AssetDatabase.GetAssetPath(tempItem.SpineAsset) : "",
SpineAnimName = tempItem.SpineAnimName
};
selectedTable.Items.Add(newItem);
}
// 保存SO
EditorUtility.SetDirty(selectedTable);
AssetDatabase.SaveAssets();
// 同步JSON
SyncToJson(selectedTable);
// 同步Thrift Bytes新增
SyncToThriftBytes(selectedTable);
// 更新manifest文件
UpdateManifest();
// 生成合并的Bytes文件
GenerateMergedThriftBytes();
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_Json/art_table_manifest.json";
try
{
// 查找所有JSON文件从JSON目录扫描
string[] jsonFiles = Directory.GetFiles(JSON_ROOT_PATH, "*.json", SearchOption.AllDirectories);
List<string> tablePaths = new List<string>();
foreach (string fullPath in jsonFiles)
{
string relativePath = fullPath.Replace("\\", "/").Replace(Application.dataPath.Replace("/Assets", "").Replace("\\", "/") + "/", "");
// 排除manifest文件本身
if (relativePath.EndsWith("art_table_manifest.json"))
continue;
tablePaths.Add(relativePath);
}
// 排序路径
tablePaths.Sort();
// 创建或更新manifest
ArtTableManifest manifest;
// 如果文件存在,读取现有的预加载配置
if (File.Exists(MANIFEST_PATH))
{
string existingJson = File.ReadAllText(MANIFEST_PATH);
manifest = JsonUtility.FromJson<ArtTableManifest>(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<int> validTableIds = new List<int>();
foreach (string jsonPath in tablePaths)
{
try
{
string jsonContent = File.ReadAllText(jsonPath);
var jsonData = JsonUtility.FromJson<ArtTableJsonData>(jsonContent);
if (jsonData != null)
{
validTableIds.Add(jsonData.TableId);
}
}
catch
{
// 忽略无法解析的文件
}
}
// 过滤掉已删除表的ID
if (manifest.preloadTableIds != null && manifest.preloadTableIds.Length > 0)
{
var cleanedPreloadIds = manifest.preloadTableIds.Where(id => validTableIds.Contains(id)).ToArray();
int removedCount = manifest.preloadTableIds.Length - cleanedPreloadIds.Length;
manifest.preloadTableIds = cleanedPreloadIds;
if (removedCount > 0)
{
Debug.Log($"[ArtResourceConfigEditor] 从预加载配置中移除了 {removedCount} 个已删除表的ID");
}
}
// 序列化为JSON
string json = JsonUtility.ToJson(manifest, true);
// 确保目录存在
string directory = Path.GetDirectoryName(MANIFEST_PATH);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 写入文件
File.WriteAllText(MANIFEST_PATH, json);
AssetDatabase.ImportAsset(MANIFEST_PATH);
Debug.Log($"[ArtResourceConfigEditor] Manifest文件已更新: {MANIFEST_PATH}, 共 {tablePaths.Count} 个JSON表");
}
catch (System.Exception ex)
{
Debug.LogError($"[ArtResourceConfigEditor] 更新Manifest文件失败: {ex.Message}");
}
}
/// <summary>
/// 将单个表同步到Thrift Bytes格式单表文件暂不使用
/// </summary>
private void SyncToThriftBytes(ArtTableSO table)
{
// 注意这个方法生成单表bytes但我们实际使用的是GenerateMergedThriftBytes()
// 保留此方法以防将来需要单表加载
Debug.Log($"[ArtResourceConfigEditor] 跳过单表Bytes生成: {table.TableName}(使用合并模式)");
}
/// <summary>
/// 生成合并的Thrift Bytes文件所有表合并
/// 参考 ConfigManager 的加载方式
/// </summary>
private void GenerateMergedThriftBytes()
{
try
{
var startTime = System.Diagnostics.Stopwatch.StartNew();
// 确保输出目录存在
if (!Directory.Exists(BYTES_ROOT_PATH))
{
Directory.CreateDirectory(BYTES_ROOT_PATH);
}
string outputPath = Path.Combine(BYTES_ROOT_PATH, "ArtResourceConfig.bytes");
// 加载所有SO表
string[] allSOGuids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { SO_ROOT_PATH });
List<ArtTableSO> allSOTables = new List<ArtTableSO>();
foreach (string guid in allSOGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
ArtTableSO table = AssetDatabase.LoadAssetAtPath<ArtTableSO>(path);
if (table != null)
{
allSOTables.Add(table);
}
}
// 创建Thrift数据结构
var thriftConfig = new Byway.Thrift.Data.ArtResourceConfig
{
Tables = new List<Byway.Thrift.Data.ArtTable>(),
Version = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
GenerateTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
};
// 转换所有表
foreach (var soTable in allSOTables)
{
var thriftTable = new Byway.Thrift.Data.ArtTable
{
TableId = soTable.TableId,
TableName = soTable.TableName,
Items = new List<Byway.Thrift.Data.ArtItem>()
};
foreach (var soItem in soTable.Items)
{
var thriftItem = new Byway.Thrift.Data.ArtItem
{
Id = soItem.Id,
Name = soItem.Name ?? "",
Desc = soItem.Desc ?? "",
SpritePath = soItem.Sprite != null ? AssetDatabase.GetAssetPath(soItem.Sprite) : "",
SpineAssetPath = soItem.SpineAsset != null ? AssetDatabase.GetAssetPath(soItem.SpineAsset) : "",
SpineAnimName = soItem.SpineAnimName ?? ""
};
thriftTable.Items.Add(thriftItem);
}
thriftConfig.Tables.Add(thriftTable);
}
// 加载预加载配置从manifest读取
const string MANIFEST_PATH = "Assets/Art_SubModule/Art_Json/art_table_manifest.json";
if (File.Exists(MANIFEST_PATH))
{
string manifestJson = File.ReadAllText(MANIFEST_PATH);
var manifest = JsonUtility.FromJson<ArtTableManifest>(manifestJson);
if (manifest != null && manifest.preloadTableIds != null)
{
thriftConfig.PreloadTableIds = manifest.preloadTableIds.ToList();
}
}
// 序列化到bytes
byte[] bytesData;
using (var memoryStream = new MemoryStream())
{
using (var transport = new TStreamTransport(null, memoryStream, new TConfiguration()))
{
using (var protocol = new TBinaryProtocol(transport))
{
thriftConfig.WriteAsync(protocol, System.Threading.CancellationToken.None).GetAwaiter().GetResult();
}
}
bytesData = memoryStream.ToArray();
}
// 写入文件
File.WriteAllBytes(outputPath, bytesData);
AssetDatabase.ImportAsset(outputPath);
startTime.Stop();
Debug.Log($"[ArtResourceConfigEditor] ✅ Thrift Bytes生成成功\n" +
$" 路径: {outputPath}\n" +
$" 表数量: {thriftConfig.Tables.Count}\n" +
$" 文件大小: {bytesData.Length / 1024f:F2} KB\n" +
$" 耗时: {startTime.ElapsedMilliseconds} ms");
}
catch (System.Exception ex)
{
Debug.LogError($"[ArtResourceConfigEditor] ❌ 生成Thrift Bytes失败: {ex.Message}\n{ex.StackTrace}");
}
}
/// <summary>
/// 静态版本生成合并的Thrift Bytes文件供外部工具调用
/// </summary>
public static void GenerateMergedThriftBytesStatic()
{
try
{
var startTime = System.Diagnostics.Stopwatch.StartNew();
// 确保输出目录存在
const string BYTES_ROOT_PATH = "Assets/Art_SubModule/Art_Bytes";
if (!Directory.Exists(BYTES_ROOT_PATH))
{
Directory.CreateDirectory(BYTES_ROOT_PATH);
}
string outputPath = Path.Combine(BYTES_ROOT_PATH, "ArtResourceConfig.bytes");
// 加载所有SO表
const string SO_ROOT_PATH = "Assets/Art_SubModule/Art_SO";
string[] allSOGuids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { SO_ROOT_PATH });
List<ArtTableSO> allSOTables = new List<ArtTableSO>();
foreach (string guid in allSOGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
ArtTableSO table = AssetDatabase.LoadAssetAtPath<ArtTableSO>(path);
if (table != null)
{
allSOTables.Add(table);
}
}
// 创建Thrift数据结构
var thriftConfig = new Byway.Thrift.Data.ArtResourceConfig
{
Tables = new List<Byway.Thrift.Data.ArtTable>(),
Version = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
GenerateTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
};
// 转换所有表
foreach (var soTable in allSOTables)
{
var thriftTable = new Byway.Thrift.Data.ArtTable
{
TableId = soTable.TableId,
TableName = soTable.TableName,
Items = new List<Byway.Thrift.Data.ArtItem>()
};
foreach (var soItem in soTable.Items)
{
var thriftItem = new Byway.Thrift.Data.ArtItem
{
Id = soItem.Id,
Name = soItem.Name ?? "",
Desc = soItem.Desc ?? "",
SpritePath = soItem.Sprite != null ? AssetDatabase.GetAssetPath(soItem.Sprite) : "",
SpineAssetPath = soItem.SpineAsset != null ? AssetDatabase.GetAssetPath(soItem.SpineAsset) : "",
SpineAnimName = soItem.SpineAnimName ?? ""
};
thriftTable.Items.Add(thriftItem);
}
thriftConfig.Tables.Add(thriftTable);
}
// 加载预加载配置从manifest读取
const string MANIFEST_PATH = "Assets/Art_SubModule/Art_Json/art_table_manifest.json";
if (File.Exists(MANIFEST_PATH))
{
string manifestJson = File.ReadAllText(MANIFEST_PATH);
var manifest = JsonUtility.FromJson<ArtTableManifest>(manifestJson);
if (manifest != null && manifest.preloadTableIds != null)
{
thriftConfig.PreloadTableIds = manifest.preloadTableIds.ToList();
}
}
// 序列化到bytes
byte[] bytesData;
using (var memoryStream = new MemoryStream())
{
using (var transport = new TStreamTransport(null, memoryStream, new TConfiguration()))
{
using (var protocol = new TBinaryProtocol(transport))
{
thriftConfig.WriteAsync(protocol, System.Threading.CancellationToken.None).GetAwaiter().GetResult();
}
}
bytesData = memoryStream.ToArray();
}
// 写入文件
File.WriteAllBytes(outputPath, bytesData);
AssetDatabase.ImportAsset(outputPath);
startTime.Stop();
Debug.Log($"[ArtResourceConfigEditor] ✅ Thrift Bytes生成成功\n" +
$" 路径: {outputPath}\n" +
$" 表数量: {thriftConfig.Tables.Count}\n" +
$" 文件大小: {bytesData.Length / 1024f:F2} KB\n" +
$" 耗时: {startTime.ElapsedMilliseconds} ms");
}
catch (System.Exception ex)
{
Debug.LogError($"[ArtResourceConfigEditor] ❌ 生成Thrift Bytes失败: {ex.Message}\n{ex.StackTrace}");
}
}
[System.Serializable]
public class ArtTableManifest
{
public string[] tablePaths;
public int[] preloadTableIds;
}
[System.Serializable]
public class ArtTableJsonData
{
public int TableId;
public string TableName;
public List<ArtItemJsonData> 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<SpriteAtlas>(atlasPath);
if (atlas != null && atlas.CanBindTo(sprite))
{
var info = new SpriteAtlasInfo
{
atlas = atlas,
atlasPath = atlasPath
};
spriteAtlasCache[sprite] = info;
return info;
}
}
spriteAtlasCache[sprite] = null;
return null;
}
#endregion
#region
/// <summary>
/// 修复所有配置表中丢失的引用
/// 按照路径来找到引用(与"更新所有美术资源路径"相反)
/// </summary>
private static void FixAllMissingReferences()
{
if (!EditorUtility.DisplayDialog("修复丢失的引用",
"此操作将遍历所有配置表,根据路径信息修复丢失的引用。\n" +
"适用于跨项目拉取代码后引用丢失但路径正确的情况。\n\n" +
"确定要继续吗?",
"确定", "取消"))
{
return;
}
try
{
EditorUtility.DisplayProgressBar("修复丢失的引用", "正在查找配置表...", 0f);
// 查找所有ArtTableSO文件
string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { SO_ROOT_PATH });
if (guids.Length == 0)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("提示", "未找到任何配置表。", "确定");
return;
}
int totalFixed = 0;
int totalTables = 0;
List<string> errorMessages = new List<string>();
List<string> processedTables = new List<string>();
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
ArtTableSO table = AssetDatabase.LoadAssetAtPath<ArtTableSO>(path);
if (table == null) continue;
EditorUtility.DisplayProgressBar("修复丢失的引用",
$"正在处理: {table.TableName} ({i + 1}/{guids.Length})",
(float)i / guids.Length);
int fixedInTable = FixTableReferences(table, errorMessages);
if (fixedInTable > 0)
{
totalFixed += fixedInTable;
totalTables++;
processedTables.Add($"{table.TableName}: {fixedInTable}个");
// 保存修改
EditorUtility.SetDirty(table);
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
// 显示结果
string message = $"修复完成!\n\n" +
$"处理的配置表: {guids.Length}个\n" +
$"修复引用的配置表: {totalTables}个\n" +
$"总共修复的引用: {totalFixed}个";
if (processedTables.Count > 0)
{
message += "\n\n修复详情:\n" + string.Join("\n", processedTables);
}
if (errorMessages.Count > 0)
{
message += "\n\n⚠ 以下问题需要注意:\n" + string.Join("\n", errorMessages);
}
EditorUtility.DisplayDialog(totalFixed > 0 ? "修复成功" : "无需修复", message, "确定");
Debug.Log($"[修复丢失引用] {message}");
}
catch (System.Exception ex)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("错误", $"修复过程中出现错误:\n{ex.Message}", "确定");
Debug.LogError($"[修复丢失引用] 错误: {ex.Message}\n{ex.StackTrace}");
}
}
/// <summary>
/// 修复单个配置表的引用
/// </summary>
private static int FixTableReferences(ArtTableSO table, List<string> errorMessages)
{
int fixedCount = 0;
foreach (var item in table.Items)
{
// 修复Sprite引用
if (item.Sprite == null && !string.IsNullOrEmpty(item.SpritePath))
{
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(item.SpritePath);
if (sprite != null)
{
item.Sprite = sprite;
fixedCount++;
Debug.Log($"[修复引用] {table.TableName}/{item.Name}: 修复了Sprite引用 ({item.SpritePath})");
}
else
{
string error = $"[{table.TableName}/{item.Name}] 找不到Sprite: {item.SpritePath}";
errorMessages.Add(error);
Debug.LogWarning(error);
}
}
// 修复SpineAsset引用
if (item.SpineAsset == null && !string.IsNullOrEmpty(item.SpineAssetPath))
{
SkeletonDataAsset spineAsset = AssetDatabase.LoadAssetAtPath<SkeletonDataAsset>(item.SpineAssetPath);
if (spineAsset != null)
{
item.SpineAsset = spineAsset;
fixedCount++;
Debug.Log($"[修复引用] {table.TableName}/{item.Name}: 修复了SpineAsset引用 ({item.SpineAssetPath})");
}
else
{
string error = $"[{table.TableName}/{item.Name}] 找不到SpineAsset: {item.SpineAssetPath}";
errorMessages.Add(error);
Debug.LogWarning(error);
}
}
// 检查既没有引用又没有路径的情况
if (item.Sprite == null && string.IsNullOrEmpty(item.SpritePath) &&
item.SpineAsset == null && string.IsNullOrEmpty(item.SpineAssetPath))
{
string error = $"[{table.TableName}/{item.Name}] 既没有引用也没有路径,需要手动配置";
errorMessages.Add(error);
Debug.LogWarning(error);
}
}
return fixedCount;
}
/// <summary>
/// 清理所有配置表中引用丢失的Item
/// </summary>
private static void CleanupAllMissingItems()
{
if (!EditorUtility.DisplayDialog("清理丢失文件",
"此操作将删除所有配置表中引用丢失的资源项。\n" +
"同时会同步更新对应的JSON文件。\n\n" +
"⚠️ 此操作不可恢复!\n\n" +
"确定要继续吗?",
"确定", "取消"))
{
return;
}
try
{
EditorUtility.DisplayProgressBar("清理丢失文件", "正在查找配置表...", 0f);
// 查找所有ArtTableSO文件
string[] guids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { SO_ROOT_PATH });
if (guids.Length == 0)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("提示", "未找到任何配置表。", "确定");
return;
}
int totalRemoved = 0;
int totalTables = 0;
List<string> cleanedTables = new List<string>();
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
ArtTableSO table = AssetDatabase.LoadAssetAtPath<ArtTableSO>(path);
if (table == null) continue;
EditorUtility.DisplayProgressBar("清理丢失文件",
$"正在处理: {table.TableName} ({i + 1}/{guids.Length})",
(float)i / guids.Length);
int removedInTable = CleanupTableMissingItems(table);
if (removedInTable > 0)
{
totalRemoved += removedInTable;
totalTables++;
cleanedTables.Add($"{table.TableName}: 移除{removedInTable}个");
// 保存修改
EditorUtility.SetDirty(table);
// 同步到JSON
SyncTableToJson(table);
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
// 显示结果
string message = $"清理完成!\n\n" +
$"处理的配置表: {guids.Length}个\n" +
$"清理的配置表: {totalTables}个\n" +
$"总共移除的资源项: {totalRemoved}个";
if (cleanedTables.Count > 0)
{
message += "\n\n清理详情:\n" + string.Join("\n", cleanedTables);
}
EditorUtility.DisplayDialog(totalRemoved > 0 ? "清理成功" : "无需清理", message, "确定");
Debug.Log($"[清理丢失文件] {message}");
}
catch (System.Exception ex)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("错误", $"清理过程中出现错误:\n{ex.Message}", "确定");
Debug.LogError($"[清理丢失文件] 错误: {ex.Message}\n{ex.StackTrace}");
}
}
/// <summary>
/// 清理单个配置表中引用丢失的Item
/// </summary>
private static int CleanupTableMissingItems(ArtTableSO table)
{
List<ArtItemData> itemsToRemove = new List<ArtItemData>();
foreach (var item in table.Items)
{
bool hasMissingReference = false;
string missingInfo = "";
// 检查Sprite引用是否丢失
if (!string.IsNullOrEmpty(item.SpritePath) && item.Sprite == null)
{
hasMissingReference = true;
missingInfo += $" Sprite({item.SpritePath})";
}
// 检查SpineAsset引用是否丢失
if (!string.IsNullOrEmpty(item.SpineAssetPath) && item.SpineAsset == null)
{
hasMissingReference = true;
missingInfo += $" SpineAsset({item.SpineAssetPath})";
}
if (hasMissingReference)
{
itemsToRemove.Add(item);
Debug.Log($"[清理丢失文件] {table.TableName}/{item.Name}: 标记为删除 - 丢失:{missingInfo}");
}
}
// 移除标记的Item
foreach (var item in itemsToRemove)
{
table.Items.Remove(item);
}
return itemsToRemove.Count;
}
/// <summary>
/// 同步单个配置表到JSON静态版本
/// </summary>
private static void SyncTableToJson(ArtTableSO table)
{
string soPath = AssetDatabase.GetAssetPath(table);
string relativePath = soPath.Replace(SO_ROOT_PATH, "").Replace(".asset", ".json");
string jsonPath = JSON_ROOT_PATH + relativePath;
// 确保目录存在
string directory = Path.GetDirectoryName(jsonPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 创建JSON数据
var jsonData = new ArtTableJsonData
{
TableId = table.TableId,
TableName = table.TableName,
Items = table.Items.Select(item => new ArtItemJsonData
{
Id = item.Id,
Name = item.Name,
Desc = item.Desc,
SpritePath = item.Sprite != null ? AssetDatabase.GetAssetPath(item.Sprite) : "",
SpineAssetPath = item.SpineAsset != null ? AssetDatabase.GetAssetPath(item.SpineAsset) : "",
SpineAnimName = item.SpineAnimName
}).ToList()
};
string json = JsonUtility.ToJson(jsonData, true);
File.WriteAllText(jsonPath, json);
AssetDatabase.Refresh();
Debug.Log($"[同步JSON] {jsonPath}");
}
#endregion
}
}