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:
413
Assets/_Game/Scripts/Editor/Player/FormEditorWindow.cs
Normal file
413
Assets/_Game/Scripts/Editor/Player/FormEditorWindow.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Player/FormEditorWindow.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Player/FormEditorWindow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15618e4fc32a98346a68e945428fcb47
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user