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