UI 系统
This commit is contained in:
@@ -323,11 +323,12 @@ namespace BaseGames.Editor
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton("ENM_ChaoFeng_Stats.asset", () => { CreateChaoFengStatsSO(); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ENM_ChaoFeng_AnimConfig.asset",() => { CreateChaoFengAnimConfigSO(); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Idle.asset", () => { CreateChaoFengSkillSO("Idle", "chaofeng_idle"); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Slam.asset", () => { CreateChaoFengSkillSO("Slam", "chaofeng_slam"); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Sweep.asset", () => { CreateChaoFengSkillSO("Sweep", "chaofeng_sweep"); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_WindBlade.asset", () => { CreateChaoFengSkillSO("WindBlade", "chaofeng_windblade"); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Summon.asset", () => { CreateChaoFengSkillSO("Summon", "chaofeng_summon"); RefreshSOStatus(); }));
|
||||
foreach (var (skName, skId, skPhases, skWeight) in ChaoFengSkillDefs)
|
||||
{
|
||||
string cName = skName; string cId = skId; int[] cPhases = skPhases; float cWeight = skWeight;
|
||||
factory.Add(MakeFactoryButton($"ABL_ChaoFeng_{cName}.asset",
|
||||
() => { CreateChaoFengSkillSO(cName, cId, cPhases, cWeight); RefreshSOStatus(); }));
|
||||
}
|
||||
root.Add(factory);
|
||||
|
||||
var createAllBtn = new Button(() => { CreateAllChaoFengSOs(); RefreshSOStatus(); })
|
||||
@@ -570,10 +571,10 @@ namespace BaseGames.Editor
|
||||
private static (string ablName, string ablId)[] GetEnemyAbilityDefs(string enemyId) => enemyId switch
|
||||
{
|
||||
"E001" => new[] { ("Alert", "e001_alert"), ("Chase", "e001_chase") },
|
||||
"E002" => new[] { ("Strike", "e002_strike") },
|
||||
"E002" => new[] { ("CeilingStrike", "e002_ceiling_strike") },
|
||||
"E003" => new[] { ("Fall", "e003_fall") },
|
||||
"E004" => new[] { ("Bite", "e004_bite"), ("Slam", "e004_slam"), ("Acid", "e004_acid"),
|
||||
("Charge", "e004_charge"), ("Chase", "e004_chase") },
|
||||
"E004" => new[] { ("Appear", "e004_appear"), ("Bite", "e004_bite"), ("HeadSlam", "e004_headslam"),
|
||||
("Acid", "e004_acid"), ("Flip", "e004_flip") },
|
||||
"E005" => new[] { ("Bite", "e005_bite"), ("Acid", "e005_acid") },
|
||||
"E006" => new[] { ("Leap", "e006_leap"), ("Chase", "e006_chase") },
|
||||
_ => System.Array.Empty<(string, string)>(),
|
||||
@@ -669,26 +670,38 @@ namespace BaseGames.Editor
|
||||
EditorScaffoldUtils.CreateSOAsset<EnemyAnimationConfigSO>(dir, "ENM_ChaoFeng_AnimConfig");
|
||||
}
|
||||
|
||||
private static void CreateChaoFengSkillSO(string skillName, string skillId)
|
||||
private static void CreateChaoFengSkillSO(string skillName, string skillId, int[] phaseIndices, float weight)
|
||||
{
|
||||
string dir = "Assets/_Game/Data/Enemies/ChaoFeng/Abilities";
|
||||
string name = $"ABL_ChaoFeng_{skillName}";
|
||||
var so = EditorScaffoldUtils.CreateSOAsset<BossSkillSO>(dir, name);
|
||||
if (so != null)
|
||||
{
|
||||
so.skillId = skillId;
|
||||
so.displayName = skillName;
|
||||
so.availablePhaseIndices = phaseIndices;
|
||||
so.weight = weight;
|
||||
EditorUtility.SetDirty(so);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>嘲风技能集(计划):Phase0 四技能加权随机 + Phase1 风石。</summary>
|
||||
private static readonly (string name, string id, int[] phases, float weight)[] ChaoFengSkillDefs =
|
||||
{
|
||||
("Boomerang", "boomerang", new[] { 0 }, 1.0f),
|
||||
("FanCombo", "fan_combo", new[] { 0 }, 1.5f),
|
||||
("TornadoSmall", "tornado_small", new[] { 0 }, 1.2f),
|
||||
("TornadoLarge", "tornado_large", new[] { 0 }, 0.8f),
|
||||
("WindStone", "wind_stone", new[] { 1 }, 1.0f),
|
||||
};
|
||||
|
||||
private static void CreateAllChaoFengSOs()
|
||||
{
|
||||
CreateChaoFengStatsSO();
|
||||
CreateChaoFengAnimConfigSO();
|
||||
foreach (var (n, id) in new[] { ("Idle","chaofeng_idle"), ("Slam","chaofeng_slam"),
|
||||
("Sweep","chaofeng_sweep"), ("WindBlade","chaofeng_windblade"),
|
||||
("Summon","chaofeng_summon") })
|
||||
CreateChaoFengSkillSO(n, id);
|
||||
foreach (var (n, id, phases, weight) in ChaoFengSkillDefs)
|
||||
CreateChaoFengSkillSO(n, id, phases, weight);
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.DisplayDialog("创建完成",
|
||||
"全部嘲风 SO 已创建(已存在的跳过)。\n放置到场景后检查 BossSkillExecutor._skills 绑定。", "确定");
|
||||
@@ -757,18 +770,15 @@ namespace BaseGames.Editor
|
||||
|
||||
const string dir = "Assets/_Game/Data/Enemies/ChaoFeng";
|
||||
const string ablDir = "Assets/_Game/Data/Enemies/ChaoFeng/Abilities";
|
||||
var checks = new (string label, UnityEngine.Object asset)[]
|
||||
var checks = new List<(string label, UnityEngine.Object asset)>
|
||||
{
|
||||
("ENM_ChaoFeng_Stats", FindAtPath<EnemyStatsSO>($"{dir}/ENM_ChaoFeng_Stats.asset")),
|
||||
("ENM_ChaoFeng_AnimConfig",FindAtPath<EnemyAnimationConfigSO>($"{dir}/ENM_ChaoFeng_AnimConfig.asset")),
|
||||
("ABL_ChaoFeng_Idle", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Idle.asset")),
|
||||
("ABL_ChaoFeng_Slam", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Slam.asset")),
|
||||
("ABL_ChaoFeng_Sweep", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Sweep.asset")),
|
||||
("ABL_ChaoFeng_WindBlade", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_WindBlade.asset")),
|
||||
("ABL_ChaoFeng_Summon", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Summon.asset")),
|
||||
};
|
||||
foreach (var (skName, _, _, _) in ChaoFengSkillDefs)
|
||||
checks.Add(($"ABL_ChaoFeng_{skName}", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_{skName}.asset")));
|
||||
|
||||
_bossStatusPanel.Add(MakeStatusGrid(checks));
|
||||
_bossStatusPanel.Add(MakeStatusGrid(checks.ToArray()));
|
||||
}
|
||||
|
||||
// ── 辅助:状态格 ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -5,6 +5,8 @@ using BaseGames.Boss;
|
||||
using BaseGames.Camera;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Combat.StatusEffects;
|
||||
using BaseGames.Core.Assets;
|
||||
using BaseGames.Core.Pool;
|
||||
using BaseGames.Dialogue;
|
||||
using BaseGames.Enemies;
|
||||
using BaseGames.Enemies.Abilities;
|
||||
@@ -546,37 +548,26 @@ namespace BaseGames.Editor
|
||||
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
|
||||
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||
|
||||
// HurtBox(初始禁用,悬挂阶段无法被攻击)
|
||||
// HurtBox(component.enabled 初始为 false:仅悬挂脆弱窗口期间由能力开启)
|
||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||
hurtCap.isTrigger = true;
|
||||
hurtCap.size = new Vector2(0.45f, 0.65f);
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
hurtBoxT.gameObject.SetActive(false);
|
||||
hurtBox.enabled = false; // 悬挂阶段外不可被攻击;CeilingHangStrikeAbility 在 _loopClip 期间开启
|
||||
|
||||
// LandingHitBox(落地瞬间 AoE,由 CeilingDropAbility 激活)
|
||||
Transform landingHitBoxT = GetOrCreateChild(go.transform, "LandingHitBox");
|
||||
SetLayer(landingHitBoxT.gameObject, "EnemyHitBox", report);
|
||||
BoxCollider2D landingCol = GetOrAddComponent<BoxCollider2D>(landingHitBoxT.gameObject);
|
||||
landingCol.isTrigger = true;
|
||||
landingCol.size = new Vector2(0.8f, 0.3f);
|
||||
HitBox landingHitBox = GetOrAddComponent<HitBox>(landingHitBoxT.gameObject);
|
||||
landingHitBoxT.gameObject.SetActive(false);
|
||||
|
||||
// ContactDamageZone(地面巡逻时造成接触伤害,落地后由行为树启用)
|
||||
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
|
||||
SetLayer(contactT.gameObject, "EnemyHitBox", report);
|
||||
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
|
||||
contactCol.isTrigger = true;
|
||||
contactCol.radius = 0.35f;
|
||||
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
|
||||
GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
|
||||
contactT.gameObject.SetActive(false);
|
||||
// AttackHitBox(钻出啃咬瞬间判定,由 CeilingHangStrikeAbility 激活)
|
||||
Transform attackHitBoxT = GetOrCreateChild(go.transform, "AttackHitBox");
|
||||
SetLayer(attackHitBoxT.gameObject, "EnemyHitBox", report);
|
||||
BoxCollider2D attackCol = GetOrAddComponent<BoxCollider2D>(attackHitBoxT.gameObject);
|
||||
attackCol.isTrigger = true;
|
||||
attackCol.size = new Vector2(0.6f, 0.8f); // 正下方钻出范围
|
||||
HitBox attackHitBox = GetOrAddComponent<HitBox>(attackHitBoxT.gameObject);
|
||||
|
||||
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
||||
Transform dropT = GetOrCreateChild(abilitiesT, "CeilingDropAbility");
|
||||
CeilingDropAbility dropAbility = GetOrAddComponent<CeilingDropAbility>(dropT.gameObject);
|
||||
Transform strikeT = GetOrCreateChild(abilitiesT, "CeilingHangStrikeAbility");
|
||||
CeilingHangStrikeAbility strikeAbility = GetOrAddComponent<CeilingHangStrikeAbility>(strikeT.gameObject);
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E002_Stats");
|
||||
@@ -601,21 +592,18 @@ namespace BaseGames.Editor
|
||||
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
|
||||
report);
|
||||
|
||||
AssignReference(dropAbility, "_landingHitBox", landingHitBox, report);
|
||||
AssignLayerMask(dropAbility, "_groundMask",
|
||||
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
|
||||
report);
|
||||
AssignAsset(strikeAbility, "_config", report, false, "ABL_E002_CeilingStrike");
|
||||
AssignReference(strikeAbility, "_attackHitBox", attackHitBox, report);
|
||||
AssignReference(strikeAbility, "_hurtBox", hurtBox, report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null)
|
||||
{
|
||||
AssignReference(landingHitBox, "_defaultSource", dmgSrc, report);
|
||||
AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
|
||||
}
|
||||
AssignReference(attackHitBox, "_defaultSource", dmgSrc, report);
|
||||
|
||||
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_range" }, report);
|
||||
SetupPerceptionSystemSlots(sensorHub, new[] { "attack_range" }, report);
|
||||
report.Add("★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。");
|
||||
report.Add("★ HurtBox / ContactDamageZone 初始禁用;落地后由行为树激活。");
|
||||
report.Add("★ HurtBox.enabled 初始为 false;钻出后由 CeilingHangStrikeAbility 在悬挂窗口开启。");
|
||||
report.Add("★ attack_range 槽位为正下方 BoxCast(玩家经过检测区);按需在 Inspector 调整 offset/size。");
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。");
|
||||
report.Add("★ BD 树逻辑建议:Idle(悬挂)→ IsSensorDetecting(aggro) → UseAbility(CeilingDrop) → IsGrounded → Patrol(Pace)。");
|
||||
|
||||
@@ -775,27 +763,20 @@ namespace BaseGames.Editor
|
||||
HitBox slamHitBox = GetOrAddComponent<HitBox>(slamT.gameObject);
|
||||
slamT.gameObject.SetActive(false);
|
||||
|
||||
Transform chargeHitBoxT = GetOrCreateChild(go.transform, "ChargeHitBox");
|
||||
SetLayer(chargeHitBoxT.gameObject, "EnemyHitBox", report);
|
||||
BoxCollider2D chargeHitCol = GetOrAddComponent<BoxCollider2D>(chargeHitBoxT.gameObject);
|
||||
chargeHitCol.isTrigger = true;
|
||||
chargeHitCol.size = new Vector2(0.9f, 0.8f);
|
||||
HitBox chargeHitBox = GetOrAddComponent<HitBox>(chargeHitBoxT.gameObject);
|
||||
chargeHitBoxT.gameObject.SetActive(false);
|
||||
|
||||
Transform acidMuzzleT = GetOrCreateChild(go.transform, "AcidMuzzle");
|
||||
|
||||
// 能力集(计划 E004):出场 / 撕咬 / 头槌 / 酸液 / 转身
|
||||
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
||||
Transform appearAblT = GetOrCreateChild(abilitiesT, "AppearAbility");
|
||||
AppearAbility appearAbl = GetOrAddComponent<AppearAbility>(appearAblT.gameObject);
|
||||
Transform biteAblT = GetOrCreateChild(abilitiesT, "MeleeAttackAbility_Bite");
|
||||
MeleeAttackAbility biteAbl = GetOrAddComponent<MeleeAttackAbility>(biteAblT.gameObject);
|
||||
Transform slamAblT = GetOrCreateChild(abilitiesT, "RepeatSlamAbility");
|
||||
Transform slamAblT = GetOrCreateChild(abilitiesT, "RepeatSlamAbility_HeadSlam");
|
||||
RepeatSlamAbility slamAbl = GetOrAddComponent<RepeatSlamAbility>(slamAblT.gameObject);
|
||||
Transform acidAblT = GetOrCreateChild(abilitiesT, "ProjectileAttackAbility_Acid");
|
||||
ProjectileAttackAbility acidAbl = GetOrAddComponent<ProjectileAttackAbility>(acidAblT.gameObject);
|
||||
Transform chargeAblT = GetOrCreateChild(abilitiesT, "ChargeAbility");
|
||||
ChargeAbility chargeAbl = GetOrAddComponent<ChargeAbility>(chargeAblT.gameObject);
|
||||
Transform chaseAblT = GetOrCreateChild(abilitiesT, "ContactChaseAbility");
|
||||
ContactChaseAbility chaseAbl = GetOrAddComponent<ContactChaseAbility>(chaseAblT.gameObject);
|
||||
Transform flipAblT = GetOrCreateChild(abilitiesT, "FacePlayerAbility_Flip");
|
||||
FacePlayerAbility flipAbl = GetOrAddComponent<FacePlayerAbility>(flipAblT.gameObject);
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E004_Stats");
|
||||
@@ -821,26 +802,26 @@ namespace BaseGames.Editor
|
||||
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
|
||||
report);
|
||||
|
||||
AssignAsset(biteAbl,"_config", report, false, "ABL_E004_Bite");
|
||||
AssignAsset(slamAbl, "_config", report, false, "ABL_E004_Slam");
|
||||
AssignAsset(acidAbl, "_config", report, false, "ABL_E004_Acid");
|
||||
AssignAsset(chargeAbl, "_config", report, false, "ABL_E004_Charge");
|
||||
AssignAsset(chaseAbl, "_config", report, false, "ABL_E004_Chase");
|
||||
AssignAsset(appearAbl, "_config", report, false, "ABL_E004_Appear");
|
||||
AssignAsset(biteAbl, "_config", report, false, "ABL_E004_Bite");
|
||||
AssignAsset(slamAbl, "_config", report, false, "ABL_E004_HeadSlam");
|
||||
AssignAsset(acidAbl, "_config", report, false, "ABL_E004_Acid");
|
||||
AssignAsset(flipAbl, "_config", report, false, "ABL_E004_Flip");
|
||||
|
||||
AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report);
|
||||
AssignReference(slamAbl, "_hitBox", slamHitBox, report);
|
||||
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
|
||||
AssignReference(chargeAbl, "_chargeHitBox", chargeHitBox, report);
|
||||
AssignReference(slamAbl, "_hitBox", slamHitBox, report);
|
||||
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null)
|
||||
{
|
||||
AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
|
||||
AssignReference(slamHitBox, "_defaultSource", dmgSrc, report);
|
||||
AssignReference(chargeHitBox, "_defaultSource", dmgSrc, report);
|
||||
AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
|
||||
AssignReference(slamHitBox, "_defaultSource", dmgSrc, report);
|
||||
}
|
||||
|
||||
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
|
||||
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "sight" }, report);
|
||||
report.Add("★ AppearAbility._appearClip / FacePlayerAbility._faceClip 等动画 Clip 待美术接入后在 Inspector 指定。");
|
||||
report.Add("★ 在 E004_ZhiMu._deathPreClip 配置死亡前摇动画(两阶段死亡 Death_Pre 无敌)。");
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
@@ -1106,20 +1087,21 @@ namespace BaseGames.Editor
|
||||
hurtCap.size = new Vector2(1.1f, 1.9f);
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
|
||||
// Phase1 attack hitboxes (disabled by default; abilities enable/disable as needed)
|
||||
HitBox biteHB = CreateDisabledHitBox(go.transform, "Phase1_BiteHitBox", "EnemyHitBox",
|
||||
true, report, size: new Vector2(0.8f, 0.5f));
|
||||
HitBox swipeR = CreateDisabledHitBox(go.transform, "Phase1_SwipeHitBox_R","EnemyHitBox",
|
||||
true, report, size: new Vector2(1.2f, 0.4f));
|
||||
HitBox swipeL = CreateDisabledHitBox(go.transform, "Phase1_SwipeHitBox_L","EnemyHitBox",
|
||||
true, report, size: new Vector2(1.2f, 0.4f));
|
||||
HitBox stompHB = CreateDisabledHitBox(go.transform, "Phase1_StompHitBox", "EnemyHitBox",
|
||||
false, report, radius: 1.0f);
|
||||
// Phase1 攻击 HitBox(默认禁用;技能执行时由 BossSkillExecutor 开关)。
|
||||
// 计划:挥扇三连 FanCombo ×3 + 龙卷接触 Tornado。
|
||||
HitBox fan1 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_1", "EnemyHitBox",
|
||||
true, report, size: new Vector2(1.0f, 0.5f));
|
||||
HitBox fan2 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_2", "EnemyHitBox",
|
||||
true, report, size: new Vector2(1.0f, 0.5f));
|
||||
HitBox fan3 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_3", "EnemyHitBox",
|
||||
true, report, size: new Vector2(1.2f, 0.6f));
|
||||
HitBox tornadoHB = CreateDisabledHitBox(go.transform, "Phase1_Tornado_HitBox", "EnemyHitBox",
|
||||
true, report, size: new Vector2(0.6f, 1.2f));
|
||||
|
||||
// Muzzle transforms for Phase 2 skills
|
||||
GetOrCreateChild(go.transform, "WindBladeMuzzle");
|
||||
GetOrCreateChild(go.transform, "TornadoMuzzle");
|
||||
GetOrCreateChild(go.transform, "SummonSpawnPoint");
|
||||
// 弹体发射点(Phase1 回旋扇 / 龙卷;Phase2 风石)
|
||||
Transform boomerangMuzzleT = GetOrCreateChild(go.transform, "BoomerangMuzzle");
|
||||
Transform tornadoMuzzleT = GetOrCreateChild(go.transform, "TornadoMuzzle");
|
||||
Transform windStoneMuzzleT = GetOrCreateChild(go.transform, "WindStoneMuzzle");
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(bossBase, "_statsSO", report, false, "ENM_ChaoFeng_Stats");
|
||||
@@ -1133,6 +1115,22 @@ namespace BaseGames.Editor
|
||||
AssignReference(bossBase, "_hurtBox", hurtBox, report);
|
||||
AssignReference(skillExec, "_animancer", animancer, report);
|
||||
|
||||
// 浮空 / 击落 / 弹体发射点接线(计划)
|
||||
AssignReference(bossBase, "_floatController", floatCtrl, report);
|
||||
AssignReference(bossBase, "_knockdownCounter", knockdown, report);
|
||||
AssignReference(bossBase, "_boomerangMuzzle", boomerangMuzzleT, report);
|
||||
AssignReference(bossBase, "_tornadoMuzzle", tornadoMuzzleT, report);
|
||||
AssignReference(bossBase, "_windStoneMuzzle", windStoneMuzzleT, report);
|
||||
AssignReference(floatCtrl, "_rb", rb, report);
|
||||
AssignReference(knockdown, "_boss", bossBase, report);
|
||||
AssignReference(knockdown, "_floatCtrl", floatCtrl, report);
|
||||
|
||||
// 弹体配置接线(ProjectileConfigSO,存在时自动绑定)
|
||||
AssignAsset(bossBase, "_boomerangConfig", report, false, "PROJ_Boomerang_Config");
|
||||
AssignAsset(bossBase, "_tornadoSmallConfig", report, false, "PROJ_TornadoSmall_Config");
|
||||
AssignAsset(bossBase, "_tornadoLargeConfig", report, false, "PROJ_TornadoLarge_Config");
|
||||
AssignAsset(bossBase, "_windStoneConfig", report, false, "PROJ_WindStone_Config");
|
||||
|
||||
AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
AssignAsset(bossBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||||
AssignAsset(bossBase, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
|
||||
@@ -1150,30 +1148,34 @@ namespace BaseGames.Editor
|
||||
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
|
||||
report);
|
||||
|
||||
// Collect BossSkillSOs and assign to executor
|
||||
// 收集 BossSkillSO 并赋给执行器(计划技能集)
|
||||
var skillAssets = new System.Collections.Generic.List<Object>();
|
||||
foreach (var n in new[] { "ABL_ChaoFeng_Idle", "ABL_ChaoFeng_Slam", "ABL_ChaoFeng_Sweep",
|
||||
"ABL_ChaoFeng_WindBlade", "ABL_ChaoFeng_Summon" })
|
||||
foreach (var n in new[] { "ABL_ChaoFeng_Boomerang", "ABL_ChaoFeng_FanCombo",
|
||||
"ABL_ChaoFeng_TornadoSmall", "ABL_ChaoFeng_TornadoLarge",
|
||||
"ABL_ChaoFeng_WindStone" })
|
||||
{
|
||||
Object sk = FindFirstAsset(n);
|
||||
if (sk != null) skillAssets.Add(sk);
|
||||
else report.Add($"未找到 BossSkillSO:{n},请先一键创建 SO 后再重新运行此放置操作。");
|
||||
else report.Add($"未找到 BossSkillSO:{n},请先一键创建 ChaoFeng SO 后再重新运行此放置操作。");
|
||||
}
|
||||
if (skillAssets.Count > 0)
|
||||
AssignObjectArray(skillExec, "_skills", skillAssets.ToArray(), report);
|
||||
|
||||
AssignString(skillExec, "_bossId", "ChaoFeng", report);
|
||||
AssignObjectArray(skillExec, "_hitBoxes", new Object[] { fan1, fan2, fan3, tornadoHB }, report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null)
|
||||
{
|
||||
foreach (var hb in new[] { biteHB, swipeR, swipeL, stompHB })
|
||||
foreach (var hb in new[] { fan1, fan2, fan3, tornadoHB })
|
||||
if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report);
|
||||
}
|
||||
|
||||
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
|
||||
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "sight" }, report);
|
||||
|
||||
report.Add("★ 设置 BossSkillExecutor._bossId = \"ChaoFeng\"。");
|
||||
report.Add("★ 将各 Phase1 HitBox 引用拖入 BossSkillExecutor._hitBoxes 数组。");
|
||||
report.Add("★ 将 WindBladeMuzzle / TornadoMuzzle / SummonSpawnPoint 拖入对应 BossSkillSO 字段。");
|
||||
report.Add("★ FanCombo 三段 HitBox 与 Tornado HitBox 已挂入 BossSkillExecutor._hitBoxes。");
|
||||
report.Add("★ 将 BoomerangMuzzle / TornadoMuzzle / WindStoneMuzzle 拖入对应 BossSkillSO 的发射点字段(如有)。");
|
||||
report.Add("★ 回旋扇收招/阶段过渡/击败演出等动画 Clip 待美术接入后在 ChaoFengBoss Inspector 指定。");
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 Boss_ChaoFeng.asset。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
@@ -1277,6 +1279,276 @@ namespace BaseGames.Editor
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// 放置 + 存盘为 Prefab + 注册 Addressable
|
||||
//
|
||||
// 规范:敌人/弹体 Prefab 须落到 Prefabs/ 下并注册 Addressable
|
||||
// (地址 = 文件名;分组/标签由 AddressableRules 推导,与校验器一致)。
|
||||
// 复用上方各 PlaceE00X / PlaceChaoFeng 搭建逻辑,确保场景搭建与 Prefab 产出同源。
|
||||
// 菜单:BaseGames → Scene → Save Prefab → …
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
private const string EnemyPrefabRoot = "Assets/_Game/Prefabs/Enemies";
|
||||
private const string ProjectilePrefabFolder = "Assets/_Game/Prefabs/Combat/Projectiles";
|
||||
|
||||
/// <summary>敌人 Prefab 存盘目标:键 = 根对象名(= 地址),值 = (目标文件夹, 规范标签之外的额外标签)。</summary>
|
||||
private static readonly Dictionary<string, (string folder, string[] extraLabels)> EnemyPrefabTargets =
|
||||
new()
|
||||
{
|
||||
{ "ENM_CaoZhi", (EnemyPrefabRoot + "/E001", null) },
|
||||
{ "ENM_HuangZhi", (EnemyPrefabRoot + "/E002", null) },
|
||||
// E003 幼蛭可被 E005 死亡时对象池生成 → 规范 Enemy 标签之外额外加 Poolable + Preload
|
||||
{ "ENM_YouZhi", (EnemyPrefabRoot + "/E003",
|
||||
new[] { AddressKeys.Labels.Poolable, AddressKeys.Labels.Preload }) },
|
||||
{ "ENM_ZhiMu", (EnemyPrefabRoot + "/E004", null) },
|
||||
{ "ENM_FeiZhi", (EnemyPrefabRoot + "/E005", null) },
|
||||
{ "ENM_Huan", (EnemyPrefabRoot + "/E006", null) },
|
||||
{ "ENM_ChaoFeng", (EnemyPrefabRoot + "/ChaoFeng", null) },
|
||||
};
|
||||
|
||||
/// <summary>弹体 Prefab 存盘目标:键 = 地址(= PoolKey),值 = Projectile 子类类型。</summary>
|
||||
private static readonly Dictionary<string, System.Type> ProjectileTypes =
|
||||
new()
|
||||
{
|
||||
{ "PROJ_Boomerang", typeof(ReturnProjectile) }, // 回旋扇(往返)
|
||||
{ "PROJ_ZhiMu_Acid", typeof(ArcProjectile) }, // E004 酸液(抛物线)
|
||||
{ "PROJ_FeiZhi_Acid", typeof(ArcProjectile) }, // E005 酸液(抛物线)
|
||||
{ "PROJ_WindStone", typeof(ArcProjectile) }, // 嘲风风石(抛物/落体)
|
||||
{ "PROJ_TornadoSmall", typeof(LinearProjectile) }, // 小龙卷(左右直线)
|
||||
{ "PROJ_TornadoLarge", typeof(LinearProjectile) }, // 大龙卷(定点,速度由 config 控)
|
||||
};
|
||||
|
||||
// ── 各敌人「放置 + 存盘」菜单 ─────────────────────────────────────────
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Enemy E001 (草蛭)", priority = 200)]
|
||||
public static void SaveE001Prefab() => PlaceAndSaveEnemyPrefab("ENM_CaoZhi", PlaceE001_CaoZhi);
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Enemy E002 (簧蛭)", priority = 201)]
|
||||
public static void SaveE002Prefab() => PlaceAndSaveEnemyPrefab("ENM_HuangZhi", PlaceE002_HuangZhi);
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Enemy E003 (幼蛭)", priority = 202)]
|
||||
public static void SaveE003Prefab() => PlaceAndSaveEnemyPrefab("ENM_YouZhi", PlaceE003_YouZhi_Enemy);
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Enemy E004 (蛭母)", priority = 203)]
|
||||
public static void SaveE004Prefab() => PlaceAndSaveEnemyPrefab("ENM_ZhiMu", PlaceE004_ZhiMu_Enemy);
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Enemy E005 (肥蛭)", priority = 204)]
|
||||
public static void SaveE005Prefab() => PlaceAndSaveEnemyPrefab("ENM_FeiZhi", PlaceE005_FeiZhi_Enemy);
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Enemy E006 (讙)", priority = 205)]
|
||||
public static void SaveE006Prefab() => PlaceAndSaveEnemyPrefab("ENM_Huan", PlaceE006_Huan);
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Boss 嘲风 (ChaoFeng)", priority = 206)]
|
||||
public static void SaveChaoFengPrefab() => PlaceAndSaveEnemyPrefab("ENM_ChaoFeng", PlaceChaoFeng);
|
||||
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/★ All Enemies + Boss", priority = 210)]
|
||||
public static void SaveAllEnemyPrefabs()
|
||||
{
|
||||
PlaceAndSaveEnemyPrefab("ENM_CaoZhi", PlaceE001_CaoZhi, removeSceneInstance: true);
|
||||
PlaceAndSaveEnemyPrefab("ENM_HuangZhi", PlaceE002_HuangZhi, removeSceneInstance: true);
|
||||
PlaceAndSaveEnemyPrefab("ENM_YouZhi", PlaceE003_YouZhi_Enemy, removeSceneInstance: true);
|
||||
PlaceAndSaveEnemyPrefab("ENM_ZhiMu", PlaceE004_ZhiMu_Enemy, removeSceneInstance: true);
|
||||
PlaceAndSaveEnemyPrefab("ENM_FeiZhi", PlaceE005_FeiZhi_Enemy, removeSceneInstance: true);
|
||||
PlaceAndSaveEnemyPrefab("ENM_Huan", PlaceE006_Huan, removeSceneInstance: true);
|
||||
PlaceAndSaveEnemyPrefab("ENM_ChaoFeng", PlaceChaoFeng, removeSceneInstance: true);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("[SceneObjectPlacer] 已批量生成全部敌人/Boss Prefab 并注册 Addressable。");
|
||||
}
|
||||
|
||||
// ── 各弹体「放置 + 存盘」菜单 ─────────────────────────────────────────
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_Boomerang", priority = 220)]
|
||||
public static void SaveBoomerangPrefab() => PlaceAndSaveProjectile("PROJ_Boomerang");
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_ZhiMu_Acid", priority = 221)]
|
||||
public static void SaveZhiMuAcidPrefab() => PlaceAndSaveProjectile("PROJ_ZhiMu_Acid");
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_FeiZhi_Acid", priority = 222)]
|
||||
public static void SaveFeiZhiAcidPrefab() => PlaceAndSaveProjectile("PROJ_FeiZhi_Acid");
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_WindStone", priority = 223)]
|
||||
public static void SaveWindStonePrefab() => PlaceAndSaveProjectile("PROJ_WindStone");
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_TornadoSmall", priority = 224)]
|
||||
public static void SaveTornadoSmallPrefab() => PlaceAndSaveProjectile("PROJ_TornadoSmall");
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/Projectile PROJ_TornadoLarge", priority = 225)]
|
||||
public static void SaveTornadoLargePrefab() => PlaceAndSaveProjectile("PROJ_TornadoLarge");
|
||||
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/★ All Projectiles", priority = 230)]
|
||||
public static void SaveAllProjectilePrefabs()
|
||||
{
|
||||
foreach (var key in ProjectileTypes.Keys)
|
||||
PlaceAndSaveProjectile(key, removeSceneInstance: true);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("[SceneObjectPlacer] 已批量生成全部弹体 Prefab 并注册 Addressable。");
|
||||
}
|
||||
|
||||
private const string ProjectileConfigFolder = "Assets/_Game/Data/Combat/Projectiles";
|
||||
|
||||
/// <summary>
|
||||
/// 弹体配置默认值:键 = PoolKey(= Prefab 地址),值 = (Speed, Lifetime, LaunchAngleDeg, GravityScale)。
|
||||
/// ArcProjectile 用 LaunchAngleDeg + GravityScale 形成抛物线;LinearProjectile 用 Speed 直线;
|
||||
/// ReturnProjectile(回旋扇)无重力、速度由弹体脚本控制。数值为占位,策划可在 Inspector 调整。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, (float speed, float lifetime, float launchAngle, float gravity)> ProjectileConfigDefs =
|
||||
new()
|
||||
{
|
||||
{ "PROJ_Boomerang", (10f, 6f, 0f, 0f) }, // 回旋扇:直线飞出,脚本控制往返
|
||||
{ "PROJ_ZhiMu_Acid", ( 9f, 4f, 45f, 1f) }, // E004 酸液:抛物线
|
||||
{ "PROJ_FeiZhi_Acid", ( 9f, 4f, 45f, 1f) }, // E005 酸液:抛物线
|
||||
{ "PROJ_WindStone", ( 6f, 4f, -90f, 2.5f) }, // 风石:向下落体
|
||||
{ "PROJ_TornadoSmall", ( 7f, 4f, 0f, 0f) }, // 小龙卷:水平直线
|
||||
{ "PROJ_TornadoLarge", ( 0f, 5f, 0f, 0f) }, // 大龙卷:定点驻留
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 创建 6 个 <see cref="ProjectileConfigSO"/>(PoolKey 与弹体 Prefab 地址一致),
|
||||
/// 绑定默认 DamageSource。配置 SO 经 Inspector 引用,不注册 Addressable。
|
||||
/// </summary>
|
||||
[MenuItem("BaseGames/Scene/Save Prefab/★ Projectile Configs (SO)", priority = 231)]
|
||||
public static void CreateProjectileConfigs()
|
||||
{
|
||||
var report = new List<string>();
|
||||
var enemyDmg = FindFirstAsset("CMB_DS_EnemyBody") as DamageSourceSO;
|
||||
var bossDmg = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody") as DamageSourceSO;
|
||||
|
||||
foreach (var kv in ProjectileConfigDefs)
|
||||
{
|
||||
string poolKey = kv.Key;
|
||||
var cfg = EditorScaffoldUtils.CreateSOAsset<ProjectileConfigSO>(ProjectileConfigFolder, $"{poolKey}_Config");
|
||||
if (cfg == null)
|
||||
cfg = AssetDatabase.LoadAssetAtPath<ProjectileConfigSO>($"{ProjectileConfigFolder}/{poolKey}_Config.asset");
|
||||
if (cfg == null) { report.Add($"✗ 创建失败:{poolKey}_Config"); continue; }
|
||||
|
||||
cfg.PoolKey = poolKey;
|
||||
cfg.Speed = kv.Value.speed;
|
||||
cfg.Lifetime = kv.Value.lifetime;
|
||||
cfg.LaunchAngleDeg = kv.Value.launchAngle;
|
||||
cfg.GravityScale = kv.Value.gravity;
|
||||
cfg.DamageSource = poolKey.Contains("Acid") ? enemyDmg : bossDmg;
|
||||
EditorUtility.SetDirty(cfg);
|
||||
report.Add($"✅ {poolKey}_Config (PoolKey={poolKey}, spd={kv.Value.speed}, grav={kv.Value.gravity})");
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("[SceneObjectPlacer] ProjectileConfigSO 创建完成。\n " + string.Join("\n ", report));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调用现有 PlaceE00X / PlaceChaoFeng 在场景中搭建敌人,随后存盘为 Prefab 并注册 Addressable。
|
||||
/// </summary>
|
||||
/// <param name="key">根对象名(同时作为 Prefab 文件名与 Addressable 地址)。</param>
|
||||
/// <param name="placer">现有的放置委托。</param>
|
||||
/// <param name="removeSceneInstance">true 时存盘后删除场景实例(批量生成时避免堆叠)。</param>
|
||||
public static void PlaceAndSaveEnemyPrefab(string key, System.Action placer, bool removeSceneInstance = false)
|
||||
{
|
||||
if (!EnemyPrefabTargets.TryGetValue(key, out var target))
|
||||
{
|
||||
Debug.LogError($"[SceneObjectPlacer] 未登记的敌人 Prefab 键:{key}");
|
||||
return;
|
||||
}
|
||||
|
||||
placer();
|
||||
GameObject root = Selection.activeGameObject;
|
||||
if (root == null)
|
||||
{
|
||||
Debug.LogError($"[SceneObjectPlacer] {key}:放置后未取得根对象,已跳过 Prefab 存盘。");
|
||||
return;
|
||||
}
|
||||
|
||||
var report = new List<string>();
|
||||
string prefabPath = SaveRootAsPrefab(root, key, target.folder, target.extraLabels, report);
|
||||
|
||||
if (removeSceneInstance && !string.IsNullOrEmpty(prefabPath))
|
||||
{
|
||||
var sc = root.scene;
|
||||
Undo.DestroyObjectImmediate(root);
|
||||
if (sc.IsValid())
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(sc);
|
||||
}
|
||||
else if (root != null)
|
||||
{
|
||||
EditorUtility.SetDirty(root);
|
||||
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(root.scene);
|
||||
}
|
||||
|
||||
Debug.Log($"[SceneObjectPlacer] {key} Prefab 流程完成。\n " + string.Join("\n ", report));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 搭建一个弹体 GameObject(Rigidbody2D + Collider(trigger) + HitBox + PooledObject + 指定 Projectile 子类),
|
||||
/// 存盘为 Prefab 并注册 Addressable。运行时由发射方 Initialize(ProjectileConfigSO,...) 注入速度/重力/伤害源。
|
||||
/// </summary>
|
||||
public static void PlaceAndSaveProjectile(string key, bool removeSceneInstance = false)
|
||||
{
|
||||
if (!ProjectileTypes.TryGetValue(key, out var projType))
|
||||
{
|
||||
Debug.LogError($"[SceneObjectPlacer] 未登记的弹体键:{key}");
|
||||
return;
|
||||
}
|
||||
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName($"Place {key}");
|
||||
|
||||
GameObject go = new GameObject(key);
|
||||
Undo.RegisterCreatedObjectUndo(go, $"Place {key}");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "EnemyProjectile", report);
|
||||
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||||
rb.bodyType = RigidbodyType2D.Dynamic; // ArcProjectile 运行时按 config 设置 gravityScale
|
||||
rb.gravityScale = 0f; // 默认无重力(直线/回旋扇);抛物线弹由 config 注入
|
||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||||
|
||||
CircleCollider2D col = GetOrAddComponent<CircleCollider2D>(go);
|
||||
col.isTrigger = true;
|
||||
col.radius = 0.25f;
|
||||
|
||||
HitBox hitBox = GetOrAddComponent<HitBox>(go); // Projectile [RequireComponent(HitBox)]
|
||||
GetOrAddComponent<PooledObject>(go); // 对象池归还所需
|
||||
Undo.AddComponent(go, projType); // Projectile 子类
|
||||
SetupSpriteRenderer(go);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null)
|
||||
AssignReference(hitBox, "_defaultSource", dmgSrc, report);
|
||||
else
|
||||
report.Add("未找到 CMB_DS_EnemyBody;HitBox._defaultSource 未绑定(运行时也会用 ProjectileConfigSO.DamageSource)。");
|
||||
|
||||
report.Add($"弹体类型:{projType.Name};Layer=EnemyProjectile;已挂 HitBox + PooledObject。");
|
||||
report.Add($"★ 创建对应 ProjectileConfigSO,其 PoolKey 必须 = \"{key}\"(与地址一致)。");
|
||||
report.Add("★ 速度/重力/伤害源由发射方在 Initialize(ProjectileConfigSO,...) 时注入,无需序列化到 Prefab。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
|
||||
string prefabPath = SaveRootAsPrefab(go, key, ProjectilePrefabFolder, null, report);
|
||||
if (removeSceneInstance && !string.IsNullOrEmpty(prefabPath))
|
||||
Undo.DestroyObjectImmediate(go);
|
||||
|
||||
Debug.Log($"[SceneObjectPlacer] {key} 弹体 Prefab 流程完成。\n " + string.Join("\n ", report));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将场景根对象存盘为 Prefab(已存在则弹窗确认覆盖)并注册 Addressable,返回 Prefab 路径(失败/取消返回 null)。
|
||||
/// </summary>
|
||||
private static string SaveRootAsPrefab(GameObject root, string fileName, string folder,
|
||||
string[] extraLabels, List<string> report)
|
||||
{
|
||||
EditorScaffoldUtils.EnsureFolder(folder);
|
||||
AssetDatabase.Refresh();
|
||||
string prefabPath = $"{folder}/{fileName}.prefab";
|
||||
|
||||
if (System.IO.File.Exists(prefabPath)
|
||||
&& !EditorUtility.DisplayDialog("Prefab 已存在",
|
||||
$"{prefabPath}\n已存在,覆盖?", "覆盖", "取消"))
|
||||
{
|
||||
report.Add($"用户取消覆盖:{prefabPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
GameObject prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(root, prefabPath, InteractionMode.UserAction);
|
||||
if (prefab == null)
|
||||
{
|
||||
report.Add($"✗ Prefab 存盘失败:{prefabPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
report.Add($"✅ 已存盘 Prefab:{prefabPath}");
|
||||
AddressableRegistrar.Register(prefabPath, fileName, extraLabels, report);
|
||||
return prefabPath;
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Hazard (LethalTrap)", priority = 120)]
|
||||
public static void PlaceLethalTrap()
|
||||
{
|
||||
|
||||
100
Assets/_Game/Scripts/Editor/Shared/AddressableRegistrar.cs
Normal file
100
Assets/_Game/Scripts/Editor/Shared/AddressableRegistrar.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 可复用的 Addressable 注册器:把资产登记到正确的分组并按规范打标。
|
||||
///
|
||||
/// 分组与标签均由 <see cref="AddressableRules"/> 推导(规范来源:
|
||||
/// Docs/Standards/AddressablesLabelSpec.md §3 / AssetFolderSpec.md §8),
|
||||
/// 因此结果与 <c>AddressableManagerWindow</c>、<c>AddressKeyValidator</c> 完全一致。
|
||||
///
|
||||
/// 供各脚手架(如 <c>SceneObjectPlacerTool</c> 创建 Prefab 后)一键注册,
|
||||
/// 避免每个工具重复实现 Addressables Settings API。
|
||||
/// </summary>
|
||||
public static class AddressableRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// 将指定路径的资产注册为 Addressable。
|
||||
/// 地址默认取文件名(可用 <paramref name="addressOverride"/> 覆盖);
|
||||
/// 分组与标签按 <see cref="AddressableRules"/> 推导,
|
||||
/// <paramref name="extraLabels"/> 在规范标签之上追加(如 E003 幼蛭额外的 Poolable/Preload)。
|
||||
/// </summary>
|
||||
/// <returns>最终写入的地址;Addressables 未初始化或资产不存在时返回 null。</returns>
|
||||
public static string Register(string assetPath, string addressOverride = null,
|
||||
IEnumerable<string> extraLabels = null, List<string> report = null)
|
||||
{
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null)
|
||||
{
|
||||
report?.Add("Addressable Asset Settings 不存在(未初始化),已跳过 Addressable 注册。");
|
||||
return null;
|
||||
}
|
||||
|
||||
string guid = AssetDatabase.AssetPathToGUID(assetPath);
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
{
|
||||
report?.Add($"资产不存在,无法注册 Addressable:{assetPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
string address = string.IsNullOrEmpty(addressOverride)
|
||||
? System.IO.Path.GetFileNameWithoutExtension(assetPath)
|
||||
: addressOverride;
|
||||
string groupName = AddressableRules.GetExpectedGroup(address);
|
||||
var group = groupName != null ? EnsureGroup(settings, groupName) : settings.DefaultGroup;
|
||||
|
||||
var entry = settings.FindAssetEntry(guid)
|
||||
?? settings.CreateOrMoveEntry(guid, group, false, false);
|
||||
if (entry == null)
|
||||
{
|
||||
report?.Add($"创建 Addressable 条目失败:{assetPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
entry.address = address;
|
||||
settings.MoveEntry(entry, group, false, false);
|
||||
|
||||
var labels = new HashSet<string>(AddressableRules.GetExpectedLabels(address));
|
||||
if (extraLabels != null)
|
||||
foreach (var l in extraLabels)
|
||||
if (!string.IsNullOrWhiteSpace(l)) labels.Add(l);
|
||||
foreach (var l in labels)
|
||||
SetLabel(settings, entry, l);
|
||||
|
||||
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, entry, true);
|
||||
report?.Add($"✅ Addressable 注册:{address} → 分组 {group.name}"
|
||||
+ (labels.Count > 0 ? $",标签 [{string.Join(", ", labels.OrderBy(x => x))}]" : ",无标签"));
|
||||
return address;
|
||||
}
|
||||
|
||||
private static AddressableAssetGroup EnsureGroup(AddressableAssetSettings settings, string name)
|
||||
{
|
||||
var existing = settings.groups.FirstOrDefault(g => g != null && g.name == name);
|
||||
if (existing != null) return existing;
|
||||
|
||||
var tmpl = settings.GroupTemplateObjects.FirstOrDefault() as AddressableAssetGroupTemplate;
|
||||
var schemas = tmpl != null ? new List<AddressableAssetGroupSchema>(tmpl.SchemaObjects) : null;
|
||||
var created = settings.CreateGroup(name, false, false, true, schemas);
|
||||
if (created != null)
|
||||
Debug.Log($"[AddressableRegistrar] 已自动创建分组:{name}");
|
||||
return created ?? settings.DefaultGroup;
|
||||
}
|
||||
|
||||
private static void SetLabel(AddressableAssetSettings settings, AddressableAssetEntry entry, string label)
|
||||
{
|
||||
if (!settings.GetLabels().Contains(label))
|
||||
{
|
||||
settings.AddLabel(label, true);
|
||||
Debug.Log($"[AddressableRegistrar] 已创建标签:{label}");
|
||||
}
|
||||
entry.SetLabel(label, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8637b71204995e4bbb5540e205ba431
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user