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/SceneData.xlsx 的 SceneData Sheet
/// 关联 ArtTableSO "SceneDataPic" 提供预览图选择
///
public class SceneDataEditor : BaseDesignToolEditor
{
#region 常量
private const string SCENE_EXCEL_NAME = "SceneData.xlsx";
private const string SCENE_SHEET_NAME = "SceneData";
private const string RESOURCES_PATH_PREFIX = "Assets/Art_SubModule/GameMain/UI/UISprites/Area/";
private const string SCENE_DATA_PIC_TABLE_NAME = "SceneDataPic";
#endregion
#region 数据
private List sceneDataList = new List();
private ArtTableSO sceneDataPicSO;
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() => "SceneDataEditor_DocsPath";
protected override string GetWindowTitle() => "场景标题和预览图配置";
protected override Vector2 GetMinWindowSize() => new Vector2(1300, 600);
protected override void LoadConfigData()
{
spriteCache.Clear();
textureCache.Clear();
LoadSceneDataPicSO();
LoadSceneDataExcel();
}
protected override void DrawDataEditor()
{
if (sceneDataPicSO == null)
{
EditorGUILayout.HelpBox(
$"未找到 ArtTableSO \"{SCENE_DATA_PIC_TABLE_NAME}\",预览图选择功能不可用。\n请先在美术资源工具中创建名为 \"{SCENE_DATA_PIC_TABLE_NAME}\" 的资源表。",
MessageType.Warning);
}
EditorGUILayout.BeginVertical("box");
// 标题 + 新增按钮
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"场景数据列表(共 {sceneDataList.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);
SceneRowData toDelete = null;
foreach (var data in sceneDataList)
{
DrawDataRow(data, ref toDelete);
}
if (toDelete != null)
{
sceneDataList.Remove(toDelete);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
protected override void SaveDataToExcel()
{
try
{
string excelPath = GetDocsConfigFilePath(SCENE_EXCEL_NAME);
if (!File.Exists(excelPath))
{
EditorUtility.DisplayDialog("错误", $"文件不存在: {excelPath}", "确定");
return;
}
using (var package = new ExcelPackage(new FileInfo(excelPath)))
{
var ws = package.Workbook.Worksheets[SCENE_SHEET_NAME];
if (ws == null)
{
EditorUtility.DisplayDialog("错误", $"未找到 Sheet: {SCENE_SHEET_NAME}", "确定");
return;
}
// 查找列索引
int idCol = -1, sceneIdCol = -1, areaIdCol = -1, titleCol = -1;
int iconCol = -1, iconGrayCol = -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 "SceneId": sceneIdCol = c; break;
case "AreaId": areaIdCol = c; break;
case "Title": titleCol = c; break;
case "Icon": iconCol = c; break;
case "IconGray": iconGrayCol = 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 < sceneDataList.Count; i++)
{
int row = i + 3;
var data = sceneDataList[i];
ws.Cells[row, idCol].Value = data.Id;
if (sceneIdCol > 0) ws.Cells[row, sceneIdCol].Value = GetSceneId(data.Id);
if (areaIdCol > 0) ws.Cells[row, areaIdCol].Value = GetAreaId(data.Id);
if (titleCol > 0) ws.Cells[row, titleCol].Value = GetTitle(data.Id);
if (iconCol > 0) ws.Cells[row, iconCol].Value = data.Icon ?? "";
if (iconGrayCol > 0) ws.Cells[row, iconGrayCol].Value = data.IconGray ?? "";
if (resPathCol > 0) ws.Cells[row, resPathCol].Value = GetResourcesPath(data.Icon);
}
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 LoadSceneDataPicSO()
{
sceneDataPicSO = 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 == SCENE_DATA_PIC_TABLE_NAME)
{
sceneDataPicSO = so;
break;
}
}
if (sceneDataPicSO == null)
{
Debug.LogWarning($"未找到 ArtTableSO: {SCENE_DATA_PIC_TABLE_NAME},预览图选择功能将不可用");
}
}
private void LoadSceneDataExcel()
{
sceneDataList.Clear();
string excelPath = GetDocsConfigFilePath(SCENE_EXCEL_NAME);
if (!File.Exists(excelPath))
{
throw new Exception($"未找到文件: {excelPath}");
}
using (var package = new ExcelPackage(new FileInfo(excelPath)))
{
var ws = package.Workbook.Worksheets[SCENE_SHEET_NAME];
if (ws == null)
{
throw new Exception($"未找到 Sheet: {SCENE_SHEET_NAME}");
}
// 查找列
int idCol = -1, iconCol = -1, iconGrayCol = -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 == "Icon") iconCol = c;
else if (h == "IconGray") iconGrayCol = 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;
sceneDataList.Add(new SceneRowData
{
Id = id,
Icon = iconCol > 0 ? (ws.Cells[row, iconCol].Text ?? "") : "",
IconGray = iconGrayCol > 0 ? (ws.Cells[row, iconGrayCol].Text ?? "") : "",
});
}
}
}
#endregion
#region UI 绘制
private void DrawTableHeader()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("Id", EditorStyles.miniLabel, GUILayout.Width(35));
EditorGUILayout.LabelField("SceneId", EditorStyles.miniLabel, GUILayout.Width(55));
EditorGUILayout.LabelField("AreaId", EditorStyles.miniLabel, GUILayout.Width(45));
EditorGUILayout.LabelField("Title", EditorStyles.miniLabel, GUILayout.Width(170));
EditorGUILayout.LabelField("Icon", EditorStyles.miniLabel, GUILayout.Width(168));
EditorGUILayout.LabelField("预览", EditorStyles.miniLabel, GUILayout.Width(55));
EditorGUILayout.LabelField("IconGray", EditorStyles.miniLabel, GUILayout.Width(168));
EditorGUILayout.LabelField("预览", EditorStyles.miniLabel, GUILayout.Width(55));
EditorGUILayout.LabelField("ResourcesPath", EditorStyles.miniLabel);
EditorGUILayout.LabelField("", GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
}
private void DrawDataRow(SceneRowData data, ref SceneRowData toDelete)
{
EditorGUILayout.BeginHorizontal();
// Id(只读)
EditorGUILayout.LabelField(data.Id.ToString(), GUILayout.Width(35));
// SceneId(自动)
EditorGUILayout.LabelField(GetSceneId(data.Id).ToString(), GUILayout.Width(55));
// AreaId(自动)
EditorGUILayout.LabelField(GetAreaId(data.Id).ToString(), GUILayout.Width(45));
// Title(自动)
EditorGUILayout.LabelField(GetTitle(data.Id), GUILayout.Width(170));
// Icon + 选择按钮
EditorGUILayout.BeginHorizontal(GUILayout.Width(168));
data.Icon = EditorGUILayout.TextField(data.Icon, GUILayout.Width(120));
GUI.enabled = sceneDataPicSO != null;
if (GUILayout.Button("设置预览图", EditorStyles.miniButton, GUILayout.Width(60)))
{
var capturedData = data;
SceneDataPicPickerWindow.Show(sceneDataPicSO, "选择 Icon 预览图", name =>
{
capturedData.Icon = name;
// 清除缓存以便刷新
spriteCache.Remove(name);
textureCache.Remove(name);
Repaint();
});
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
// Icon 预览
DrawSpritePreview(data.Icon, 50);
// IconGray + 选择按钮
EditorGUILayout.BeginHorizontal(GUILayout.Width(168));
data.IconGray = EditorGUILayout.TextField(data.IconGray, GUILayout.Width(120));
GUI.enabled = sceneDataPicSO != null;
if (GUILayout.Button("设置预览图", EditorStyles.miniButton, GUILayout.Width(60)))
{
var capturedData = data;
SceneDataPicPickerWindow.Show(sceneDataPicSO, "选择 IconGray 预览图", name =>
{
capturedData.IconGray = name;
spriteCache.Remove(name);
textureCache.Remove(name);
Repaint();
});
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
// IconGray 预览
DrawSpritePreview(data.IconGray, 50);
// ResourcesPath(自动,只读)
EditorGUILayout.LabelField(GetResourcesPath(data.Icon), EditorStyles.miniLabel);
// 删除按钮
GUI.backgroundColor = Color.red;
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
if (EditorUtility.DisplayDialog("确认删除",
$"确定要删除 Id={data.Id} 的行吗?", "删除", "取消"))
{
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 (sceneDataList.Count > 0)
{
newId = sceneDataList.Max(x => x.Id) + 1;
}
sceneDataList.Add(new SceneRowData
{
Id = newId,
Icon = "",
IconGray = "",
});
scrollPosition = new Vector2(0, float.MaxValue);
}
#endregion
#region 自动计算
private static int GetSceneId(int id) => ((id - 1) / 5) + 1;
private static int GetAreaId(int id) => ((id - 1) % 5) + 1;
private static string GetTitle(int id) => $"CS_ScenePanel_Scene{id}";
private static string GetResourcesPath(string icon)
{
if (string.IsNullOrEmpty(icon)) return "";
return $"{RESOURCES_PATH_PREFIX}{icon}.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 (sceneDataPicSO != null)
{
var item = sceneDataPicSO.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 SceneRowData
{
public int Id;
public string Icon = "";
public string IconGray = "";
}
#endregion
}
///
/// 场景预览图选择弹窗
/// 展示 SceneDataPic ArtTableSO 中的所有图片,点击选择后回调
///
public class SceneDataPicPickerWindow : 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("错误", "SceneDataPic 资源表为空或不存在", "确定");
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();
}
}
}