refactor(editor): reorganize Editor directory and unify menu hierarchy

File directory changes (mirror Scripts/ module structure):
- AbilityTypeDrawer.cs         → Equipment/
- CharacterWizardWindow.cs     → Character/
- FormEditorWindow.cs          → Player/
- GMToolWindow.cs              → Tools/
- SOManagerWindow.cs           → Tools/
- Map/MapRoomDataEditor.cs     → World/Map/
- Navigation/ (root)           → Enemies/Navigation/
- Achievements/                → Progression/

Menu hierarchy changes (BaseGames/ top-level):
- Data/: +Character Wizard (from Tools/), +Boss Skill Sequence (from Tools/)
- Addressables/: +Addressable Batch Tool, +Asset Reference Graph, +Validate Address Keys (from Tools/Verification/)
- Scene/Setup/: +Boot Flow Wizard, +Scaffold *, +Auto-Open Persistent (from Tools/)
- Scene/: +Camera Area Setup (from Camera/), +Bake All NavSurfaces (from Tools/)
- Events/: +Event Bus Monitor, +Event Chain Viewer, +Create/Reimport Event Channels (from Tools/)
- Tools/Validation/: +Validate All SOs, +Apply/Validate Script Order (from Tools/ flat)
- Tools/Maintenance/: +Missing Scripts/*, +Physics2D Layer Matrix/* (from Tools/ flat)

Result: BaseGames/Tools/ reduced from 16 flat items to 4 items + 2 submenus

Docs: update AssetFolderSpec §12 editor tool table with new menu paths

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-20 11:52:17 +08:00
parent 442d4c9cfc
commit 82ce9ff09a
38 changed files with 115 additions and 61 deletions

View File

@@ -0,0 +1,413 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Player;
namespace BaseGames.Editor
{
/// <summary>
/// 形态系统可视化编辑器W-06
/// 技术UI Toolkit TwoPaneSplitView + 三列形态网格。
/// 菜单BaseGames / Data / Form Editor
///
/// 左栏FormConfigSO 列表 + [新建] 按钮。
/// 右栏:
/// · 三列网格(天魂 / 地魂 / 命魂),每列显示对应 FormSO 详情及武器引用。
/// · 各列可独立新建/重新绑定 FormSO。
/// · "一键自动填充" 按钮:按 formType 枚举在 Project 中搜索已有 FormSO 并赋值。
/// · 底部:在 Project 中选中 FormConfigSO / 在 Inspector 中编辑 原始字段。
/// </summary>
public class FormEditorWindow : EditorWindow
{
// ── 常量 ──────────────────────────────────────────────────────────────
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
private const string DataRoot = "Assets/_Game/Data/Player/Forms";
private static readonly StyleSheet _uss;
private static readonly (FormType type, string label, Color accent)[] FormDefs =
{
(FormType.TianHun, "天魂", new Color(0.40f, 0.70f, 1.00f)),
(FormType.DiHun, "地魂", new Color(0.55f, 0.85f, 0.40f)),
(FormType.MingHun, "命魂", new Color(0.80f, 0.30f, 0.30f)),
};
static FormEditorWindow()
{
_uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
}
[MenuItem("BaseGames/Data/Form Editor", priority = 103)]
public static void Open()
{
var wnd = GetWindow<FormEditorWindow>();
wnd.titleContent = new GUIContent("Form Editor");
wnd.minSize = new Vector2(760, 420);
}
// ── 状态 ──────────────────────────────────────────────────────────────
private List<FormConfigSO> _configs = new();
private List<FormConfigSO> _filtered = new();
private FormConfigSO _selected;
private string _searchText = "";
private ListView _configList;
private VisualElement _detailRoot;
private InspectorElement _rawInspector;
// 三列形态格(每列一个 FormSO ObjectField
private readonly ObjectField[] _formFields = new ObjectField[3];
private readonly ObjectField[] _weaponFields = new ObjectField[3];
private readonly VisualElement[]_columnRoots = new VisualElement[3];
// ── 生命周期 ──────────────────────────────────────────────────────────
public void CreateGUI()
{
if (_uss != null)
rootVisualElement.styleSheets.Add(_uss);
// ── Toolbar ───────────────────────────────────────────────────────
var toolbar = new Toolbar();
var search = new ToolbarSearchField { style = { flexGrow = 1 } };
search.RegisterValueChangedCallback(e => { _searchText = e.newValue; RefreshFilter(); });
search.tooltip = "按名称过滤 FormConfigSO";
toolbar.Add(search);
var btnNew = new ToolbarButton(CreateNewFormConfig) { text = "+ 新建 FormConfig" };
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
btnRefresh.tooltip = "重新扫描 Project 中的 FormConfigSO 资产";
toolbar.Add(btnNew);
toolbar.Add(btnRefresh);
rootVisualElement.Add(toolbar);
// ── 分栏 ──────────────────────────────────────────────────────────
var split = new TwoPaneSplitView(0, 200, TwoPaneSplitViewOrientation.Horizontal);
// 左栏
var leftPane = new VisualElement { style = { minWidth = 140 } };
_configList = new ListView
{
selectionType = SelectionType.Single,
makeItem = MakeListItem,
bindItem = BindListItem,
style = { flexGrow = 1 },
showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly,
};
_configList.selectionChanged += OnConfigSelected;
leftPane.Add(_configList);
split.Add(leftPane);
// 右栏ScrollView
_detailRoot = new ScrollView(ScrollViewMode.Vertical) { style = { flexGrow = 1 } };
_detailRoot.contentContainer.style.paddingLeft = 10;
_detailRoot.contentContainer.style.paddingRight = 10;
_detailRoot.contentContainer.style.paddingTop = 10;
_detailRoot.contentContainer.style.paddingBottom = 10;
_detailRoot.Add(new HelpBox("← 在左侧选择一个 FormConfigSO 开始编辑", HelpBoxMessageType.Info));
split.Add(_detailRoot);
rootVisualElement.Add(split);
RefreshAll();
}
// ── 列表 ──────────────────────────────────────────────────────────────
private VisualElement MakeListItem()
{
var label = new Label();
label.AddToClassList("list-item");
return label;
}
private void BindListItem(VisualElement ve, int idx)
{
if (ve is Label lbl && idx < _filtered.Count)
lbl.text = _filtered[idx].name;
}
private void OnConfigSelected(IEnumerable<object> objs)
{
_selected = objs.FirstOrDefault() as FormConfigSO;
RebuildDetail();
}
private void RefreshFilter()
{
string s = _searchText.ToLowerInvariant();
_filtered = string.IsNullOrEmpty(s)
? new List<FormConfigSO>(_configs)
: _configs.Where(c => c.name.ToLowerInvariant().Contains(s)).ToList();
_configList.itemsSource = _filtered;
_configList.Rebuild();
}
private void RefreshAll()
{
_configs = EditorScaffoldUtils.FindAllAssetsOfType<FormConfigSO>();
_configs.Sort((a, b) => string.Compare(a.name, b.name, System.StringComparison.Ordinal));
RefreshFilter();
}
// ── 右栏详情 ──────────────────────────────────────────────────────────
private void RebuildDetail()
{
_detailRoot.Clear();
if (_selected == null) return;
// 标题 + 快捷操作
var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 8 } };
header.Add(new Label(_selected.name) { style = { fontSize = 14, unityFontStyleAndWeight = FontStyle.Bold, flexGrow = 1 } });
var btnPing = new Button(() => EditorGUIUtility.PingObject(_selected)) { text = "⌖ Ping" };
var btnAuto = new Button(AutoFillForms) { text = "自动填充形态", tooltip = "按 formType 枚举在 Project 中搜索已有 FormSO 并赋值到 forms[] 数组" };
header.Add(btnAuto);
header.Add(btnPing);
_detailRoot.Add(header);
// 三列形态网格
var formGrid = new VisualElement();
formGrid.style.flexDirection = FlexDirection.Row;
formGrid.style.marginBottom = 10;
_detailRoot.Add(formGrid);
for (int i = 0; i < 3; i++)
formGrid.Add(BuildFormColumn(i));
// 分割线
_detailRoot.Add(MakeSeparator());
// 原始 Inspector完整字段编辑
var inspHeader = new Label("原始 Inspector 编辑") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } };
_detailRoot.Add(inspHeader);
_rawInspector = new InspectorElement(_selected);
_detailRoot.Add(_rawInspector);
}
private VisualElement BuildFormColumn(int colIdx)
{
var (formType, label, accent) = FormDefs[colIdx];
var col = new VisualElement();
col.style.flexGrow = 1;
col.style.marginRight = colIdx < 2 ? 8 : 0;
col.style.borderTopWidth = 2;
col.style.borderTopColor = accent;
col.style.borderLeftWidth = 1;
col.style.borderRightWidth = 1;
col.style.borderBottomWidth = 1;
col.style.borderLeftColor = new Color(accent.r, accent.g, accent.b, 0.35f);
col.style.borderRightColor = new Color(accent.r, accent.g, accent.b, 0.35f);
col.style.borderBottomColor = new Color(accent.r, accent.g, accent.b, 0.35f);
col.style.borderTopLeftRadius = 4;
col.style.borderTopRightRadius = 4;
col.style.borderBottomLeftRadius = 4;
col.style.borderBottomRightRadius = 4;
col.style.paddingLeft = 8;
col.style.paddingRight = 8;
col.style.paddingTop = 8;
col.style.paddingBottom = 8;
_columnRoots[colIdx] = col;
// 列标题 + 色块
var titleRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 6 } };
var colorDot = new VisualElement();
colorDot.style.width = 12;
colorDot.style.height = 12;
colorDot.style.borderTopLeftRadius = 6;
colorDot.style.borderTopRightRadius = 6;
colorDot.style.borderBottomLeftRadius = 6;
colorDot.style.borderBottomRightRadius = 6;
colorDot.style.backgroundColor = accent;
colorDot.style.marginRight = 6;
titleRow.Add(colorDot);
titleRow.Add(new Label(label) { style = { unityFontStyleAndWeight = FontStyle.Bold } });
col.Add(titleRow);
// 当前 FormSO 显示
FormSO current = GetFormByType(formType);
var formField = new ObjectField("FormSO") { objectType = typeof(FormSO), value = current };
formField.RegisterValueChangedCallback(e =>
{
SetFormByType(formType, e.newValue as FormSO);
// 联动刷新武器字段
if (_weaponFields[colIdx] != null)
_weaponFields[colIdx].value = (e.newValue as FormSO)?.defaultWeapon;
});
_formFields[colIdx] = formField;
col.Add(formField);
// 武器预览(只读,跟随 FormSO.defaultWeapon
var weaponField = new ObjectField("默认武器") { objectType = typeof(WeaponSO), value = current?.defaultWeapon };
weaponField.SetEnabled(false);
_weaponFields[colIdx] = weaponField;
col.Add(weaponField);
// 新建该形态 FormSO
var btnCreate = new Button(() => CreateFormForType(formType, colIdx))
{
text = current == null ? $"+ 新建 {label} FormSO" : "重新新建(覆盖)",
tooltip = $"在 {DataRoot} 创建 Form_{formType}.asset",
style = { marginTop = 8 }
};
col.Add(btnCreate);
// 新建该形态 WeaponSO
var btnWeapon = new Button(() => CreateWeaponForType(formType, colIdx))
{
text = $"+ 新建 {label} WeaponSO",
tooltip = $"在 Assets/_Game/Data/Player/Weapons 创建 Weapon_{formType}.asset",
style = { marginTop = 2 }
};
col.Add(btnWeapon);
return col;
}
// ── 操作:新建 FormConfigSO ───────────────────────────────────────────
private void CreateNewFormConfig()
{
var cfg = EditorScaffoldUtils.CreateSOAsset<FormConfigSO>(DataRoot, "FormConfig");
if (cfg != null)
{
RefreshAll();
// 选中新建项
int idx = _filtered.IndexOf(cfg);
if (idx >= 0) _configList.SetSelection(idx);
}
}
// ── 操作:新建 FormSO ──────────────────────────────────────────────────
private void CreateFormForType(FormType formType, int colIdx)
{
if (_selected == null) return;
EditorScaffoldUtils.EnsureFolder(DataRoot);
string path = $"{DataRoot}/Form_{formType}.asset";
var form = AssetDatabase.LoadAssetAtPath<FormSO>(path);
if (form == null)
{
form = ScriptableObject.CreateInstance<FormSO>();
form.formId = $"Form_{formType}";
form.displayName = FormDefs[colIdx].label;
form.formType = formType;
AssetDatabase.CreateAsset(form, path);
AssetDatabase.SaveAssets();
}
SetFormByType(formType, form);
_formFields[colIdx].value = form;
EditorGUIUtility.PingObject(form);
}
// ── 操作:新建 WeaponSO ───────────────────────────────────────────────
private void CreateWeaponForType(FormType formType, int colIdx)
{
string dir = "Assets/_Game/Data/Player/Weapons";
var weapon = EditorScaffoldUtils.CreateSOAsset<WeaponSO>(dir, $"Weapon_{formType}");
if (weapon == null) weapon = AssetDatabase.LoadAssetAtPath<WeaponSO>($"{dir}/Weapon_{formType}.asset");
if (weapon == null) return;
// 赋值到对应 FormSO.defaultWeapon
var form = GetFormByType(formType);
if (form != null)
{
form.defaultWeapon = weapon;
EditorUtility.SetDirty(form);
AssetDatabase.SaveAssets();
_weaponFields[colIdx].value = weapon;
}
}
// ── 操作:自动填充形态 ────────────────────────────────────────────────
private void AutoFillForms()
{
if (_selected == null) return;
var allForms = EditorScaffoldUtils.FindAllAssetsOfType<FormSO>();
bool changed = false;
for (int i = 0; i < 3; i++)
{
var (ftype, _, _) = FormDefs[i];
if (GetFormByType(ftype) != null) continue;
var match = allForms.FirstOrDefault(f => f.formType == ftype);
if (match != null)
{
SetFormByType(ftype, match);
_formFields[i].value = match;
changed = true;
}
}
if (changed)
{
EditorUtility.SetDirty(_selected);
AssetDatabase.SaveAssets();
Debug.Log("[FormEditorWindow] 自动填充完成。");
}
else
{
Debug.Log("[FormEditorWindow] 未发现需要填充的空槽,或 Project 中无匹配 FormSO。");
}
}
// ── 数据访问(操作 FormConfigSO.forms[])────────────────────────────
private FormSO GetFormByType(FormType type)
{
if (_selected == null || _selected.forms == null) return null;
return _selected.forms.FirstOrDefault(f => f != null && f.formType == type);
}
private void SetFormByType(FormType type, FormSO form)
{
if (_selected == null) return;
if (_selected.forms == null || _selected.forms.Length < 3)
{
var arr = new FormSO[3];
if (_selected.forms != null)
for (int i = 0; i < _selected.forms.Length && i < 3; i++)
arr[i] = _selected.forms[i];
_selected.forms = arr;
}
// 按 FormDefs 顺序TianHun=0, DiHun=1, MingHun=2
for (int i = 0; i < FormDefs.Length; i++)
{
if (FormDefs[i].type == type)
{
_selected.forms[i] = form;
EditorUtility.SetDirty(_selected);
AssetDatabase.SaveAssets();
return;
}
}
}
// ── UI 辅助 ───────────────────────────────────────────────────────────
private static VisualElement MakeSeparator()
{
var sep = new VisualElement();
sep.style.height = 1;
sep.style.backgroundColor = new Color(0.3f, 0.3f, 0.3f, 0.6f);
sep.style.marginTop = 10;
sep.style.marginBottom = 10;
return sep;
}
}
}

View File

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