Files
zeling_v2/Assets/_Game/Scripts/Editor/Tools/SOManagerWindow.cs
Joywayer bb3afd130f feat: Add SkillModule and WeaponModule for managing skills and weapons
- Implemented SkillModule to manage FormSkillSO assets with a detailed UI for editing and displaying skill properties.
- Implemented WeaponModule to manage WeaponSO assets with a detailed UI for editing and displaying weapon properties.
- Created AssetOperations class for centralized CRUD operations on ScriptableObject assets, including create, rename, delete, and clone functionalities.
- Added DetailHeader for displaying and renaming asset names in the UI.
- Introduced SoListPane for a reusable ScriptableObject list panel with search functionality and context menus.
- Added meta files for all new scripts to ensure proper asset management in Unity.
2026-05-21 07:09:53 +08:00

442 lines
18 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 UnityEngine.UIElements;
namespace BaseGames.Editor
{
/// <summary>
/// SO 资产总管理窗口 —— 浏览、搜索并在 Project 窗口定位项目中所有 ScriptableObject 资产。
///
/// 布局:顶部搜索栏 | 左侧分类列表 | 右侧资产列表(名称 / 类型 / 路径)
/// 功能:单击资产行 → Project 窗口 Ping 并选中;双击 → 同上并聚焦 Project 窗口。
/// 菜单BaseGames / Tools / SO Manager (Priority 2)
/// </summary>
public class SOManagerWindow : EditorWindow
{
private const string DataRoot = "Assets/_Game/Data";
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
// ── 数据模型 ──────────────────────────────────────────────────────────
private sealed class CategoryEntry
{
public string Label;
public string Folder; // null = 全部
public int Count;
}
private sealed class AssetEntry
{
public string Name;
public string TypeName;
public string AssetPath;
public ScriptableObject Asset;
public string Guid;
}
private readonly List<CategoryEntry> _categories = new();
private readonly List<AssetEntry> _allAssets = new();
private readonly List<AssetEntry> _filtered = new();
private int _selectedCatIdx = 0;
private string _search = "";
// ── UI 引用 ───────────────────────────────────────────────────────────
private ListView _catList;
private ListView _assetList;
private TextField _searchField;
private Label _statusLabel;
private VisualElement _renameContainer;
// ── 菜单入口 ──────────────────────────────────────────────────────────
[MenuItem("BaseGames/Tools/SO Manager", priority = 2)]
public static void Open()
{
var wnd = GetWindow<SOManagerWindow>();
wnd.titleContent = new GUIContent("SO Manager",
EditorGUIUtility.IconContent("d_ScriptableObject Icon").image);
wnd.minSize = new Vector2(680, 420);
}
// ── 生命周期 ──────────────────────────────────────────────────────────
public void CreateGUI()
{
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
if (uss != null) rootVisualElement.styleSheets.Add(uss);
BuildUI();
Refresh();
}
private void OnFocus() => Refresh();
// ── UI 构建 ───────────────────────────────────────────────────────────
private void BuildUI()
{
rootVisualElement.style.flexDirection = FlexDirection.Column;
// ─ 顶部工具栏 ──────────────────────────────────────────────────────
var toolbar = new VisualElement();
toolbar.AddToClassList("editor-toolbar");
var searchLbl = new Label("搜索:");
searchLbl.style.unityTextAlign = TextAnchor.MiddleLeft;
searchLbl.style.marginRight = 4;
_searchField = new TextField();
_searchField.style.flexGrow = 1;
_searchField.RegisterValueChangedCallback(e =>
{
_search = e.newValue;
ApplyFilter();
});
var refreshBtn = new Button(Refresh) { text = "↻ 刷新" };
refreshBtn.style.marginLeft = 8;
refreshBtn.style.width = 58;
toolbar.Add(searchLbl);
toolbar.Add(_searchField);
toolbar.Add(refreshBtn);
rootVisualElement.Add(toolbar);
// ─ 主体:两栏分割 ──────────────────────────────────────────────────
var split = new TwoPaneSplitView(0, 164, TwoPaneSplitViewOrientation.Horizontal);
split.style.flexGrow = 1;
// 左栏:分类列表 ────────────────────────────────────────────────────
var leftPane = new VisualElement();
leftPane.style.flexDirection = FlexDirection.Column;
leftPane.style.minWidth = 100;
var catHeader = new Label("分类");
catHeader.AddToClassList("pane-header");
leftPane.Add(catHeader);
_catList = new ListView
{
makeItem = MakeCatItem,
bindItem = BindCatItem,
selectionType = SelectionType.Single,
fixedItemHeight = 26,
};
_catList.style.flexGrow = 1;
_catList.selectionChanged += _ =>
{
if (_catList.selectedIndex >= 0)
{
_selectedCatIdx = _catList.selectedIndex;
ApplyFilter();
}
};
leftPane.Add(_catList);
// 右栏:资产列表 ────────────────────────────────────────────────────
var rightPane = new VisualElement();
rightPane.style.flexDirection = FlexDirection.Column;
// 列标题行paddingRight 额外加 13px滚动条宽度确保列对齐
var colHeader = new VisualElement();
colHeader.AddToClassList("pane-header");
colHeader.style.flexDirection = FlexDirection.Row;
colHeader.style.paddingLeft = 8;
colHeader.style.paddingRight = 21; // 8 + 13 (滚动条占位)
colHeader.style.paddingTop = 4;
colHeader.style.paddingBottom = 4;
colHeader.Add(MakeHeaderLabel("资产名", true, 0));
colHeader.Add(MakeHeaderLabel("类型", false, 190));
colHeader.Add(MakeHeaderLabel("路径", true, 0));
rightPane.Add(colHeader);
_assetList = new ListView
{
makeItem = MakeAssetRow,
bindItem = BindAssetRow,
selectionType = SelectionType.Single,
fixedItemHeight = 22,
};
_assetList.style.flexGrow = 1;
_assetList.selectionChanged += _ => OnAssetPicked();
_assetList.itemsChosen += _ => FocusProjectWindow();
_assetList.AddManipulator(new ContextualMenuManipulator(BuildAssetContextMenu));
rightPane.Add(_assetList);
// 滚动条常显,确保 header 列与内容列对齐
_assetList.schedule.Execute(() =>
{
var sv = _assetList.Q<ScrollView>();
if (sv != null) sv.verticalScrollerVisibility = ScrollerVisibility.AlwaysVisible;
});
// 状态栏
_statusLabel = new Label("—");
_statusLabel.style.paddingLeft = 8;
_statusLabel.style.paddingTop = 3;
_statusLabel.style.paddingBottom = 3;
_statusLabel.style.borderTopWidth = 1;
_statusLabel.style.borderTopColor = new Color(0.0f, 0.0f, 0.0f, 0.20f);
_statusLabel.AddToClassList("so-path-label");
rightPane.Add(_statusLabel);
// 重命名容器(选中资产后显示)
_renameContainer = new VisualElement { style = { display = DisplayStyle.None } };
rightPane.Add(_renameContainer);
split.Add(leftPane);
split.Add(rightPane);
rootVisualElement.Add(split);
}
private static Label MakeHeaderLabel(string text, bool grow, int fixedWidth)
{
var lbl = new Label(text);
lbl.style.unityFontStyleAndWeight = FontStyle.Bold;
lbl.style.unityTextAlign = TextAnchor.MiddleLeft;
lbl.style.overflow = Overflow.Hidden;
lbl.style.minWidth = 0;
if (grow) lbl.style.flexGrow = 1;
if (fixedWidth > 0) lbl.style.width = fixedWidth;
return lbl;
}
// ── 分类列表项 ────────────────────────────────────────────────────────
private static VisualElement MakeCatItem()
{
var lbl = new Label();
lbl.style.paddingLeft = 10;
lbl.style.paddingRight = 6;
lbl.style.overflow = Overflow.Hidden;
lbl.style.unityTextAlign = TextAnchor.MiddleLeft;
return lbl;
}
private void BindCatItem(VisualElement el, int i)
{
if (i >= _categories.Count) return;
var cat = _categories[i];
((Label)el).text = $"{cat.Label} ({cat.Count})";
}
// ── 资产列表项 ────────────────────────────────────────────────────────
private static VisualElement MakeAssetRow()
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.paddingLeft = 8;
row.style.paddingRight = 8;
var nameEl = new Label { name = "n" };
nameEl.style.flexGrow = 1;
nameEl.style.minWidth = 0;
nameEl.style.overflow = Overflow.Hidden;
nameEl.style.textOverflow = TextOverflow.Ellipsis;
var typeEl = new Label { name = "t" };
typeEl.style.width = 190;
typeEl.style.minWidth = 0;
typeEl.style.overflow = Overflow.Hidden;
typeEl.style.textOverflow = TextOverflow.Ellipsis;
typeEl.AddToClassList("so-type-label");
var pathEl = new Label { name = "p" };
pathEl.style.flexGrow = 1;
pathEl.style.overflow = Overflow.Hidden;
pathEl.style.textOverflow = TextOverflow.Ellipsis;
pathEl.AddToClassList("so-path-label");
row.Add(nameEl);
row.Add(typeEl);
row.Add(pathEl);
return row;
}
private void BindAssetRow(VisualElement el, int i)
{
if (i >= _filtered.Count) return;
var e = _filtered[i];
el.Q<Label>("n").text = e.Name;
el.Q<Label>("t").text = e.TypeName;
// 显示相对于 DataRoot 的路径,去掉文件名本身只保留目录
string rel = e.AssetPath.StartsWith(DataRoot + "/")
? e.AssetPath.Substring(DataRoot.Length + 1)
: e.AssetPath;
// 去掉最后的文件名,只显示目录部分
string dir = Path.GetDirectoryName(rel)?.Replace('\\', '/') ?? "";
el.Q<Label>("p").text = string.IsNullOrEmpty(dir) ? "/" : dir;
}
// ── 资产选中 ──────────────────────────────────────────────────────────
private void OnAssetPicked()
{
int idx = _assetList.selectedIndex;
if (idx < 0 || idx >= _filtered.Count)
{
_renameContainer.style.display = DisplayStyle.None;
return;
}
var asset = _filtered[idx].Asset;
if (asset == null)
{
_renameContainer.style.display = DisplayStyle.None;
return;
}
EditorGUIUtility.PingObject(asset);
Selection.activeObject = asset;
// 更新重命名栏
_renameContainer.Clear();
var (pfx, conv) = EditorScaffoldUtils.GetAssetPrefixInfo(asset.GetType());
var entry = _filtered[idx];
string savedGuid = entry.Guid;
_renameContainer.Add(EditorScaffoldUtils.MakeRenameBar(asset, pfx, conv, _ =>
{
Refresh();
int ni = _filtered.FindIndex(e => e.Guid == savedGuid);
if (ni >= 0) _assetList.SetSelection(ni);
}));
// 删除按钮
var btnDelete = new Button(() => DeleteCurrentAsset(asset)) { text = "删除…" };
btnDelete.AddToClassList("action-button--danger");
_renameContainer.Add(btnDelete);
_renameContainer.style.display = DisplayStyle.Flex;
}
private static void FocusProjectWindow()
{
EditorApplication.ExecuteMenuItem("Window/General/Project");
}
// ── 数据逻辑 ──────────────────────────────────────────────────────────
private void Refresh()
{
_allAssets.Clear();
// 扫描 DataRoot 下所有 ScriptableObject 资产
var guids = AssetDatabase.FindAssets("t:ScriptableObject", new[] { DataRoot });
foreach (var guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<ScriptableObject>(path);
if (asset == null) continue;
_allAssets.Add(new AssetEntry
{
Name = asset.name,
TypeName = asset.GetType().Name,
AssetPath = path,
Asset = asset,
Guid = guid,
});
}
// 构建分类:首项为"全部",其余为 DataRoot 的直接子目录
_categories.Clear();
_categories.Add(new CategoryEntry
{
Label = "全部",
Folder = null,
Count = _allAssets.Count,
});
if (AssetDatabase.IsValidFolder(DataRoot))
{
foreach (var sub in AssetDatabase.GetSubFolders(DataRoot).OrderBy(f => f))
{
string folderName = Path.GetFileName(sub);
int count = _allAssets.Count(a =>
a.AssetPath.StartsWith(sub + "/", StringComparison.Ordinal));
if (count == 0) continue;
_categories.Add(new CategoryEntry
{
Label = folderName,
Folder = sub,
Count = count,
});
}
}
_catList.itemsSource = _categories;
_catList.Rebuild();
int clampedIdx = Mathf.Clamp(_selectedCatIdx, 0, _categories.Count - 1);
_catList.SetSelection(clampedIdx);
_selectedCatIdx = clampedIdx;
ApplyFilter();
}
private void DeleteCurrentAsset(ScriptableObject asset)
{
if (!EditorScaffoldUtils.DeleteSOAsset(asset)) return;
_renameContainer.Clear();
_renameContainer.style.display = DisplayStyle.None;
Refresh();
}
private void BuildAssetContextMenu(ContextualMenuPopulateEvent evt)
{
int idx = _assetList.selectedIndex;
if (idx < 0 || idx >= _filtered.Count) return;
var asset = _filtered[idx].Asset;
if (asset == null) return;
evt.menu.AppendAction("在 Project 中定位", _ => EditorScaffoldUtils.PingAndSelect(asset));
evt.menu.AppendAction("在 Inspector 中打开", _ => Selection.activeObject = asset);
evt.menu.AppendSeparator();
evt.menu.AppendAction("删除…", _ => DeleteCurrentAsset(asset));
}
private void ApplyFilter()
{
_filtered.Clear();
string folder = (_selectedCatIdx >= 0 && _selectedCatIdx < _categories.Count)
? _categories[_selectedCatIdx].Folder
: null;
IEnumerable<AssetEntry> source = folder == null
? _allAssets
: _allAssets.Where(a => a.AssetPath.StartsWith(folder + "/", StringComparison.Ordinal));
foreach (var entry in source)
{
if (string.IsNullOrEmpty(_search)
|| entry.Name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0
|| entry.TypeName.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0)
{
_filtered.Add(entry);
}
}
// 同分类内按类型分组,再按名称排序
_filtered.Sort((a, b) =>
{
int cmp = string.Compare(a.TypeName, b.TypeName, StringComparison.OrdinalIgnoreCase);
return cmp != 0 ? cmp : string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
});
_assetList.itemsSource = _filtered;
_assetList.Rebuild();
int total = folder == null
? _allAssets.Count
: _allAssets.Count(a => a.AssetPath.StartsWith(folder + "/", StringComparison.Ordinal));
_statusLabel.text = string.IsNullOrEmpty(_search)
? $"共 {_filtered.Count} 个资产"
: $"筛选 {_filtered.Count} / {total} 个资产(搜索:{_search}";
}
}
}