Design_SubModule/Scripts/Editor/Design_Tools/Scene/SceneDataEditor.cs
2026-04-15 14:23:58 +08:00

633 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
using ArtResource;
namespace DesignTools.Scene
{
/// <summary>
/// 场景标题和预览图配置工具
/// 读取 Docs/config/SceneData.xlsx 的 SceneData Sheet
/// 关联 ArtTableSO "SceneDataPic" 提供预览图选择
/// </summary>
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<SceneRowData> sceneDataList = new List<SceneRowData>();
private ArtTableSO sceneDataPicSO;
private Dictionary<string, Sprite> spriteCache = new Dictionary<string, Sprite>();
private Dictionary<string, Texture2D> textureCache = new Dictionary<string, Texture2D>();
#endregion
#region
[MenuItem("策划工具/场景/场景标题和预览图配置")]
public static void ShowWindow()
{
var window = GetWindow<SceneDataEditor>("场景标题和预览图配置");
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<ArtTableSO>(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<Texture2D>(path);
textureCache[name] = tex;
return tex;
}
#endregion
#region
private class SceneRowData
{
public int Id;
public string Icon = "";
public string IconGray = "";
}
#endregion
}
/// <summary>
/// 场景预览图选择弹窗
/// 展示 SceneDataPic ArtTableSO 中的所有图片,点击选择后回调
/// </summary>
public class SceneDataPicPickerWindow : EditorWindow
{
private ArtTableSO tableSO;
private Action<string> 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<string> onSelect)
{
if (tableSO == null || tableSO.Items == null || tableSO.Items.Count == 0)
{
EditorUtility.DisplayDialog("错误", "SceneDataPic 资源表为空或不存在", "确定");
return;
}
var window = CreateInstance<SceneDataPicPickerWindow>();
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();
}
}
}