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:
835
Assets/_Game/Scripts/Editor/Character/CharacterWizardWindow.cs
Normal file
835
Assets/_Game/Scripts/Editor/Character/CharacterWizardWindow.cs
Normal file
@@ -0,0 +1,835 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Boss;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Enemies;
|
||||
using BaseGames.Equipment;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Parry;
|
||||
using BaseGames.Player;
|
||||
using BaseGames.Player.States;
|
||||
using BaseGames.Skills;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色创建向导(W-01)— 统一入口窗口。
|
||||
/// 技术:UI Toolkit,三标签页(玩家 / 小怪 / Boss)。
|
||||
/// 菜单:BaseGames / Tools / Character Wizard
|
||||
///
|
||||
/// 各标签页均提供:
|
||||
/// ① 当前 SO 资产状态速览(绿色=已存在 / 橙色=缺失)
|
||||
/// ② SO 资产工厂(一键创建所需的所有 ScriptableObject)
|
||||
/// ③ 场景搭建快捷按钮(调用 SceneObjectPlacerTool.PlaceXxx)
|
||||
/// ④ 跳转到对应专项编辑器窗口
|
||||
/// </summary>
|
||||
public class CharacterWizardWindow : EditorWindow
|
||||
{
|
||||
// ── 常量 ──────────────────────────────────────────────────────────────
|
||||
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
|
||||
private const string DataRoot = "Assets/_Game/Data";
|
||||
|
||||
private static StyleSheet _uss;
|
||||
private static StyleSheet Uss =>
|
||||
_uss != null ? _uss : (_uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath));
|
||||
|
||||
[MenuItem("BaseGames/Data/Character Wizard", priority = 1)]
|
||||
public static void Open()
|
||||
{
|
||||
var wnd = GetWindow<CharacterWizardWindow>();
|
||||
wnd.titleContent = new GUIContent("Character Wizard", EditorGUIUtility.IconContent("d_Prefab Icon").image);
|
||||
wnd.minSize = new Vector2(520, 600);
|
||||
}
|
||||
|
||||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||||
private int _activeTab = 0;
|
||||
private Button _btnPlayer, _btnEnemy, _btnBoss;
|
||||
|
||||
// SO 状态缓存(避免每帧重查)
|
||||
private List<(string label, bool exists)> _playerSOStatus = new();
|
||||
private List<(string label, bool exists)> _enemySOStatus = new();
|
||||
private List<(string label, bool exists)> _bossSOStatus = new();
|
||||
private double _lastRefreshTime;
|
||||
|
||||
// 小怪类型选择
|
||||
private int _enemyTypeIndex = 0;
|
||||
private static readonly string[] EnemyTypeLabels = { "普通(近战)", "远程", "飞行" };
|
||||
|
||||
// Boss 命名字段
|
||||
private string _bossId = "NewBoss";
|
||||
private string _enemyId = "NewEnemy";
|
||||
private string _playerId = "Player";
|
||||
|
||||
// SO 状态面板(按标签页缓存)
|
||||
private VisualElement _playerStatusPanel;
|
||||
private VisualElement _enemyStatusPanel;
|
||||
private VisualElement _bossStatusPanel;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
public void CreateGUI()
|
||||
{
|
||||
if (Uss != null)
|
||||
rootVisualElement.styleSheets.Add(Uss);
|
||||
|
||||
rootVisualElement.style.flexDirection = FlexDirection.Column;
|
||||
|
||||
BuildTabBar();
|
||||
BuildTabContents();
|
||||
RefreshSOStatus();
|
||||
SwitchTab(0);
|
||||
}
|
||||
|
||||
private void OnFocus() => RefreshSOStatus();
|
||||
|
||||
// ── 标签栏 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildTabBar()
|
||||
{
|
||||
var bar = new VisualElement();
|
||||
bar.AddToClassList("tab-bar");
|
||||
|
||||
_btnPlayer = MakeTabButton("玩家", () => SwitchTab(0));
|
||||
_btnEnemy = MakeTabButton("小怪", () => SwitchTab(1));
|
||||
_btnBoss = MakeTabButton("Boss", () => SwitchTab(2));
|
||||
|
||||
bar.Add(_btnPlayer);
|
||||
bar.Add(_btnEnemy);
|
||||
bar.Add(_btnBoss);
|
||||
rootVisualElement.Add(bar);
|
||||
}
|
||||
|
||||
private Button MakeTabButton(string label, Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = label };
|
||||
btn.AddToClassList("tab-btn");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void SwitchTab(int idx)
|
||||
{
|
||||
_activeTab = idx;
|
||||
var tabs = rootVisualElement.Query<VisualElement>(className: "tab-content").ToList();
|
||||
for (int i = 0; i < tabs.Count; i++)
|
||||
tabs[i].style.display = (i == idx) ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
_btnPlayer.EnableInClassList("tab-btn--active", idx == 0);
|
||||
_btnEnemy .EnableInClassList("tab-btn--active", idx == 1);
|
||||
_btnBoss .EnableInClassList("tab-btn--active", idx == 2);
|
||||
}
|
||||
|
||||
// ── 标签页内容 ────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildTabContents()
|
||||
{
|
||||
rootVisualElement.Add(BuildPlayerTab());
|
||||
rootVisualElement.Add(BuildEnemyTab());
|
||||
rootVisualElement.Add(BuildBossTab());
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// 玩家标签页
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private VisualElement BuildPlayerTab()
|
||||
{
|
||||
var root = MakeTabContent();
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产状态"));
|
||||
_playerStatusPanel = new VisualElement();
|
||||
root.Add(_playerStatusPanel);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
|
||||
root.Add(MakeHelpBox("在 Project 中创建下列 ScriptableObject 资产。若已存在则跳过。"));
|
||||
|
||||
var idRow = MakeLabeledTextField("资产名称前缀", _playerId, v => _playerId = v);
|
||||
root.Add(idRow);
|
||||
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton("PlayerStatsSO", () => CreatePlayerStat()));
|
||||
factory.Add(MakeFactoryButton("PlayerMovementConfigSO", () => CreateMovementConfig()));
|
||||
factory.Add(MakeFactoryButton("PlayerAnimationConfigSO", () => CreateAnimConfig()));
|
||||
factory.Add(MakeFactoryButton("FormConfigSO + 3 FormSO", () => CreateFormConfig()));
|
||||
factory.Add(MakeFactoryButton("WeaponSO × 3(形态)", () => CreateFormWeapons()));
|
||||
factory.Add(MakeFactoryButton("DamageSourceSO(连击×3)", () => CreatePlayerDamageSources()));
|
||||
factory.Add(MakeFactoryButton("ParryConfigSO", () => CreateParryConfig()));
|
||||
factory.Add(MakeFactoryButton("ShieldConfigSO", () => CreateShieldConfig()));
|
||||
factory.Add(MakeFactoryButton("EquipmentConfigSO", () => CreateEquipmentConfig()));
|
||||
factory.Add(MakeFactoryButton("CharmCatalogSO", () => CreateCharmCatalog()));
|
||||
root.Add(factory);
|
||||
|
||||
var createAllBtn = new Button(CreateAllPlayerSOs) { text = "★ 一键创建全部 Player SO" };
|
||||
createAllBtn.style.marginTop = 6;
|
||||
createAllBtn.style.height = 26;
|
||||
root.Add(createAllBtn);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||
root.Add(MakeHelpBox("在当前活动场景中放置玩家 GameObject(带完整组件树)。"));
|
||||
|
||||
var sceneGroup = MakeActionGroup();
|
||||
sceneGroup.Add(MakeSceneButton("放置玩家到场景", SceneObjectPlacerTool.PlacePlayer));
|
||||
sceneGroup.Add(MakeSceneButton("指定所有 SO 到场景角色", AssignAllPlayerSOsToScene));
|
||||
sceneGroup.Add(MakeSceneButton("放置地面平台", SceneObjectPlacerTool.PlaceGroundPlatform));
|
||||
sceneGroup.Add(MakeSceneButton("放置存档点", SceneObjectPlacerTool.PlaceSavePoint));
|
||||
root.Add(sceneGroup);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||||
|
||||
var jumpGroup = MakeActionGroup();
|
||||
jumpGroup.Add(MakeJumpButton("武器编辑器", () => Combat.WeaponEditorWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("技能编辑器", () => Skills.SkillEditorWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("形态编辑器", () => FormEditorWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
root.Add(jumpGroup);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// 小怪标签页
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private VisualElement BuildEnemyTab()
|
||||
{
|
||||
var root = MakeTabContent();
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产状态"));
|
||||
_enemySOStatus.Clear();
|
||||
_enemyStatusPanel = new VisualElement();
|
||||
root.Add(_enemyStatusPanel);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ 敌人类型选择"));
|
||||
|
||||
var typeRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginBottom = 4 } };
|
||||
for (int i = 0; i < EnemyTypeLabels.Length; i++)
|
||||
{
|
||||
int captured = i;
|
||||
var btn = new Button(() =>
|
||||
{
|
||||
_enemyTypeIndex = captured;
|
||||
// 高亮激活按钮(简单刷新所有同类按钮样式)
|
||||
RefreshEnemyTypeButtons(root);
|
||||
})
|
||||
{ text = EnemyTypeLabels[i] };
|
||||
btn.name = $"enemy-type-{i}";
|
||||
btn.EnableInClassList("type-btn--active", i == _enemyTypeIndex);
|
||||
typeRow.Add(btn);
|
||||
}
|
||||
root.Add(typeRow);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
|
||||
root.Add(MakeHelpBox("每个敌人建议独立命名,便于 Loot / BD 资产管理。"));
|
||||
|
||||
var idRow = MakeLabeledTextField("敌人 ID", _enemyId, v => _enemyId = v);
|
||||
root.Add(idRow);
|
||||
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton("EnemyStatsSO", () => CreateEnemyStat()));
|
||||
factory.Add(MakeFactoryButton("LootTableSO", () => CreateLootTable()));
|
||||
factory.Add(MakeFactoryButton("AttackPatternSO × 2", () => CreateEnemyAttackPatterns()));
|
||||
factory.Add(MakeFactoryButton("DamageSourceSO", () => CreateEnemyDamageSource()));
|
||||
root.Add(factory);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||
root.Add(MakeHelpBox("根据选中的类型在场景中生成对应的敌人 GameObject。"));
|
||||
|
||||
var sceneGroup = MakeActionGroup();
|
||||
sceneGroup.Add(MakeSceneButton("放置敌人到场景", PlaceSelectedEnemyType));
|
||||
root.Add(sceneGroup);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||||
|
||||
var jumpGroup = MakeActionGroup();
|
||||
jumpGroup.Add(MakeJumpButton("敌人数据管理", () => Enemies.EnemyDataWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
root.Add(jumpGroup);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Boss 标签页
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private VisualElement BuildBossTab()
|
||||
{
|
||||
var root = MakeTabContent();
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产状态"));
|
||||
_bossStatusPanel = new VisualElement();
|
||||
root.Add(_bossStatusPanel);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
|
||||
root.Add(MakeHelpBox("每个 Boss 独立目录:Assets/_Game/Data/Boss/<BossId>/"));
|
||||
|
||||
var idRow = MakeLabeledTextField("Boss ID", _bossId, v => _bossId = v);
|
||||
root.Add(idRow);
|
||||
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton("EnemyStatsSO(Boss)", () => CreateBossStat()));
|
||||
factory.Add(MakeFactoryButton("LootTableSO(Boss)", () => CreateBossLoot()));
|
||||
factory.Add(MakeFactoryButton("AttackPatternSO × 3(阶段)", () => CreateBossAttackPatterns()));
|
||||
factory.Add(MakeFactoryButton("BossSkillSO × 3", () => CreateBossSkills()));
|
||||
factory.Add(MakeFactoryButton("SkillSequenceSO(Phase 1)", () => CreateBossSkillSequence(1)));
|
||||
factory.Add(MakeFactoryButton("SkillSequenceSO(Phase 2)", () => CreateBossSkillSequence(2)));
|
||||
factory.Add(MakeFactoryButton("DamageSourceSO × 3", () => CreateBossDamageSources()));
|
||||
root.Add(factory);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||
|
||||
var sceneGroup = MakeActionGroup();
|
||||
sceneGroup.Add(MakeSceneButton("放置 Boss 到场景", SceneObjectPlacerTool.PlaceBossEnemy));
|
||||
root.Add(sceneGroup);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||||
|
||||
var jumpGroup = MakeActionGroup();
|
||||
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
|
||||
jumpGroup.Add(MakeJumpButton("敌人数据管理", () => Enemies.EnemyDataWindow.Open()));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
root.Add(jumpGroup);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:玩家 ────────────────────────────────────────────────
|
||||
|
||||
private void CreatePlayerStat()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<PlayerStatsSO>(
|
||||
$"{DataRoot}/Player", "PLY_PlayerStats");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateMovementConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<PlayerMovementConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_PlayerMovementConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateAnimConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<PlayerAnimationConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_PlayerAnimationConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateFormConfig()
|
||||
{
|
||||
string configDir = $"{DataRoot}/Player";
|
||||
string formsDir = $"{DataRoot}/Player/Forms";
|
||||
EditorScaffoldUtils.EnsureFolder(formsDir);
|
||||
|
||||
var cfg = EditorScaffoldUtils.CreateSOAsset<FormConfigSO>(configDir, "PLY_FormConfig");
|
||||
if (cfg == null) cfg = AssetDatabase.LoadAssetAtPath<FormConfigSO>($"{configDir}/PLY_FormConfig.asset");
|
||||
|
||||
var formTypes = new[] { ("TianHun", FormType.TianHun, "天魂"), ("DiHun", FormType.DiHun, "地魂"), ("MingHun", FormType.MingHun, "命魂") };
|
||||
var forms = new List<FormSO>();
|
||||
foreach (var (id, ftype, dname) in formTypes)
|
||||
{
|
||||
string path = $"{formsDir}/PLY_Form_{id}.asset";
|
||||
var form = AssetDatabase.LoadAssetAtPath<FormSO>(path);
|
||||
if (form == null)
|
||||
{
|
||||
form = ScriptableObject.CreateInstance<FormSO>();
|
||||
form.formId = $"Form_{id}";
|
||||
form.displayName = dname;
|
||||
form.formType = ftype;
|
||||
AssetDatabase.CreateAsset(form, path);
|
||||
}
|
||||
forms.Add(form);
|
||||
}
|
||||
|
||||
if (cfg != null && (cfg.forms == null || cfg.forms.Length == 0))
|
||||
{
|
||||
cfg.forms = forms.ToArray();
|
||||
EditorUtility.SetDirty(cfg);
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorGUIUtility.PingObject(cfg);
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateFormWeapons()
|
||||
{
|
||||
string dir = $"{DataRoot}/Combat/Weapons";
|
||||
foreach (var id in new[] { "TianHun", "DiHun", "MingHun" })
|
||||
EditorScaffoldUtils.CreateSOAsset<WeaponSO>(dir, $"WPN_{id}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreatePlayerDamageSources()
|
||||
{
|
||||
string dir = $"{DataRoot}/Combat/DamageSources";
|
||||
foreach (var label in new[] { "Attack1", "Attack2", "Attack3" })
|
||||
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"CMB_Player_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:玩家(Config 类)────────────────────────────────────────
|
||||
|
||||
private void CreateParryConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<ParryConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_ParryConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateShieldConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<ShieldConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_ShieldConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateEquipmentConfig()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<EquipmentConfigSO>(
|
||||
$"{DataRoot}/Player", "PLY_EquipmentConfig");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateCharmCatalog()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<CharmCatalogSO>(
|
||||
$"{DataRoot}/Progression/Charms", "PLY_CharmCatalog");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一键创建全部 Player 所需 SO(已存在则跳过)。
|
||||
/// 完成后提示用户点击"指定所有 SO 到场景角色"完成绑定。
|
||||
/// </summary>
|
||||
private void CreateAllPlayerSOs()
|
||||
{
|
||||
CreatePlayerStat();
|
||||
CreateMovementConfig();
|
||||
CreateAnimConfig();
|
||||
CreateFormConfig();
|
||||
CreateFormWeapons();
|
||||
CreatePlayerDamageSources();
|
||||
CreateParryConfig();
|
||||
CreateShieldConfig();
|
||||
CreateEquipmentConfig();
|
||||
CreateCharmCatalog();
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.DisplayDialog("创建完成",
|
||||
"全部 Player SO 已创建(已存在的跳过)。\n" +
|
||||
"请在场景中放置角色后,点击「▣ 指定所有 SO 到场景角色」完成绑定。",
|
||||
"确定");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找场景中的 PlayerController,将项目中已存在的配置 SO 全部指定给对应组件字段。
|
||||
/// 使用 SerializedObject 赋值,自动标记 dirty 并保存。
|
||||
/// </summary>
|
||||
private void AssignAllPlayerSOsToScene()
|
||||
{
|
||||
var pc = UnityEngine.Object.FindObjectOfType<PlayerController>();
|
||||
if (pc == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog("未找到角色",
|
||||
"场景中没有 PlayerController。\n请先使用「▣ 放置玩家到场景」。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
var plyGo = pc.gameObject;
|
||||
int count = 0;
|
||||
|
||||
// 通过 SerializedObject 写入 SerializeField(支持撤销)
|
||||
var missing = new System.Collections.Generic.List<string>();
|
||||
|
||||
void TryAssign<T>(Component comp, string field) where T : ScriptableObject
|
||||
{
|
||||
if (comp == null) return;
|
||||
var so = EditorScaffoldUtils.FindAllAssetsOfType<T>().FirstOrDefault();
|
||||
if (so == null)
|
||||
{
|
||||
missing.Add($"{typeof(T).Name}({comp.GetType().Name}.{field})");
|
||||
return;
|
||||
}
|
||||
var sObj = new SerializedObject(comp);
|
||||
var prop = sObj.FindProperty(field);
|
||||
if (prop == null) return;
|
||||
prop.objectReferenceValue = so;
|
||||
sObj.ApplyModifiedProperties();
|
||||
count++;
|
||||
}
|
||||
|
||||
var stats = plyGo.GetComponent<PlayerStats>();
|
||||
var movement = plyGo.GetComponent<PlayerMovement>();
|
||||
var form = plyGo.GetComponent<FormController>();
|
||||
var parry = plyGo.GetComponent<ParrySystem>();
|
||||
var shield = plyGo.GetComponent<ShieldComponent>();
|
||||
var equip = plyGo.GetComponent<EquipmentManager>();
|
||||
var wall = plyGo.GetComponent<PlayerWallDetector>();
|
||||
|
||||
// PlayerStats
|
||||
TryAssign<PlayerStatsSO> (stats, "_config");
|
||||
// PlayerMovement
|
||||
TryAssign<PlayerMovementConfigSO> (movement, "_config");
|
||||
// PlayerController(多个字段)
|
||||
TryAssign<PlayerMovementConfigSO> (pc, "_movementConfig");
|
||||
TryAssign<PlayerAnimationConfigSO> (pc, "_animConfig");
|
||||
TryAssign<InputReaderSO> (pc, "_inputReader");
|
||||
TryAssign<FormConfigSO> (pc, "_formConfig");
|
||||
// FormController
|
||||
TryAssign<FormConfigSO> (form, "_config");
|
||||
// ParrySystem
|
||||
TryAssign<ParryConfigSO> (parry, "_config");
|
||||
// ShieldComponent
|
||||
TryAssign<ShieldConfigSO> (shield, "_config");
|
||||
// EquipmentManager
|
||||
TryAssign<EquipmentConfigSO> (equip, "_config");
|
||||
TryAssign<CharmCatalogSO> (equip, "_charmCatalog");
|
||||
// PlayerWallDetector(复用移动配置)
|
||||
TryAssign<PlayerMovementConfigSO> (wall, "_config");
|
||||
|
||||
EditorUtility.SetDirty(plyGo);
|
||||
RefreshSOStatus();
|
||||
|
||||
string msg = $"已将 {count} 个 SO 引用指定到场景角色 [{plyGo.name}]。";
|
||||
if (missing.Count > 0)
|
||||
msg += $"\n\n★ 以下 SO 尚未创建,请先点击工厂按钮创建后再次指定:\n • " +
|
||||
string.Join("\n • ", missing);
|
||||
else
|
||||
msg += "\n全部 SO 绑定完成,可在 Inspector 中确认各组件字段。";
|
||||
EditorUtility.DisplayDialog("指定完成", msg, "确定");
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:小怪 ────────────────────────────────────────────────
|
||||
|
||||
private void CreateEnemyStat()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_enemyId}_Stats");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateLootTable()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_enemyId}_Loot");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateEnemyAttackPatterns()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
foreach (var label in new[] { "Melee", "Ranged" })
|
||||
EditorScaffoldUtils.CreateSOAsset<AttackPatternSO>(dir, $"ENM_{_enemyId}_Pattern_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateEnemyDamageSource()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"ENM_{_enemyId}_DS");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:Boss ─────────────────────────────────────────────────
|
||||
|
||||
private void CreateBossStat()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}";
|
||||
EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_bossId}_Stats");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossLoot()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}";
|
||||
EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_bossId}_Loot");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossAttackPatterns()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}/Patterns";
|
||||
foreach (var label in new[] { "Phase1", "Phase2_A", "Phase2_B" })
|
||||
EditorScaffoldUtils.CreateSOAsset<AttackPatternSO>(dir, $"ENM_{_bossId}_Pattern_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossSkills()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}/Skills";
|
||||
foreach (var label in new[] { "Skill_Slam", "Skill_Sweep", "Skill_Summon" })
|
||||
EditorScaffoldUtils.CreateSOAsset<BossSkillSO>(dir, $"SKL_{_bossId}_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossSkillSequence(int phase)
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}/Skills";
|
||||
EditorScaffoldUtils.CreateSOAsset<SkillSequenceSO>(dir, $"SKL_{_bossId}_Phase{phase}_Sequence");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossDamageSources()
|
||||
{
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}/DamageSources";
|
||||
foreach (var label in new[] { "Slam", "Sweep", "Projectile" })
|
||||
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"ENM_{_bossId}_DS_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
// ── 场景搭建 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void PlaceSelectedEnemyType()
|
||||
{
|
||||
switch (_enemyTypeIndex)
|
||||
{
|
||||
case 0: SceneObjectPlacerTool.PlaceEnemy(); break;
|
||||
case 1: SceneObjectPlacerTool.PlaceEnemy(); break; // 复用,类型通过 SO 区分
|
||||
case 2: SceneObjectPlacerTool.PlaceEnemy(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── SO 状态面板刷新 ───────────────────────────────────────────────────
|
||||
|
||||
private void RefreshSOStatus()
|
||||
{
|
||||
_lastRefreshTime = EditorApplication.timeSinceStartup;
|
||||
BuildPlayerStatus();
|
||||
BuildEnemyStatus();
|
||||
BuildBossStatus();
|
||||
}
|
||||
|
||||
private void BuildPlayerStatus()
|
||||
{
|
||||
if (_playerStatusPanel == null) return;
|
||||
_playerStatusPanel.Clear();
|
||||
|
||||
var checks = new (string label, UnityEngine.Object asset)[]
|
||||
{
|
||||
("PlayerStatsSO", FindFirst<PlayerStatsSO>()),
|
||||
("PlayerMovementConfigSO", FindFirst<PlayerMovementConfigSO>()),
|
||||
("PlayerAnimationConfigSO", FindFirst<PlayerAnimationConfigSO>()),
|
||||
("FormConfigSO", FindFirst<FormConfigSO>()),
|
||||
("FormSO(天魂)", FindFormOfType(FormType.TianHun)),
|
||||
("FormSO(地魂)", FindFormOfType(FormType.DiHun)),
|
||||
("FormSO(命魂)", FindFormOfType(FormType.MingHun)),
|
||||
("WeaponSO(≥3)", FindFirstIfCount<WeaponSO>(3)),
|
||||
("ParryConfigSO", FindFirst<ParryConfigSO>()),
|
||||
("ShieldConfigSO", FindFirst<ShieldConfigSO>()),
|
||||
("EquipmentConfigSO", FindFirst<EquipmentConfigSO>()),
|
||||
("CharmCatalogSO", FindFirst<CharmCatalogSO>()),
|
||||
};
|
||||
|
||||
_playerStatusPanel.Add(MakeStatusGrid(checks));
|
||||
}
|
||||
|
||||
private void BuildEnemyStatus()
|
||||
{
|
||||
if (_enemyStatusPanel == null) return;
|
||||
_enemyStatusPanel.Clear();
|
||||
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
var checks = new (string label, UnityEngine.Object asset)[]
|
||||
{
|
||||
("EnemyStatsSO", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{_enemyId}_Stats.asset")),
|
||||
("LootTableSO", FindAtPath<LootTableSO>($"{dir}/ENM_{_enemyId}_Loot.asset")),
|
||||
("AttackPatternSO×2", FindAtPath<AttackPatternSO>($"{dir}/ENM_{_enemyId}_Pattern_Melee.asset")),
|
||||
("DamageSourceSO", FindAtPath<DamageSourceSO>($"{dir}/ENM_{_enemyId}_DS.asset")),
|
||||
};
|
||||
|
||||
_enemyStatusPanel.Add(MakeStatusGrid(checks));
|
||||
}
|
||||
|
||||
private void BuildBossStatus()
|
||||
{
|
||||
if (_bossStatusPanel == null) return;
|
||||
_bossStatusPanel.Clear();
|
||||
|
||||
string dir = $"{DataRoot}/Boss/{_bossId}";
|
||||
var checks = new (string label, UnityEngine.Object asset)[]
|
||||
{
|
||||
("EnemyStatsSO(Boss)", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{_bossId}_Stats.asset")),
|
||||
("LootTableSO", FindAtPath<LootTableSO>($"{dir}/ENM_{_bossId}_Loot.asset")),
|
||||
("AttackPatternSO(Phase1)", FindAtPath<AttackPatternSO>($"{dir}/Patterns/ENM_{_bossId}_Pattern_Phase1.asset")),
|
||||
("BossSkillSO(≥1)", EditorScaffoldUtils.FindAllAssetsOfType<BossSkillSO>()
|
||||
.FirstOrDefault(s => s.name.StartsWith("SKL_" + _bossId, StringComparison.OrdinalIgnoreCase))),
|
||||
("SkillSequenceSO(Phase1)", FindAtPath<SkillSequenceSO>($"{dir}/Skills/SKL_{_bossId}_Phase1_Sequence.asset")),
|
||||
};
|
||||
|
||||
_bossStatusPanel.Add(MakeStatusGrid(checks));
|
||||
}
|
||||
|
||||
// ── 辅助:状态格 ─────────────────────────────────────────────────────
|
||||
|
||||
private static VisualElement MakeStatusGrid((string label, UnityEngine.Object asset)[] items)
|
||||
{
|
||||
var grid = new VisualElement();
|
||||
grid.style.flexDirection = FlexDirection.Row;
|
||||
grid.style.flexWrap = Wrap.Wrap;
|
||||
grid.style.marginBottom = 6;
|
||||
|
||||
foreach (var (label, asset) in items)
|
||||
{
|
||||
bool exists = asset != null;
|
||||
VisualElement chip;
|
||||
if (exists)
|
||||
{
|
||||
var captured = asset;
|
||||
var btn = new Button(() =>
|
||||
{
|
||||
EditorGUIUtility.PingObject(captured);
|
||||
Selection.activeObject = captured;
|
||||
}) { text = $"✔ {label}" };
|
||||
chip = btn;
|
||||
}
|
||||
else
|
||||
{
|
||||
chip = new Label($"✘ {label}");
|
||||
}
|
||||
|
||||
chip.style.marginRight = 6;
|
||||
chip.style.marginBottom = 4;
|
||||
chip.style.paddingLeft = 6;
|
||||
chip.style.paddingRight = 6;
|
||||
chip.style.paddingTop = 2;
|
||||
chip.style.paddingBottom = 2;
|
||||
chip.style.borderTopLeftRadius = 4;
|
||||
chip.style.borderTopRightRadius = 4;
|
||||
chip.style.borderBottomLeftRadius = 4;
|
||||
chip.style.borderBottomRightRadius = 4;
|
||||
|
||||
if (exists)
|
||||
{
|
||||
chip.style.backgroundColor = new Color(0.18f, 0.55f, 0.22f, 0.85f);
|
||||
chip.style.color = new Color(0.85f, 1.00f, 0.85f);
|
||||
}
|
||||
else
|
||||
{
|
||||
chip.style.backgroundColor = new Color(0.65f, 0.35f, 0.05f, 0.85f);
|
||||
chip.style.color = new Color(1.00f, 0.90f, 0.70f);
|
||||
}
|
||||
|
||||
grid.Add(chip);
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
// ── 辅助:UI 组件构建 ─────────────────────────────────────────────────
|
||||
|
||||
private static VisualElement MakeTabContent()
|
||||
{
|
||||
var root = new ScrollView(ScrollViewMode.Vertical);
|
||||
root.AddToClassList("tab-content");
|
||||
root.style.flexGrow = 1;
|
||||
root.contentContainer.style.paddingLeft = 8;
|
||||
root.contentContainer.style.paddingRight = 8;
|
||||
root.contentContainer.style.paddingTop = 8;
|
||||
root.contentContainer.style.paddingBottom = 8;
|
||||
return root;
|
||||
}
|
||||
|
||||
private static Label MakeSectionHeader(string text)
|
||||
{
|
||||
var lbl = new Label(text);
|
||||
lbl.AddToClassList("section-header");
|
||||
return lbl;
|
||||
}
|
||||
|
||||
private static VisualElement MakeActionGroup()
|
||||
{
|
||||
var group = new VisualElement();
|
||||
group.AddToClassList("action-buttons");
|
||||
return group;
|
||||
}
|
||||
|
||||
private static Button MakeFactoryButton(string label, Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = $"+ {label}" };
|
||||
btn.AddToClassList("wizard-factory-btn");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static Button MakeSceneButton(string label, Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = $"▣ {label}" };
|
||||
btn.AddToClassList("wizard-scene-btn");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static Button MakeJumpButton(string label, Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = $"⇒ {label}" };
|
||||
btn.AddToClassList("wizard-jump-btn");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static HelpBox MakeHelpBox(string text)
|
||||
{
|
||||
return new HelpBox(text, HelpBoxMessageType.Info);
|
||||
}
|
||||
|
||||
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 = 8;
|
||||
sep.style.marginBottom = 8;
|
||||
return sep;
|
||||
}
|
||||
|
||||
private static VisualElement MakeLabeledTextField(string label, string value, Action<string> onChange)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.marginBottom = 4;
|
||||
row.style.alignItems = Align.Center;
|
||||
|
||||
var lbl = new Label(label) { style = { minWidth = 110, marginRight = 6 } };
|
||||
var field = new TextField { value = value, style = { flexGrow = 1 } };
|
||||
field.RegisterValueChangedCallback(e => onChange(e.newValue));
|
||||
row.Add(lbl);
|
||||
row.Add(field);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void RefreshEnemyTypeButtons(VisualElement tabRoot)
|
||||
{
|
||||
for (int i = 0; i < EnemyTypeLabels.Length; i++)
|
||||
{
|
||||
var btn = tabRoot.Q<Button>($"enemy-type-{i}");
|
||||
btn?.EnableInClassList("type-btn--active", i == _enemyTypeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 资产查找辅助 ──────────────────────────────────────────
|
||||
|
||||
private static T FindFirst<T>() where T : ScriptableObject
|
||||
=> EditorScaffoldUtils.FindAllAssetsOfType<T>().FirstOrDefault();
|
||||
|
||||
/// <summary>返回第一个,仅当总数达到 minCount 时才认为满足。</summary>
|
||||
private static T FindFirstIfCount<T>(int minCount) where T : ScriptableObject
|
||||
{
|
||||
var list = EditorScaffoldUtils.FindAllAssetsOfType<T>();
|
||||
return list.Count >= minCount ? list[0] : null;
|
||||
}
|
||||
|
||||
private static T FindAtPath<T>(string path) where T : UnityEngine.Object
|
||||
=> AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
|
||||
private static FormSO FindFormOfType(FormType type)
|
||||
=> EditorScaffoldUtils.FindAllAssetsOfType<FormSO>()
|
||||
.FirstOrDefault(f => f.formType == type);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user