using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; using OfficeOpenXml; using ArtResource; namespace DesignTools.Scene { /// /// 发射器插图名称和资源设置工具 /// 读取 Docs/config/NetAssetData.xlsx 的 LevelLauncherData Sheet /// 关联 ArtTableSO "LauncherMergeStoryPic" 提供预览图选择 /// public class LevelLauncherDataEditor : BaseDesignToolEditor { #region 常量 private const string EXCEL_NAME = "NetAssetData.xlsx"; private const string SHEET_NAME = "LevelLauncherData"; private const string RESOURCES_PATH_PREFIX = "Assets/Art_SubModule/GameMain/UI/UISprites/Area/"; private const string PIC_TABLE_NAME = "LauncherMergeStoryPic"; #endregion #region 数据 private List dataList = new List(); private ArtTableSO picTableSO; private Dictionary spriteCache = new Dictionary(); private Dictionary textureCache = new Dictionary(); #endregion #region 打开窗口 [MenuItem("策划工具/场景/发射器插图名称和资源设置")] public static void ShowWindow() { var window = GetWindow("发射器插图名称和资源设置"); window.minSize = window.GetMinWindowSize(); window.Show(); } #endregion #region BaseDesignToolEditor 实现 protected override string GetDocsPathPrefKey() => "LevelLauncherDataEditor_DocsPath"; protected override string GetWindowTitle() => "发射器插图名称和资源设置"; protected override Vector2 GetMinWindowSize() => new Vector2(1200, 600); protected override void LoadConfigData() { spriteCache.Clear(); textureCache.Clear(); LoadPicTableSO(); LoadExcelData(); } protected override void DrawDataEditor() { if (picTableSO == null) { EditorGUILayout.HelpBox( $"未找到 ArtTableSO \"{PIC_TABLE_NAME}\",预览图选择功能不可用。\n请先在美术资源工具中创建名为 \"{PIC_TABLE_NAME}\" 的资源表。", MessageType.Warning); } EditorGUILayout.BeginVertical("box"); // 标题 + 新增按钮 EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"发射器数据列表(共 {dataList.Count} 条)", EditorStyles.boldLabel); GUILayout.FlexibleSpace(); GUI.backgroundColor = Color.cyan; if (GUILayout.Button("+ 新增行", GUILayout.Height(25), GUILayout.Width(100))) { AddNewRow(); } GUI.backgroundColor = Color.white; EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); // 表头 DrawTableHeader(); // 数据行 scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); LauncherRowData toDelete = null; foreach (var data in dataList) { DrawDataRow(data, ref toDelete); } if (toDelete != null) { dataList.Remove(toDelete); } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } protected override void SaveDataToExcel() { try { string excelPath = GetDocsConfigFilePath(EXCEL_NAME); if (!File.Exists(excelPath)) { EditorUtility.DisplayDialog("错误", $"文件不存在: {excelPath}", "确定"); return; } using (var package = new ExcelPackage(new FileInfo(excelPath))) { var ws = package.Workbook.Worksheets[SHEET_NAME]; if (ws == null) { EditorUtility.DisplayDialog("错误", $"未找到 Sheet: {SHEET_NAME}", "确定"); return; } // 查找列索引 int idCol = -1, lvCol = -1, langKeyCol = -1, picCol = -1, resPathCol = -1; int colCount = ws.Dimension?.Columns ?? 0; for (int c = 1; c <= colCount; c++) { string h = ws.Cells[1, c].Text; switch (h) { case "Id": idCol = c; break; case "Lv": lvCol = c; break; case "LanguageKey": langKeyCol = c; break; case "Picture": picCol = c; break; case "ResourcesPath": resPathCol = c; break; } } if (idCol < 0) { EditorUtility.DisplayDialog("错误", "表结构不正确,缺少 Id 列", "确定"); return; } // 清除现有数据行(从底部向上删除,保留前两行表头) int existingRows = ws.Dimension?.Rows ?? 2; for (int row = existingRows; row >= 3; row--) { ws.DeleteRow(row); } // 写入所有数据 for (int i = 0; i < dataList.Count; i++) { int row = i + 3; var data = dataList[i]; ws.Cells[row, idCol].Value = data.Id; if (lvCol > 0) ws.Cells[row, lvCol].Value = data.Lv; if (langKeyCol > 0) ws.Cells[row, langKeyCol].Value = GetLanguageKey(data.Lv); if (picCol > 0) ws.Cells[row, picCol].Value = data.Picture ?? ""; if (resPathCol > 0) ws.Cells[row, resPathCol].Value = GetResourcesPath(data.Picture); } package.Save(); } EditorUtility.DisplayDialog("保存成功", "配置已保存到Excel文件!\n\n" + "⚠️ 重要提醒:\n" + "请及时在SourceTree或GitHubDesktop中:\n" + "1. 提交(Commit)本次修改\n" + "2. 推送(Push)到远程仓库\n\n" + "避免与其他策划产生冲突!", "我知道了"); } catch (Exception e) { EditorUtility.DisplayDialog("错误", $"保存失败: {e.Message}\n{e.StackTrace}", "确定"); } } #endregion #region 数据加载 private void LoadPicTableSO() { picTableSO = null; string[] guids = AssetDatabase.FindAssets("t:ArtTableSO"); foreach (var guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); var so = AssetDatabase.LoadAssetAtPath(path); if (so != null && so.TableName == PIC_TABLE_NAME) { picTableSO = so; break; } } if (picTableSO == null) { Debug.LogWarning($"未找到 ArtTableSO: {PIC_TABLE_NAME},预览图选择功能将不可用"); } } private void LoadExcelData() { dataList.Clear(); string excelPath = GetDocsConfigFilePath(EXCEL_NAME); if (!File.Exists(excelPath)) { throw new Exception($"未找到文件: {excelPath}"); } using (var package = new ExcelPackage(new FileInfo(excelPath))) { var ws = package.Workbook.Worksheets[SHEET_NAME]; if (ws == null) { throw new Exception($"未找到 Sheet: {SHEET_NAME}"); } // 查找列 int idCol = -1, lvCol = -1, picCol = -1; int colCount = ws.Dimension?.Columns ?? 0; for (int c = 1; c <= colCount; c++) { string h = ws.Cells[1, c].Text; if (h == "Id") idCol = c; else if (h == "Lv") lvCol = c; else if (h == "Picture") picCol = c; } if (idCol < 0) { throw new Exception("表结构不正确,缺少 Id 列"); } int rowCount = ws.Dimension?.Rows ?? 0; for (int row = 3; row <= rowCount; row++) { string idText = ws.Cells[row, idCol].Text; if (string.IsNullOrEmpty(idText)) continue; if (!int.TryParse(idText, out int id)) continue; int lv = 0; if (lvCol > 0) { int.TryParse(ws.Cells[row, lvCol].Text, out lv); } dataList.Add(new LauncherRowData { Id = id, Lv = lv, Picture = picCol > 0 ? (ws.Cells[row, picCol].Text ?? "") : "", }); } } } #endregion #region UI 绘制 private void DrawTableHeader() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); EditorGUILayout.LabelField("Id", EditorStyles.miniLabel, GUILayout.Width(35)); EditorGUILayout.LabelField("Lv", EditorStyles.miniLabel, GUILayout.Width(50)); EditorGUILayout.LabelField("LanguageKey", EditorStyles.miniLabel, GUILayout.Width(260)); EditorGUILayout.LabelField("Picture", EditorStyles.miniLabel, GUILayout.Width(188)); EditorGUILayout.LabelField("预览", EditorStyles.miniLabel, GUILayout.Width(55)); EditorGUILayout.LabelField("ResourcesPath", EditorStyles.miniLabel); EditorGUILayout.LabelField("", GUILayout.Width(60)); EditorGUILayout.EndHorizontal(); } private void DrawDataRow(LauncherRowData data, ref LauncherRowData toDelete) { EditorGUILayout.BeginHorizontal(); // Id(只读) EditorGUILayout.LabelField(data.Id.ToString(), GUILayout.Width(35)); // Lv(可编辑) data.Lv = EditorGUILayout.IntField(data.Lv, GUILayout.Width(50)); // LanguageKey(自动,只读) EditorGUILayout.LabelField(GetLanguageKey(data.Lv), GUILayout.Width(260)); // Picture + 设置按钮 EditorGUILayout.BeginHorizontal(GUILayout.Width(188)); data.Picture = EditorGUILayout.TextField(data.Picture, GUILayout.Width(120)); GUI.enabled = picTableSO != null; if (GUILayout.Button("设置预览图", EditorStyles.miniButton, GUILayout.Width(64))) { var capturedData = data; LauncherPicPickerWindow.Show(picTableSO, "选择发射器预览图", name => { capturedData.Picture = name; spriteCache.Remove(name); textureCache.Remove(name); Repaint(); }); } GUI.enabled = true; EditorGUILayout.EndHorizontal(); // Picture 预览 DrawSpritePreview(data.Picture, 50); // ResourcesPath(自动,只读) EditorGUILayout.LabelField(GetResourcesPath(data.Picture), EditorStyles.miniLabel); // 删除按钮 GUI.backgroundColor = Color.red; if (GUILayout.Button("删除", GUILayout.Width(60))) { if (EditorUtility.DisplayDialog("确认删除", $"确定要删除 Id={data.Id} (Lv={data.Lv}) 的行吗?", "删除", "取消")) { toDelete = data; } } GUI.backgroundColor = Color.white; EditorGUILayout.EndHorizontal(); } private void DrawSpritePreview(string spriteName, float size) { Rect previewRect = GUILayoutUtility.GetRect(size, size, GUILayout.Width(size), GUILayout.Height(size)); if (string.IsNullOrEmpty(spriteName)) { EditorGUI.DrawRect(previewRect, new Color(0.3f, 0.3f, 0.3f, 1f)); return; } // 优先从 ArtTableSO 获取 Sprite Sprite sprite = GetCachedSprite(spriteName); if (sprite != null && sprite.texture != null) { EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f)); 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 = previewRect; if (aspect > 1) { float newHeight = previewRect.height / aspect; drawRect.y += (previewRect.height - newHeight) * 0.5f; drawRect.height = newHeight; } else { float newWidth = previewRect.width * aspect; drawRect.x += (previewRect.width - newWidth) * 0.5f; drawRect.width = newWidth; } GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords); return; } // 回退:尝试直接从路径加载 Texture2D Texture2D fallbackTex = GetCachedTexture(spriteName); if (fallbackTex != null) { EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f)); GUI.DrawTexture(previewRect, fallbackTex, ScaleMode.ScaleToFit); } else { EditorGUI.DrawRect(previewRect, new Color(0.3f, 0.3f, 0.3f, 1f)); } } #endregion #region 数据操作 private void AddNewRow() { int newId = 1; if (dataList.Count > 0) { newId = dataList.Max(x => x.Id) + 1; } dataList.Add(new LauncherRowData { Id = newId, Lv = 0, Picture = "", }); scrollPosition = new Vector2(0, float.MaxValue); } #endregion #region 自动计算 private static string GetLanguageKey(int lv) => $"UI_MainLvPanel_chapterTip_lv_{lv}"; private static string GetResourcesPath(string picture) { if (string.IsNullOrEmpty(picture)) return ""; return $"{RESOURCES_PATH_PREFIX}{picture}.png"; } #endregion #region Sprite 缓存 private Sprite GetCachedSprite(string name) { if (string.IsNullOrEmpty(name)) return null; if (spriteCache.ContainsKey(name)) return spriteCache[name]; Sprite result = null; if (picTableSO != null) { var item = picTableSO.Items.FirstOrDefault(x => x.Name == name); if (item != null) result = item.Sprite; } spriteCache[name] = result; return result; } private Texture2D GetCachedTexture(string name) { if (string.IsNullOrEmpty(name)) return null; if (textureCache.ContainsKey(name)) return textureCache[name]; string path = GetResourcesPath(name); var tex = AssetDatabase.LoadAssetAtPath(path); textureCache[name] = tex; return tex; } #endregion #region 数据类 private class LauncherRowData { public int Id; public int Lv; public string Picture = ""; } #endregion } /// /// 发射器预览图选择弹窗 /// 展示 LauncherMergeStoryPic ArtTableSO 中的所有图片,点击选择后回调 /// public class LauncherPicPickerWindow : EditorWindow { private ArtTableSO tableSO; private Action onSelect; private string searchFilter = ""; private Vector2 scrollPos; private const float CELL_WIDTH = 120; private const float CELL_HEIGHT = 130; private const float IMAGE_SIZE = 80; public static void Show(ArtTableSO tableSO, string title, Action onSelect) { if (tableSO == null || tableSO.Items == null || tableSO.Items.Count == 0) { EditorUtility.DisplayDialog("错误", $"{title}\n资源表为空或不存在", "确定"); return; } var window = CreateInstance(); window.titleContent = new GUIContent(title); window.tableSO = tableSO; window.onSelect = onSelect; window.minSize = new Vector2(650, 450); window.ShowUtility(); } private void OnGUI() { if (tableSO == null) { EditorGUILayout.HelpBox("资源表未加载", MessageType.Error); return; } // 搜索栏 EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); EditorGUILayout.LabelField("搜索:", GUILayout.Width(40)); searchFilter = EditorGUILayout.TextField(searchFilter, EditorStyles.toolbarSearchField); if (GUILayout.Button("×", EditorStyles.miniButton, GUILayout.Width(20))) { searchFilter = ""; GUI.FocusControl(null); } EditorGUILayout.EndHorizontal(); // 过滤 var items = tableSO.Items; if (!string.IsNullOrEmpty(searchFilter)) { string filter = searchFilter.ToLower(); items = items.Where(x => x.Name != null && x.Name.ToLower().Contains(filter)).ToList(); } EditorGUILayout.LabelField($"共 {items.Count} 个资源(点击选择)", EditorStyles.miniLabel); // 网格展示 scrollPos = EditorGUILayout.BeginScrollView(scrollPos); float windowWidth = position.width - 30; int columns = Mathf.Max(1, Mathf.FloorToInt(windowWidth / CELL_WIDTH)); for (int i = 0; i < items.Count; i += columns) { EditorGUILayout.BeginHorizontal(); for (int j = 0; j < columns && (i + j) < items.Count; j++) { DrawItemCell(items[i + j]); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); } private void DrawItemCell(ArtItemData item) { EditorGUILayout.BeginVertical("box", GUILayout.Width(CELL_WIDTH), GUILayout.Height(CELL_HEIGHT)); // 图片预览 Rect imageRect = GUILayoutUtility.GetRect(IMAGE_SIZE, IMAGE_SIZE, GUILayout.Width(IMAGE_SIZE), GUILayout.Height(IMAGE_SIZE)); if (item.Sprite != null && item.Sprite.texture != null) { Sprite sprite = item.Sprite; 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 ); EditorGUI.DrawRect(imageRect, new Color(0.4f, 0.4f, 0.4f, 1f)); GUI.DrawTextureWithTexCoords(imageRect, tex, normalizedCoords); } else { EditorGUI.DrawRect(imageRect, new Color(0.3f, 0.3f, 0.3f, 1f)); GUI.Label(imageRect, "无图片", EditorStyles.centeredGreyMiniLabel); } // 名称按钮(点击选择) string displayName = item.Name ?? "???"; if (displayName.Length > 16) { displayName = displayName.Substring(0, 14) + ".."; } if (GUILayout.Button(displayName, EditorStyles.miniButton)) { onSelect?.Invoke(item.Name); Close(); } EditorGUILayout.EndVertical(); } } }