发射器插图工具

This commit is contained in:
zhang hongbo 2026-04-15 15:16:07 +08:00
parent eeaea6d431
commit a979bd06fd
2 changed files with 614 additions and 0 deletions

View File

@ -0,0 +1,603 @@
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/NetAssetData.xlsx 的 LevelLauncherData Sheet
/// 关联 ArtTableSO "LauncherMergeStoryPic" 提供预览图选择
/// </summary>
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<LauncherRowData> dataList = new List<LauncherRowData>();
private ArtTableSO picTableSO;
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<LevelLauncherDataEditor>("发射器插图名称和资源设置");
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<ArtTableSO>(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<Texture2D>(path);
textureCache[name] = tex;
return tex;
}
#endregion
#region
private class LauncherRowData
{
public int Id;
public int Lv;
public string Picture = "";
}
#endregion
}
/// <summary>
/// 发射器预览图选择弹窗
/// 展示 LauncherMergeStoryPic ArtTableSO 中的所有图片,点击选择后回调
/// </summary>
public class LauncherPicPickerWindow : 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("错误", $"{title}\n资源表为空或不存在", "确定");
return;
}
var window = CreateInstance<LauncherPicPickerWindow>();
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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1645c0799a23c904f89e540f2fc05492
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: